🎉 Add full teams administration.

This commit is contained in:
Andrey Antukh 2020-10-05 18:20:39 +02:00 committed by Hirunatan
parent f6830b4b85
commit 142036891a
62 changed files with 3175 additions and 1606 deletions

File diff suppressed because it is too large Load diff

View file

@ -35,12 +35,16 @@
//################################################# //#################################################
@import 'common/framework'; @import 'common/framework';
@import 'main/partials/modal';
@import 'main/partials/forms';
@import "main/partials/texts";
@import 'main/partials/context-menu';
@import 'main/partials/dropdown';
//################################################# //#################################################
// Partials // Partials
//################################################# //#################################################
@import "main/partials/texts";
@import "main/partials/viewer"; @import "main/partials/viewer";
@import "main/partials/viewer-header"; @import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails"; @import "main/partials/viewer-thumbnails";
@ -48,17 +52,16 @@
@import 'main/partials/activity-bar'; @import 'main/partials/activity-bar';
@import 'main/partials/color-palette'; @import 'main/partials/color-palette';
@import 'main/partials/colorpicker'; @import 'main/partials/colorpicker';
@import 'main/partials/context-menu';
@import 'main/partials/dashboard'; @import 'main/partials/dashboard';
@import 'main/partials/dashboard-header'; @import 'main/partials/dashboard-header';
@import 'main/partials/dashboard-grid'; @import 'main/partials/dashboard-grid';
@import 'main/partials/dashboard-sidebar'; @import 'main/partials/dashboard-sidebar';
@import 'main/partials/dashboard-team';
@import 'main/partials/dashboard-settings';
@import 'main/partials/debug-icons-preview'; @import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label'; @import 'main/partials/editable-label';
@import 'main/partials/forms';
@import 'main/partials/left-toolbar'; @import 'main/partials/left-toolbar';
@import 'main/partials/loader'; @import 'main/partials/loader';
@import 'main/partials/modal';
@import 'main/partials/project-bar'; @import 'main/partials/project-bar';
@import 'main/partials/sidebar'; @import 'main/partials/sidebar';
@import 'main/partials/sidebar-align-options'; @import 'main/partials/sidebar-align-options';

View file

@ -10,7 +10,7 @@
.auth { .auth {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
grid-template-columns: 388px auto; grid-template-columns: 510px auto;
} }
.auth-sidebar { .auth-sidebar {

View file

@ -71,6 +71,7 @@
width: 100%; width: 100%;
z-index: 1; z-index: 1;
} }
&:hover .overlay { &:hover .overlay {
display: block; display: block;
opacity: 1; opacity: 1;
@ -118,6 +119,13 @@
width: 100%; width: 100%;
} }
.edit-wrapper {
.element-title {
padding: 3px;
height: 25px;
}
}
} }
.item-badge { .item-badge {

View file

@ -15,6 +15,7 @@
padding: $x-small $small; padding: $x-small $small;
position: relative; position: relative;
z-index: 10; z-index: 10;
justify-content: space-between;
.element-name { .element-name {
margin-right: $small; margin-right: $small;
@ -22,7 +23,6 @@
.btn-secondary { .btn-secondary {
flex-shrink: 0; flex-shrink: 0;
margin-left: auto;
z-index: 10; z-index: 10;
height: 32px; height: 32px;
} }
@ -35,16 +35,17 @@
} }
nav { nav {
display: flex;
width: 300px;
justify-content: center;
z-index: 1;
margin-top: 39px;
ul { ul {
align-items: center;
bottom: 0;
display: flex; display: flex;
align-items: center;
font-size: $fs15; font-size: $fs15;
justify-content: center; justify-content: center;
margin: auto;
position: absolute;
width: 100%;
z-index: 1;
} }
li { li {
@ -63,7 +64,7 @@
} }
&.current { &.active {
a { a {
color: $color-black; color: $color-black;
border-color: $color-primary; border-color: $color-primary;
@ -73,11 +74,18 @@
} }
.dashboard-title { .dashboard-title {
color: $color-black;
display: flex; display: flex;
flex-shrink: 0; h1 {
font-size: $fs18; color: $color-black;
z-index: 10; display: flex;
flex-shrink: 0;
font-size: $fs18;
z-index: 10;
}
.context-menu.is-open {
margin-top: 10px;
}
} }
.icon { .icon {

View file

@ -0,0 +1,124 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This Source Code Form is "Incompatible With Secondary Licenses", as
// defined by the Mozilla Public License, v. 2.0.
//
// Copyright (c) 2020 UXBOX Labs SL
.dashboard-sidebar {
&.settings {
.back-to-dashboard {
padding: 18px;
font-size: $fs14;
cursor: pointer;
display: flex;
.icon {
display: flex;
align-items: center;
margin-right: 14px;
}
.text {
color: $color-gray-60;
}
svg {
fill: $color-gray-60;
transform: rotate(90deg);
width: 12px;
height: 12px;
}
}
}
}
.dashboard-settings {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
.form-container {
margin-top: 50px;
display: flex;
max-width: 368px;
width: 100%;
&.two-columns {
max-width: 536px;
justify-content: space-between;
flex-direction: row;
}
}
.avatar-form {
display: flex;
flex-direction: column;
width: 120px;
min-width: 120px;
img {
border-radius: 50%;
flex-shrink: 0;
height: 120px;
margin-right: $medium;
width: 120px;
}
.image-change-field {
position: relative;
width: 120px;
height: 120px;
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
width: 121px;
height: 121px;
border-radius: 50%;
font-size: $fs24;
color: $color-white;
line-height: 120px;
text-align: center;
background: $color-primary-dark;
z-index: 14;
}
input[type=file] {
width: 120px;
height: 120px;
position: absolute;
opacity: 0;
cursor: pointer;
top: 0;
z-index: 15;
}
&:hover {
img {display: none;}
.update-overlay {opacity: 1};
}
}
}
.profile-form {
display: flex;
flex-direction: column;
max-width: 368px;
width: 100%;
}
.options-form,
.password-form {
h2 {
font-size: $fs14;
margin-bottom: 20px;
}
}
}

View file

@ -25,54 +25,8 @@
padding: 0; padding: 0;
hr { hr {
margin: 10px 15px;
border-color: $color-gray-10; border-color: $color-gray-10;
} margin: 1rem 15px;
}
.dropdown {
position: absolute;
max-height: 30rem;
overflow-y: auto;
background-color: $color-white;
border-radius: 4px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
hr {
margin: 0;
border-color: $color-gray-10;
}
li {
color: $color-gray-60;
cursor: pointer;
font-size: $fs14;
display: flex;
padding: 13px 16px;
&.title {
font-weight: 600;
cursor: default;
}
&.team-item {
display: flex;
.icon {
display: flex;
align-items: center;
padding-right: 10px;
svg {
width: 20px;
height: 20px;
fill: $color-gray-60;
}
}
}
&:hover {
background-color: $color-primary-lighter;
}
} }
} }
@ -87,6 +41,10 @@
z-index: 12; z-index: 12;
max-height: 30rem; max-height: 30rem;
min-width: 189px; min-width: 189px;
li {
height: 35px;
}
} }
.options-dropdown { .options-dropdown {
@ -126,28 +84,36 @@
padding: 0px 10px; padding: 0px 10px;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
}
.team-name { .team-name {
flex-grow: 1; flex-grow: 1;
display: flex;
align-items: center;
.team-icon {
display: flex; display: flex;
align-items: center; align-items: center;
padding-right: 10px;
.team-text { svg {
color: $color-gray-60; width: 23px;
height: 23px;
fill: $color-gray-60;
}
img {
border-radius: 50%;
flex-shrink: 0;
height: 23px;
width: 23px;
} }
} }
} .team-text {
color: $color-gray-60;
.team-icon { @include text-ellipsis;
display: flex; width: 100px;
align-items: center;
padding-right: 10px;
svg {
width: 23px;
height: 23px;
fill: $color-gray-60;
} }
} }
@ -396,13 +362,13 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: $small; padding: 10px 15px;
position: relative; position: relative;
span { span {
@include text-ellipsis; @include text-ellipsis;
color: $color-black; color: $color-black;
margin: $small; margin: 10px 5px;
font-size: $fs12; font-size: $fs12;
max-width: 135px; max-width: 135px;
} }
@ -416,22 +382,14 @@
.dropdown { .dropdown {
left: 15px; left: 15px;
bottom: 50px;
z-index: 12;
max-height: 30rem;
min-width: 189px;
position: absolute;
bottom: 45px; bottom: 45px;
z-index: 12; min-width: 189px;
width: 170px; width: 170px;
@include animation(0,.2s,fadeInUp); @include animation(0,.2s,fadeInUp);
li { li {
display: flex; font-size: $fs12;
align-items: center;
font-size: $fs13;
padding: 5px 10px; padding: 5px 10px;
svg { svg {

View file

@ -0,0 +1,240 @@
.dashboard-invite-modal {
top: 65px;
right: 13px;
padding: 14px;
box-shadow: 0px 4px 8px rgba($color-black, 0.25);
border-radius: 8px;
width: 414px;
position: fixed;
form {
width: 100%;
}
.form-row {
display: flex;
flex-direction: row;
margin: 15px 0px;
}
.custom-input {
width: 272px;
margin-right: 10px;
}
.custom-select {
width: 103px
}
.action-buttons {
display: flex;
justify-content: center;
input[type=submit] {
margin-bottom: 0px;
}
}
.title {
color: $color-black;
}
}
.dashboard-team-members {
.table-field {
// border: 1px solid red;
&.name {
width: 43%;
min-width: 300px;
}
&.email {
width: 43%;
min-width: 300px;
}
&.permissions {
min-width: 120px;
user-select: none;
cursor: default;
position: relative;
}
}
.dropdown {
position: absolute;
max-height: 30rem;
overflow-y: auto;
background-color: $color-white;
border-radius: 4px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12;
top: 30px;
left: 0px;
width: 125px;
hr {
margin: 0;
border-color: $color-gray-10;
}
li {
display: flex;
align-items: center;
color: $color-gray-60;
cursor: pointer;
font-size: $fs12;
height: 31px;
padding: 5px 16px;
&.title {
font-weight: 600;
cursor: default;
}
&:hover {
background-color: $color-primary-lighter;
}
}
}
}
.dashboard-team-settings {
.team-settings {
display: flex;
justify-content: center;
margin-top: 16px;
svg {
width: 20px;
height: 20px;
}
.horizontal-blocks {
display: flex;
max-width: 1010px;
justify-content: space-between;
width: 100%;
}
.block {
display: flex;
max-width: 324px;
width: 324px;
height: 100px;
background-color: $color-white;
flex-direction: column;
padding: 12px;
.label {
font-size: 13px;
}
}
.info-block {
position: relative;
.name {
margin-top: 10px;
font-size: $fs32;
color: $color-black;
@include text-ellipsis;
margin-right: 90px;
}
.icon {
position: absolute;
padding: 15px;
width: 100px;
height: 100px;
right: 0px;
top: 0px;
img {
border-radius: 50%;
width: 70px;
height: 70px;
}
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 70px;
height: 70px;
border-radius: 50%;
color: $color-white;
background: $color-primary-dark;
z-index: 14;
svg { fill: $color-white; }
}
&:hover {
.update-overlay {
opacity: 1;
}
}
}
}
.owner-block {
img {
width: 30px;
height: 30px;
border-radius: 50%;
}
svg {
width: 12px;
height: 12px;
fill: $color-primary-dark;
}
.owner {
margin-top: 5px;
display: flex;
align-items: center;
color: $color-black;
.icon {
margin-right: 12px;
}
}
.summary {
margin-top: 5px;
color: $color-primary-dark;
.icon {
padding: 0px 10px;
margin-right: 12px;
}
}
}
.stats-block {
svg {
fill: $color-black;
}
.projects,
.files {
margin-top: 7px;
display: flex;
align-items: center;
color: $color-black;
.icon {
display: flex;
align-items: center;
padding: 0px 2px;
margin-right: 14px;
}
}
}
}
}

View file

@ -7,7 +7,7 @@
// //
// Copyright (c) 2020 UXBOX Labs SL // Copyright (c) 2020 UXBOX Labs SL
.dashboard-grid-container { .dashboard-container {
background-color: $color-dashboard; background-color: $color-dashboard;
border-top-right-radius: $br-huge; border-top-right-radius: $br-huge;
border-top-left-radius: $br-huge; border-top-left-radius: $br-huge;
@ -15,7 +15,6 @@
margin-right: $small; margin-right: $small;
overflow-y: auto; overflow-y: auto;
&.search { &.search {
margin-top: 10px; margin-top: 10px;
} }
@ -74,3 +73,88 @@
} }
} }
} }
.dashboard-table {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
font-size: $fs16;
.table-header {
max-width: 1040px;
display: flex;
background-color: $color-white;
color: $color-gray-30;
width: 100%;
height: 40px;
align-items: center;
padding: 0px 16px;
}
.table-rows {
display: flex;
flex-direction: column;
max-width: 1040px;
width: 100%;
margin-top: 20px;
color: $color-black;
}
.table-row {
display: flex;
width: 100%;
height: 45px;
align-items: center;
padding: 0px 16px;
}
.table-field {
display: flex;
align-items: center;
.icon {
padding-left: 10px;
cursor: pointer;
}
}
svg {
width: 10px;
height: 10px;
fill: $color-black;
}
}
.edit-wrapper {
border: 1px solid $color-gray-10;
border-radius: $br-small;
display: flex;
position: relative;
input.element-title {
border: 0;
height: 30px;
padding: 5px;
margin: 0;
width: 100%;
background-color: $color-white;
}
.close {
cursor: pointer;
position: absolute;
top: 1px;
right: 2px;
svg {
fill: $color-gray-30;
height: 15px;
transform: rotate(45deg) translateY(7px);
width: 15px;
margin: 0;
}
}
}

View file

@ -0,0 +1,32 @@
.dropdown {
position: absolute;
max-height: 30rem;
background-color: $color-white;
border-radius: 4px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12;
hr {
margin: 0 !important;
border-color: $color-gray-10;
}
li {
display: flex;
align-items: center;
color: $color-gray-60;
cursor: pointer;
font-size: $fs12;
height: 31px;
padding: 5px 16px;
&.title {
font-weight: 600;
cursor: default;
}
&:hover {
background-color: $color-primary-lighter;
}
}
}

View file

@ -14,9 +14,11 @@ textarea {
} }
} }
.form-container,
.generic-form { .generic-form {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column;
.forms-container { .forms-container {
display: flex; display: flex;
@ -31,6 +33,18 @@ textarea {
// flex-basis: 368px; // flex-basis: 368px;
} }
.fields-row {
margin-bottom: 20px;
flex-direction: column;
.options {
display: flex;
justify-content: flex-end;
font-size: $fs14;
margin-top: 13px;
}
}
.field { .field {
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -61,7 +75,7 @@ textarea {
.links { .links {
display: flex; display: flex;
font-size: $fs11; font-size: $fs14;
justify-content: space-between; justify-content: space-between;
margin-bottom: $medium; margin-bottom: $medium;
@ -72,13 +86,13 @@ textarea {
} }
.link-entry { .link-entry {
font-size: $fs12; font-size: $fs14;
color: $color-gray-40; color: $color-gray-40;
margin-bottom: 10px; margin-bottom: 10px;
} }
.link-entry a { .link-entry a {
font-size: $fs12; font-size: $fs14;
color: $color-primary-dark; color: $color-primary-dark;
} }
} }
@ -93,7 +107,7 @@ textarea {
border-radius: 2px; border-radius: 2px;
border: 1px solid $color-gray-20; border: 1px solid $color-gray-20;
color: $color-gray-60; color: $color-gray-60;
font-size: $fs12; font-size: $fs14;
height: 40px; height: 40px;
margin: 0; margin: 0;
padding: 15px 15px 0 15px; padding: 15px 15px 0 15px;
@ -109,7 +123,7 @@ textarea {
} }
label { label {
font-size: $fs10; font-size: $fs12;
color: $color-gray-30; color: $color-gray-30;
position: absolute; position: absolute;
left: 15px; left: 15px;
@ -181,13 +195,13 @@ textarea {
.hint { .hint {
padding: 4px; padding: 4px;
font-size: $fs10; font-size: $fs12;
} }
.error { .error {
color: $color-danger; color: $color-danger;
padding: 4px; padding: 4px;
font-size: $fs10; font-size: $fs12;
} }
} }
@ -198,13 +212,13 @@ textarea {
justify-content: center; justify-content: center;
label { label {
font-size: $fs10; font-size: $fs12;
color: $color-gray-30; color: $color-gray-30;
} }
select { select {
cursor: pointer; cursor: pointer;
font-size: $fs12; font-size: $fs14;
border: 0px; border: 0px;
opacity: 0; opacity: 0;
z-index: 10; z-index: 10;
@ -224,6 +238,7 @@ textarea {
justify-content: center; justify-content: center;
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
padding-left: 15px;
} }
@ -235,8 +250,6 @@ textarea {
border-radius: 2px; border-radius: 2px;
border: 1px solid $color-gray-20; border: 1px solid $color-gray-20;
height: 40px; height: 40px;
padding-left: 15px;
padding-right: 15px;
&.invalid { &.invalid {
border-color: $color-danger; border-color: $color-danger;
@ -261,7 +274,7 @@ textarea {
.value { .value {
color: $color-gray-60; color: $color-gray-60;
font-size: $fs12; font-size: $fs14;
width: 100%; width: 100%;
border: 0px; border: 0px;
padding: 0px; padding: 0px;
@ -273,7 +286,8 @@ textarea {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding-left: 10px; padding-left: 10px;
padding-right: 10px;
pointer-events: none;
svg { svg {
fill: $color-gray-30; fill: $color-gray-30;
@ -283,3 +297,4 @@ textarea {
} }
} }
} }

View file

@ -56,84 +56,140 @@
} }
} }
.change-email-modal { // NEW GEN MODALS
h2 {
font-size: $fs14; .modal-container {
margin-bottom: 20px; border-radius: 8px;
display: flex;
flex-direction: column;
width: 448px;
background-color: $color-dashboard;
.modal-header {
align-items: center;
background-color: $color-white;
border-radius: 8px 8px 0px 0px;
color: $color-black;
display: flex;
height: 63px;
justify-content: space-between;
} }
.confirmation { .modal-header-title {
.btn-primary { display: flex;
margin-bottom: 30px; align-items: center;
} font-size: $fs24;
padding-left: 16px;
.featured-note .icon svg { h2 {
fill: $color-success; margin: 0;
} }
}
.modal-close-button {
align-items: center;
cursor: pointer;
display: flex;
height: 30px;
justify-content: center;
margin-right: 16px;
width: 30px;
svg {
transform: rotate(45deg);
width: 16px;
height: 16px;
}
}
.modal-content {
display: flex;
flex-direction: column;
padding: 32px;
border-top: 1px solid $color-gray-10;
h3 {
color: $color-gray-40;
font-size: $fs16;
}
}
.modal-footer {
display: flex;
height: 63px;
padding: 0px 16px;
border-top: 1px solid $color-gray-10;
.action-buttons {
display: flex;
width: 100%;
height: 100%;
justify-content: flex-end;
// border: 1px solid red;
align-items: center;
input {
margin-bottom: 0px;
}
}
}
}
.change-email-modal {
h2 {
font-size: $fs18
}
h3 {
margin-bottom: 15px;
}
.modal-footer .action-buttons {
justify-content: space-around;
} }
} }
.confirm-dialog { .confirm-dialog {
background-color: $color-white; background-color: $color-white;
width: 23rem;
.modal-content { p {
padding: 20px 40px; font-size: $fs14;
color: $color-gray-40;
} }
.dialog-title { .action-buttons {
font-size: 24px;
color: $color-black;
font-weight: normal;
text-align: center;
}
.dialog-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 3rem;
width: 100%; width: 100%;
font-size: $fs14;
} }
.dialog-cancel-button { .cancel-button {
border: 1px solid $color-gray-30; border: 1px solid $color-gray-30;
background: $color-canvas; background: $color-canvas;
border-radius: 2px; border-radius: 3px;
padding: 0.5rem; padding: 0.5rem 1rem;
margin-right: 1rem;
justify-content: space-evenly;
margin-bottom: 0;
width: 100%;
cursor: pointer; cursor: pointer;
margin-right: 8px;
&:hover { &:hover {
background: $color-gray-20; background: $color-gray-20;
} }
} }
.dialog-accept-button { .accept-button {
width: 100%;
padding: 0.5rem;
border: 1px solid $color-danger; border: 1px solid $color-danger;
border-radius: 3px;
background: $color-danger; background: $color-danger;
color: $color-white; color: $color-white;
margin-bottom: 0;
cursor: pointer; cursor: pointer;
padding: 0.5rem 1rem;
&:hover { &:hover {
background: $color-danger-dark; background: $color-danger-dark;
} }
&.not-danger {
background: $color-primary;
color: $color-gray-60;
}
&.not-danger:hover {
background: $color-primary-dark;
}
} }
} }
.libraries-dialog { .libraries-dialog {

View file

@ -153,7 +153,7 @@
.change-email { .change-email {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-size: $fs12; font-size: $fs14;
color: $color-primary-dark; color: $color-primary-dark;
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 20px; margin-bottom: 20px;

View file

@ -8,7 +8,9 @@
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.config (ns app.config
(:require [app.util.object :as obj])) (:require
[app.util.object :as obj]
[cuerdas.core :as str]))
(this-as global (this-as global
(def default-language "en") (def default-language "en")
@ -24,4 +26,7 @@
(defn resolve-media-path (defn resolve-media-path
[path] [path]
(str media-uri "/" path)) (when path
(if (str/starts-with? path "data:")
path
(str media-uri "/" path))))

View file

@ -9,31 +9,24 @@
(ns app.main (ns app.main
(:require (:require
[hashp.core :include-macros true]
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.auth :refer [logout]] [app.main.data.auth :refer [logout]]
[app.main.data.users :as udu] [app.main.data.users :as udu]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui :as ui] [app.main.ui :as ui]
[app.main.ui.confirm]
[app.main.ui.modal :refer [modal]] [app.main.ui.modal :refer [modal]]
[app.main.worker] [app.main.worker]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.util.theme :as theme]
[app.util.router :as rt]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.router :as rt]
[app.util.storage :refer [storage]] [app.util.storage :refer [storage]]
[app.util.theme :as theme]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx]
;; MODALS [cljs.spec.alpha :as s]
[app.main.ui.settings.delete-account] [rumext.alpha :as mf]))
[app.main.ui.settings.change-email]
[app.main.ui.confirm]
[app.main.ui.workspace.colorpicker]
[app.main.ui.workspace.libraries]))
(declare reinit) (declare reinit)

View file

@ -151,19 +151,17 @@
;; --- Request Account Deletion ;; --- Request Account Deletion
(def request-account-deletion (defn request-account-deletion
(letfn [(on-error [{:keys [code] :as error}] [params]
(if (= :app.services.mutations.profile/owner-teams-with-people code) (ptk/reify ::request-account-deletion
(let [msg (tr "settings.notifications.profile-deletion-not-allowed")] ptk/WatchEvent
(rx/of (dm/error msg))) (watch [_ state stream]
(rx/empty)))] (let [{:keys [on-error on-success]
(ptk/reify ::request-account-deletion :or {on-error identity
ptk/WatchEvent on-success identity}} (meta params)]
(watch [_ state stream] (->> (rp/mutation :delete-profile {})
(rx/concat (rx/tap on-success)
(->> (rp/mutation :delete-profile {}) (rx/catch on-error))))))
(rx/map #(rt/nav :auth-goodbye))
(rx/catch on-error)))))))
;; --- Recovery Request ;; --- Recovery Request

View file

@ -164,7 +164,7 @@
(if shift? (if shift?
(change-stroke ids color nil nil) (change-stroke ids color nil nil)
(change-fill ids color nil nil)) (change-fill ids color nil nil))
(md/hide-modal))))] (md/hide))))]
(ptk/reify ::start-picker (ptk/reify ::start-picker
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]

View file

@ -14,6 +14,9 @@
[app.util.router :as rt] [app.util.router :as rt]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.timers :as ts] [app.util.timers :as ts]
[app.util.avatars :as avatars]
[app.main.data.media :as di]
[app.main.data.messages :as dm]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -29,10 +32,12 @@
(s/def ::created-at ::us/inst) (s/def ::created-at ::us/inst)
(s/def ::modified-at ::us/inst) (s/def ::modified-at ::us/inst)
(s/def ::is-pinned ::us/boolean) (s/def ::is-pinned ::us/boolean)
(s/def ::photo ::us/string)
(s/def ::team (s/def ::team
(s/keys :req-un [::id (s/keys :req-un [::id
::name ::name
::photo
::created-at ::created-at
::modified-at])) ::modified-at]))
@ -59,6 +64,13 @@
;; --- Fetch Team ;; --- Fetch Team
(defn assoc-team-avatar
[{:keys [photo name] :as team}]
(us/assert ::team team)
(cond-> team
(or (nil? photo) (empty? photo))
(assoc :photo (avatars/generate {:name name}))))
(defn fetch-team (defn fetch-team
[{:keys [id] :as params}] [{:keys [id] :as params}]
(letfn [(fetched [team state] (letfn [(fetched [team state]
@ -66,9 +78,21 @@
(ptk/reify ::fetch-team (ptk/reify ::fetch-team
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> (rp/query :team params) (let [profile (:profile state)]
(rx/map #(partial fetched %))))))) (->> (rp/query :team params)
(rx/map assoc-team-avatar)
(rx/map #(partial fetched %))))))))
(defn fetch-team-members
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(letfn [(fetched [members state]
(assoc-in state [:team-members id] (d/index-by :id members)))]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :team-members {:team-id id})
(rx/map #(partial fetched %)))))))
;; --- Fetch Projects ;; --- Fetch Projects
@ -83,6 +107,31 @@
(->> (rp/query :projects {:team-id team-id}) (->> (rp/query :projects {:team-id team-id})
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
(defn fetch-bundle
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::fetch-team
ptk/WatchEvent
(watch [_ state stream]
(let [profile (:profile state)]
(->> (rx/merge (ptk/watch (fetch-team params) state stream)
(ptk/watch (fetch-projects {:team-id id}) state stream))
(rx/catch (fn [{:keys [type code] :as error}]
(cond
(and (= :not-found type)
(not= id (:default-team-id profile)))
(rx/of (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})
(dm/error "Team does not found"))
(and (= :validation type)
(= :not-authorized code)
(not= id (:default-team-id profile)))
(rx/of (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})
(dm/error "Team does not found"))
:else
(rx/throw error)))))))))
;; --- Search Files ;; --- Search Files
@ -181,6 +230,114 @@
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
(defn update-team
[{:keys [id name] :as params}]
(us/assert ::team params)
(ptk/reify ::update-team
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:teams id :name] name))
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation! :update-team params)
(rx/ignore)))))
(defn update-team-photo
[{:keys [file team-id] :as params}]
(us/assert ::di/js-file file)
(us/assert ::us/uuid team-id)
(ptk/reify ::update-team-photo
ptk/WatchEvent
(watch [_ state stream]
(let [on-success di/notify-finished-loading
on-error #(do (di/notify-finished-loading)
(di/process-error %))
prepare #(hash-map :file % :team-id team-id)]
(di/notify-start-loading)
(->> (rx/of file)
(rx/map di/validate-file)
(rx/map prepare)
(rx/mapcat #(rp/mutation :update-team-photo %))
(rx/do on-success)
(rx/map #(fetch-team %))
(rx/catch on-error))))))
(defn update-team-member-role
[{:keys [team-id role member-id] :as params}]
(us/assert ::us/uuid team-id)
(us/assert ::us/uuid member-id)
(us/assert ::us/keyword role)
(ptk/reify ::update-team-member-role
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation! :update-team-member-role params)
(rx/mapcat #(rx/of (fetch-team-members {:id team-id})
(fetch-team {:id team-id})))))))
(defn delete-team-member
[{:keys [team-id member-id] :as params}]
(us/assert ::us/uuid team-id)
(us/assert ::us/uuid member-id)
(ptk/reify ::delete-team-member
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation! :delete-team-member params)
(rx/mapcat #(rx/of (fetch-team-members {:id team-id})
(fetch-team {:id team-id})))))))
(defn leave-team
[{:keys [id reassign-to] :as params}]
(us/assert ::team params)
(us/assert (s/nilable ::us/uuid) reassign-to)
(ptk/reify ::leave-team
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)]
(rx/concat
(when (uuid? reassign-to)
(->> (rp/mutation! :update-team-member-role {:team-id id
:role :owner
:member-id reassign-to})
(rx/ignore)))
(->> (rp/mutation! :leave-team {:id id})
(rx/tap on-success)
(rx/catch on-error)))))))
(defn invite-team-member
[{:keys [team-id email role] :as params}]
(us/assert ::us/uuid team-id)
(us/assert ::us/email email)
(us/assert ::us/keyword role)
(ptk/reify ::invite-team-member
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)]
(->> (rp/mutation! :invite-team-member params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-team
[{:keys [id] :as params}]
(us/assert ::team params)
(ptk/reify ::delete-team
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)]
(->> (rp/mutation! :delete-team {:id id})
(rx/tap on-success)
(rx/catch on-error))))))
(defn create-project (defn create-project
[{:keys [team-id] :as params}] [{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id) (us/assert ::us/uuid team-id)
@ -289,12 +446,12 @@
;; --- Set File shared ;; --- Set File shared
(defn set-file-shared (defn set-file-shared
[id is-shared] [{:keys [id project-id is-shared] :as params}]
{:pre [(uuid? id) (boolean? is-shared)]} (us/assert ::file params)
(ptk/reify ::set-file-shared (ptk/reify ::set-file-shared
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:files id :is-shared] is-shared)) (assoc-in state [:files project-id id :is-shared] is-shared))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]

View file

@ -8,30 +8,54 @@
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.modal (ns app.main.data.modal
(:refer-clojure :exclude [update])
(:require (:require
[potok.core :as ptk])) [potok.core :as ptk]
[app.main.store :as st]
[app.common.uuid :as uuid]
[cljs.core :as c]))
(defn show-modal [id type props] (defonce components (atom {}))
(ptk/reify ::show-modal
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc ::modal {:id id
:type type
:props props
:allow-click-outside false})))))
(defn hide-modal [] (defn show
([props]
(show (uuid/next) (:type props) props))
([type props] (show (uuid/next) type props))
([id type props]
(ptk/reify ::show-modal
ptk/UpdateEvent
(update [_ state]
(assoc state ::modal {:id id
:type type
:props props
:allow-click-outside false})))))
(defn hide
[]
(ptk/reify ::hide-modal (ptk/reify ::hide-modal
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (dissoc state ::modal))))
(dissoc ::modal)))))
(defn update-modal [options] (defn update
[options]
(ptk/reify ::update-modal (ptk/reify ::update-modal
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (c/update state ::modal merge options))))
(update ::modal merge options)))))
(defn show!
[type props]
(st/emit! (show type props)))
(defn allow-click-outside!
[]
(st/emit! (update {:allow-click-outside true})))
(defn disallow-click-outside!
[]
(st/emit! (update {:allow-click-outside false})))
(defn hide!
[]
(st/emit! (hide)))

View file

@ -106,6 +106,7 @@
(defn request-email-change (defn request-email-change
[{:keys [email] :as data}] [{:keys [email] :as data}]
(us/assert ::us/email email)
(ptk/reify ::request-email-change (ptk/reify ::request-email-change
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]

View file

@ -98,6 +98,14 @@
(seq params)) (seq params))
(send-mutation! id form))) (send-mutation! id form)))
(defmethod mutation :update-team-photo
[id params]
(let [form (js/FormData.)]
(run! (fn [[key val]]
(.append form (name key) val))
(seq params))
(send-mutation! id form)))
(defmethod mutation :login (defmethod mutation :login
[id params] [id params]
(let [uri (str cfg/public-uri "/api/login")] (let [uri (str cfg/public-uri "/api/login")]

View file

@ -0,0 +1,13 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.store)
(defmacro emitf
[& events]
`(fn []
(app.main.store/emit! ~@events)))

View file

@ -5,6 +5,7 @@
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.store (ns app.main.store
(:require-macros [app.main.store])
(:require (:require
[beicon.core :as rx] [beicon.core :as rx]
[okulary.core :as l] [okulary.core :as l]

View file

@ -9,11 +9,6 @@
(ns app.main.ui (ns app.main.ui
(:require (:require
[expound.alpha :as expound]
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]
[rumext.alpha :as mf]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -21,18 +16,22 @@
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.auth :refer [auth verify-token]] [app.main.ui.auth :refer [auth]]
[app.main.ui.auth.verify-token :refer [verify-token]]
[app.main.ui.cursors :as c]
[app.main.ui.dashboard :refer [dashboard]] [app.main.ui.dashboard :refer [dashboard]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.cursors :as c]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings] [app.main.ui.settings :as settings]
[app.main.ui.static :refer [not-found-page not-authorized-page]] [app.main.ui.static :refer [not-found-page not-authorized-page]]
[app.main.ui.viewer :refer [viewer-page]] [app.main.ui.viewer :refer [viewer-page]]
[app.main.ui.render :as render]
[app.main.ui.workspace :as workspace] [app.main.ui.workspace :as workspace]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.timers :as ts])) [app.util.timers :as ts]
[expound.alpha :as expound]
[potok.core :as ptk]
[rumext.alpha :as mf]))
;; --- Routes ;; --- Routes
@ -60,12 +59,13 @@
;; Used for export ;; Used for export
["/render-object/:file-id/:page-id/:object-id" :render-object] ["/render-object/:file-id/:page-id/:object-id" :render-object]
["/dashboard" ["/dashboard/team/:team-id"
["/team/:team-id" ["/members" :dashboard-team-members]
["/projects" :dashboard-projects] ["/settings" :dashboard-team-settings]
["/search" :dashboard-search] ["/projects" :dashboard-projects]
["/libraries" :dashboard-libraries] ["/search" :dashboard-search]
["/projects/:project-id" :dashboard-files]]] ["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]]
["/workspace/:project-id/:file-id" :workspace]]) ["/workspace/:project-id/:file-id" :workspace]])
@ -109,7 +109,9 @@
(:dashboard-search (:dashboard-search
:dashboard-projects :dashboard-projects
:dashboard-files :dashboard-files
:dashboard-libraries) :dashboard-libraries
:dashboard-team-members
:dashboard-team-settings)
[:& dashboard {:route route}] [:& dashboard {:route route}]
:viewer :viewer
@ -186,3 +188,9 @@
(ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened." (ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened."
:type :error :type :error
:timeout 5000})))))) :timeout 5000}))))))
;; (defonce foo
;; (do
;; (prn "attach listener")
;; (.addEventListener js/window "error" (fn [err] (ptk/handle-error (unchecked-get err "error"))))
;; 1))

View file

@ -9,6 +9,7 @@
(ns app.main.ui.auth (ns app.main.ui.auth
(:require (:require
[app.common.uuid :as uuid]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as du] [app.main.data.users :as du]
@ -20,6 +21,7 @@
[app.main.ui.auth.register :refer [register-page]] [app.main.ui.auth.register :refer [register-page]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.storage :refer [cache]]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as ts] [app.util.timers :as ts]
@ -35,7 +37,9 @@
(mf/defc auth (mf/defc auth
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
locale (mf/deref i18n/locale)] locale (mf/deref i18n/locale)
params (:query-params route)]
[:div.auth [:div.auth
[:section.auth-sidebar [:section.auth-sidebar
[:a.logo {:href "/#/"} i/logo] [:a.logo {:href "/#/"} i/logo]
@ -43,61 +47,9 @@
[:section.auth-content [:section.auth-content
(case section (case section
:auth-register [:& register-page {:locale locale}] :auth-register [:& register-page {:locale locale :params params}]
:auth-login [:& login-page {:locale locale}] :auth-login [:& login-page {:locale locale :params params}]
:auth-goodbye [:& goodbye-page {:locale locale}] :auth-goodbye [:& goodbye-page {:locale locale}]
:auth-recovery-request [:& recovery-request-page {:locale locale}] :auth-recovery-request [:& recovery-request-page {:locale locale}]
:auth-recovery [:& recovery-page {:locale locale :auth-recovery [:& recovery-page {:locale locale
:params (:query-params route)}])]])) :params (:query-params route)}])]]))
(defmulti handle-token (fn [token] (:iss token)))
(defmethod handle-token :verify-email
[data]
(let [msg (tr "settings.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :auth-login))))
(defmethod handle-token :change-email
[data]
(let [msg (tr "settings.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(defmethod handle-token :auth
[tdata]
(st/emit! (da/login-from-token tdata)))
(defmethod handle-token :default
[tdata]
(js/console.log "Unhandled token:" (pr-str tdata))
(st/emit! (rt/nav :auth-login)))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])]
(mf/use-effect
(fn []
(->> (rp/mutation :verify-profile-token {:token token})
(rx/subs
(fn [tdata]
(handle-token tdata))
(fn [error]
(case (:code error)
:email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))
:email-already-validated
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (dm/warn msg)))
(st/emit! (rt/nav :auth-login)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
[:div.verify-token
i/loader-pencil]))

View file

@ -20,10 +20,9 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :as fm]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr t]] [app.util.i18n :refer [tr t]]
[app.util.router :as rt])) [app.util.router :as rt]))
@ -50,18 +49,31 @@
(mf/defc login-form (mf/defc login-form
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
(let [error? (mf/use-state false) (let [error? (mf/use-state false)
submit-event (mf/use-var da/login) form (fm/use-form :spec ::login-form
:inital {})
on-error on-error
(fn [form event] (fn [form event]
(js/console.log error?)
(reset! error? true)) (reset! error? true))
on-submit on-submit
(fn [form event] (mf/use-callback
(reset! error? false) (mf/deps form)
(let [params (with-meta (:clean-data form) (fn [event]
{:on-error on-error})] (reset! error? false)
(st/emit! (@submit-event params))))] (let [params (with-meta (:clean-data @form)
{:on-error on-error})]
(st/emit! (da/login params)))))
on-submit-ldap
(mf/use-callback
(mf/deps form)
(fn [event]
(reset! error? false)
(let [params (with-meta (:clean-data @form)
{:on-error on-error})]
(st/emit! (da/login-with-ldap params)))))]
[:* [:*
(when @error? (when @error?
@ -70,28 +82,28 @@
:content (t locale "errors.auth.unauthorized") :content (t locale "errors.auth.unauthorized")
:on-close #(reset! error? false)}]) :on-close #(reset! error? false)}])
[:& form {:on-submit on-submit [:& fm/form {:on-submit on-submit :form form}
:spec ::login-form [:div.fields-row
:initial {}} [:& fm/input
[:& input {:name :email
{:name :email :type "text"
:type "text" :tab-index "2"
:tab-index "2" :help-icon i/at
:help-icon i/at :label (t locale "auth.email-label")}]]
:label (t locale "auth.email-label")}] [:div.fields-row
[:& input [:& fm/input
{:type "password" {:type "password"
:name :password :name :password
:tab-index "3" :tab-index "3"
:help-icon i/eye :help-icon i/eye
:label (t locale "auth.password-label")}] :label (t locale "auth.password-label")}]]
[:& submit-button [:& fm/submit-button
{:label (t locale "auth.login-submit-label") {:label (t locale "auth.login-submit-label")
:on-click #(reset! submit-event da/login)}] :on-click on-submit}]
(when cfg/login-with-ldap (when cfg/login-with-ldap
[:& submit-button [:& fm/submit-button
{:label (t locale "auth.login-with-ldap-submit-label") {:label (t locale "auth.login-with-ldap-submit-label")
:on-click #(reset! submit-event da/login-with-ldap)}])]])) :on-click on-submit}])]]))
(mf/defc login-page (mf/defc login-page
[{:keys [locale] :as props}] [{:keys [locale] :as props}]

View file

@ -17,15 +17,14 @@
[app.main.data.auth :as uda] [app.main.data.auth :as uda]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :as fm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [t tr]]
[app.util.router :as rt])) [app.util.router :as rt]))
(s/def ::password-1 ::fm/not-empty-string) (s/def ::password-1 ::us/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string) (s/def ::password-2 ::us/not-empty-string)
(s/def ::token ::fm/not-empty-string) (s/def ::token ::us/not-empty-string)
(s/def ::recovery-form (s/def ::recovery-form
(s/keys :req-un [::password-1 (s/keys :req-un [::password-1
@ -54,29 +53,31 @@
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [params (with-meta {:token (get-in form [:clean-data :token]) (let [mdata {:on-error on-error
:password (get-in form [:clean-data :password-2])} :on-success on-success}
{:on-error (partial on-error form) params {:token (get-in @form [:clean-data :token])
:on-success (partial on-success form)})] :password (get-in @form [:clean-data :password-2])}]
(st/emit! (uda/recover-profile params)))) (st/emit! (uda/recover-profile (with-meta params mdata)))))
(mf/defc recovery-form (mf/defc recovery-form
[{:keys [locale params] :as props}] [{:keys [locale params] :as props}]
[:& form {:on-submit on-submit (let [form (fm/use-form :spec ::recovery-form
:spec ::recovery-form :validators [password-equality]
:validators [password-equality] :initial params)]
:initial params} [:& fm/form {:on-submit on-submit
:form form}
[:div.fields-row
[:& fm/input {:type "password"
:name :password-1
:label (t locale "auth.new-password-label")}]]
[:& input {:type "password" [:div.fields-row
:name :password-1 [:& fm/input {:type "password"
:label (t locale "auth.new-password-label")}] :name :password-2
:label (t locale "auth.confirm-password-label")}]]
[:& input {:type "password" [:& fm/submit-button
:name :password-2 {:label (t locale "auth.recovery-submit-label")}]]))
:label (t locale "auth.confirm-password-label")}]
[:& submit-button
{:label (t locale "auth.recovery-submit-label")}]])
;; --- Recovery Request Page ;; --- Recovery Request Page
@ -86,7 +87,6 @@
[:div.form-container [:div.form-container
[:h1 "Forgot your password?"] [:h1 "Forgot your password?"]
[:div.subtitle "Please enter your new password"] [:div.subtitle "Please enter your new password"]
[:& recovery-form {:locale locale :params params}] [:& recovery-form {:locale locale :params params}]
[:div.links [:div.links

View file

@ -9,45 +9,48 @@
(ns app.main.ui.auth.recovery-request (ns app.main.ui.auth.recovery-request
(:require (:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.common.spec :as us] [app.common.spec :as us]
[app.main.data.auth :as uda] [app.main.data.auth :as uda]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt])) [app.util.router :as rt]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email])) (s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn- on-success
[]
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login)))
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [on-success #(st/emit! (let [params (with-meta (:clean-data @form)
(dm/info (tr "auth.notifications.recovery-token-sent")) {:on-success on-success})]
(rt/nav :auth-login))
params (with-meta (:clean-data form)
{:on-success on-success})]
(st/emit! (uda/request-profile-recovery params)))) (st/emit! (uda/request-profile-recovery params))))
(mf/defc recovery-form (mf/defc recovery-form
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
[:& form {:on-submit on-submit (let [form (fm/use-form :spec ::recovery-request-form
:spec ::recovery-request-form :initial {})]
:initial {}} [:& fm/form {:on-submit on-submit
:form form}
[:div.fields-row
[:& fm/input {:name :email
:label (t locale "auth.email-label")
:help-icon i/at
:type "text"}]]
[:& input {:name :email [:& fm/submit-button
:label (t locale "auth.email-label") {:label (t locale "auth.recovery-request-submit-label")}]]))
:help-icon i/at
:type "text"}]
[:& submit-button
{:label (t locale "auth.recovery-request-submit-label")}]])
;; --- Recovery Request Page ;; --- Recovery Request Page
@ -57,7 +60,6 @@
[:div.form-container [:div.form-container
[:h1 (t locale "auth.recovery-request-title")] [:h1 (t locale "auth.recovery-request-title")]
[:div.subtitle (t locale "auth.recovery-request-subtitle")] [:div.subtitle (t locale "auth.recovery-request-subtitle")]
[:& recovery-form {:locale locale}] [:& recovery-form {:locale locale}]
[:div.links [:div.links

View file

@ -9,16 +9,16 @@
(ns app.main.ui.auth.register (ns app.main.ui.auth.register
(:require (:require
[app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.auth :as uda] [app.main.data.users :as du]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr t]] [app.util.i18n :refer [tr t]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as tm] [app.util.timers :as tm]
@ -32,15 +32,6 @@
{:type :warning {:type :warning
:content (tr "auth.demo-warning")}]) :content (tr "auth.demo-warning")}])
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error (defn- on-error
[form error] [form error]
(case (:code error) (case (:code error)
@ -55,9 +46,14 @@
(defn- on-success (defn- on-success
[form data] [form data]
(let [msg (tr "auth.notifications.validation-email-sent" (:email data))] (if (and (:is-active data) (:claims data))
(st/emit! (rt/nav :auth-login) (let [message (tr "auth.notifications.team-invitation-accepted")]
(dm/success msg)))) (st/emit! (rt/nav :dashboard-projects {:team-id (get-in data [:claims :team-id])})
du/fetch-profile
(dm/success message)))
(let [message (tr "auth.notifications.validation-email-sent" (:email data))]
(st/emit! (rt/nav :auth-login)
(dm/success message)))))
(defn- validate (defn- validate
[data] [data]
@ -67,57 +63,74 @@
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [data (with-meta (:clean-data form) (let [data (with-meta (:clean-data @form)
{:on-error (partial on-error form) {:on-error (partial on-error form)
:on-success (partial on-success form)})] :on-success (partial on-success form)})]
(st/emit! (uda/register data)))) (st/emit! (da/register data))))
(s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::token ::us/not-empty-string)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]
:opt-un [::token]))
(mf/defc register-form (mf/defc register-form
[{:keys [locale] :as props}] [{:keys [locale params] :as props}]
[:& form {:on-submit on-submit (let [initial (mf/use-memo (mf/deps params) (constantly params))
:spec ::register-form form (fm/use-form :spec ::register-form
:validators [validate] :validators [validate]
:initial {}} :initial initial)]
[:& input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]
[:& submit-button [:& fm/form {:on-submit on-submit
{:label (t locale "auth.register-submit-label")}]]) :form form}
[:div.fields-row
[:& fm/input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]]
[:div.fields-row
[:& fm/input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]]
[:div.fields-row
[:& fm/input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]]
[:& fm/submit-button
{:label (t locale "auth.register-submit-label")}]]))
;; --- Register Page ;; --- Register Page
(mf/defc register-page (mf/defc register-page
[{:keys [locale] :as props}] [{:keys [locale params] :as props}]
[:section.generic-form [:div.form-container
[:div.form-container [:h1 (t locale "auth.register-title")]
[:h1 (t locale "auth.register-title")] [:div.subtitle (t locale "auth.register-subtitle")]
[:div.subtitle (t locale "auth.register-subtitle")] (when cfg/demo-warning
(when cfg/demo-warning [:& demo-warning])
[:& demo-warning])
[:& register-form {:locale locale}] [:& register-form {:locale locale
:params params}]
[:div.links [:div.links
[:div.link-entry [:div.link-entry
[:span (t locale "auth.already-have-account") " "] [:span (t locale "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login)) [:a {:on-click #(st/emit! (rt/nav :auth-login))
:tab-index "4"} :tab-index "4"}
(t locale "auth.login-here")]] (t locale "auth.login-here")]]
[:div.link-entry [:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "] [:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile) [:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"} :tab-index "5"}
(t locale "auth.create-demo-profile")]]]]]) (t locale "auth.create-demo-profile")]]]])

View file

@ -0,0 +1,94 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.auth.verify-token
(:require
[app.common.uuid :as uuid]
[app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
[app.main.ui.auth.register :refer [register-page]]
[app.main.ui.icons :as i]
[app.util.forms :as fm]
[app.util.storage :refer [cache]]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt]
[app.util.timers :as ts]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(defmulti handle-token (fn [token] (:iss token)))
(defmethod handle-token :verify-email
[data]
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :auth-login))))
(defmethod handle-token :change-email
[data]
(let [msg (tr "dashboard.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(defmethod handle-token :auth
[tdata]
(st/emit! (da/login-from-token tdata)))
(defmethod handle-token :team-invitation
[tdata]
(case (:state tdata)
:created
(let [message (tr "auth.notifications.team-invitation-accepted")]
(st/emit! du/fetch-profile
(rt/nav :dashboard-projects {:team-id (:team-id tdata)})
(dm/success message)))
:pending
(st/emit! (rt/nav :auth-register {} {:token (:token tdata)}))))
(defmethod handle-token :default
[tdata]
(js/console.log "Unhandled token:" (pr-str tdata))
(st/emit! (rt/nav :auth-login)))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])]
(mf/use-effect
(fn []
(->> (rp/mutation :verify-token {:token token})
(rx/subs
(fn [tdata]
(handle-token tdata))
(fn [error]
(case (:code error)
:email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))
:email-already-validated
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (dm/warn msg)))
(st/emit! (rt/nav :auth-login)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
[:div.verify-token
i/loader-pencil]))

View file

@ -20,21 +20,22 @@
[app.util.dom :as dom])) [app.util.dom :as dom]))
(def form-ctx (mf/create-context nil)) (def form-ctx (mf/create-context nil))
(def use-form fm/use-form)
(mf/defc input (mf/defc input
[{:keys [type label help-icon disabled name form hint trim] :as props}] [{:keys [type label help-icon disabled name form hint trim] :as props}]
(let [form (mf/use-ctx form-ctx) (let [form (or form (mf/use-ctx form-ctx))
type' (mf/use-state type) type' (mf/use-state type)
focus? (mf/use-state false) focus? (mf/use-state false)
locale (mf/deref i18n/locale) locale (mf/deref i18n/locale)
touched? (get-in form [:touched name]) touched? (get-in @form [:touched name])
error (get-in form [:errors name]) error (get-in @form [:errors name])
value (get-in form [:data name] "") value (get-in @form [:data name] "")
help-icon' (cond help-icon' (cond
(and (= type "password") (and (= type "password")
(= @type' "password")) (= @type' "password"))
i/eye i/eye
@ -67,7 +68,7 @@
on-blur on-blur
(fn [event] (fn [event]
(reset! focus? false) (reset! focus? false)
(when-not (get-in form [:touched name]) (when-not (get-in @form [:touched name])
(swap! form assoc-in [:touched name] true))) (swap! form assoc-in [:touched name] true)))
props (-> props props (-> props
@ -80,33 +81,33 @@
:type @type') :type @type')
(obj/clj->props))] (obj/clj->props))]
[:div.field.custom-input [:div.custom-input
{:class klass} {:class klass}
[:* [:*
[:label label] [:label label]
[:> :input props] [:> :input props]
(when help-icon' (when help-icon'
[:div.help-icon [:div.help-icon
{:style {:cursor "pointer"} {:style {:cursor "pointer"}
:on-click (when (= "password" type) :on-click (when (= "password" type)
swap-text-password)} swap-text-password)}
help-icon']) help-icon'])
(cond (cond
(and touched? (:message error)) (and touched? (:message error))
[:span.error (t locale (:message error))] [:span.error (t locale (:message error))]
(string? hint) (string? hint)
[:span.hint hint])]])) [:span.hint hint])]]))
(mf/defc select (mf/defc select
[{:keys [options label name form default] [{:keys [options label name form default]
:or {default ""}}] :or {default ""}}]
(let [form (mf/use-ctx form-ctx) (let [form (or form (mf/use-ctx form-ctx))
value (get-in form [:data name] default) value (get-in @form [:data name] default)
cvalue (d/seek #(= value (:value %)) options) cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)] on-change (fm/on-input-change form name)]
[:div.field.custom-select [:div.custom-select
[:select {:value value [:select {:value value
:on-change on-change} :on-change on-change}
(for [item options] (for [item options]
@ -122,34 +123,21 @@
(mf/defc submit-button (mf/defc submit-button
[{:keys [label form on-click] :as props}] [{:keys [label form on-click] :as props}]
(let [form (mf/use-ctx form-ctx)] (let [form (or form (mf/use-ctx form-ctx))]
[:input.btn-primary.btn-large [:input.btn-primary.btn-large
{:name "submit" {:name "submit"
:class (when-not (:valid form) "btn-disabled") :class (when-not (:valid @form) "btn-disabled")
:disabled (not (:valid form)) :disabled (not (:valid @form))
:on-click on-click :on-click on-click
:value label :value label
:type "submit"}])) :type "submit"}]))
(mf/defc form (mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}] [{:keys [on-submit form children class] :as props}]
(let [frm (fm/use-form :spec spec (let [on-submit (or on-submit (constantly nil))]
:validators validators [:& (mf/provider form-ctx) {:value form}
:initial initial)]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! frm update :data merge (initial))
(swap! frm update :data merge initial))))
[:& (mf/provider form-ctx) {:value frm}
[:form {:class class [:form {:class class
:on-submit (fn [event] :on-submit (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(on-submit frm event))} (on-submit form event))}
children]])) children]]))

View file

@ -2,53 +2,69 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.confirm (ns app.main.ui.confirm
(:require (:require
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[rumext.alpha :as mf] [app.util.dom :as dom]
[app.main.ui.modal :as modal] [app.util.i18n :as i18n :refer [tr t]]
[app.util.i18n :refer (tr)] [rumext.alpha :as mf]))
[app.util.data :refer [classnames]]
[app.util.dom :as dom]))
(mf/defc confirm-dialog (mf/defc confirm-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :confirm-dialog} ::mf/register-as :confirm}
[{:keys [message on-accept on-cancel hint cancel-text accept-text not-danger?] :as ctx}] [{:keys [message title on-accept on-cancel hint cancel-label accept-label] :as props}]
(let [message (or message (tr "ds.confirm-title")) (let [locale (mf/deref i18n/locale)
cancel-text (or cancel-text (tr "ds.confirm-cancel"))
accept-text (or accept-text (tr "ds.confirm-ok"))
accept on-accept (or on-accept identity)
(fn [event] on-cancel (or on-cancel identity)
(dom/prevent-default event) message (or message (t locale "ds.confirm-title"))
(modal/hide!) cancel-label (or cancel-label (tr "ds.confirm-cancel"))
(on-accept (dissoc ctx :on-accept :on-cancel))) accept-label (or accept-label (tr "ds.confirm-ok"))
title (or title (t locale "ds.confirm-title"))
accept-fn
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(st/emit! (modal/hide))
(on-accept props)))
cancel-fn
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(st/emit! (modal/hide))
(on-cancel props)))]
cancel
(fn [event]
(dom/prevent-default event)
(modal/hide!)
(when on-cancel
(on-cancel (dissoc ctx :on-accept :on-cancel))))]
[:div.modal-overlay [:div.modal-overlay
[:div.modal.confirm-dialog [:div.modal-container.confirm-dialog
[:a.close {:on-click cancel} i/close] [:div.modal-header
[:div.modal-header-title
[:h2 title]]
[:div.modal-close-button
{:on-click cancel-fn} i/close]]
[:div.modal-content [:div.modal-content
[:h3.dialog-title message] [:h3 message]
(if hint [:span hint]) (when (string? hint)
[:div.dialog-buttons [:p hint])]
[:input.dialog-cancel-button
{:type "button"
:value cancel-text
:on-click cancel}]
[:input.dialog-accept-button [:div.modal-footer
[:div.action-buttons
[:input.cancel-button
{:type "button" {:type "button"
:class (classnames :not-danger not-danger?) :value cancel-label
:value accept-text :on-click cancel-fn}]
:on-click accept}]]]]]))
[:input.accept-button
{:type "button"
:value accept-label
:on-click accept-fn}]]]]]))

View file

@ -20,6 +20,7 @@
[app.main.ui.dashboard.projects :refer [projects-section]] [app.main.ui.dashboard.projects :refer [projects-section]]
[app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t]] [app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt] [app.util.router :as rt]
@ -56,7 +57,7 @@
(l/derived (l/in [:projects team-id]) st/state)) (l/derived (l/in [:projects team-id]) st/state))
(mf/defc dashboard-content (mf/defc dashboard-content
[{:keys [team projects project section search-term] :as props}] [{:keys [team projects project section search-term profile] :as props}]
[:div.dashboard-content [:div.dashboard-content
(case section (case section
:dashboard-projects :dashboard-projects
@ -75,6 +76,12 @@
:dashboard-libraries :dashboard-libraries
[:& libraries-page {:team team}] [:& libraries-page {:team team}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile}]
:dashboard-team-settings
[:& team-settings-page {:team team :profile profile}]
nil)]) nil)])
(mf/defc dashboard (mf/defc dashboard
@ -96,18 +103,18 @@
(mf/use-effect (mf/use-effect
(mf/deps team-id) (mf/deps team-id)
(fn [] (st/emitf (dd/fetch-bundle {:id team-id})))
(st/emit! (dd/fetch-team {:id team-id})
(dd/fetch-projects {:team-id team-id}))))
[:section.dashboard-layout [:section.dashboard-layout
[:& sidebar {:team team [:& sidebar {:team team
:projects projects :projects projects
:project project :project project
:profile profile
:section section :section section
:search-term search-term}] :search-term search-term}]
(when team (when team
[:& dashboard-content {:projects projects [:& dashboard-content {:projects projects
:profile profile
:project project :project project
:section section :section section
:search-term search-term :search-term search-term

View file

@ -10,12 +10,13 @@
(ns app.main.ui.dashboard.files (ns app.main.ui.dashboard.files
(:require (:require
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]] [app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt] [app.util.router :as rt]
@ -24,9 +25,9 @@
(mf/defc header (mf/defc header
[{:keys [team project] :as props}] [{:keys [team project] :as props}]
(let [local (mf/use-state {:menu-open false (let [local (mf/use-state {:menu-open false
:edition false}) :edition false})
locale (mf/deref i18n/locale) locale (mf/deref i18n/locale)
project-id (:id project) project-id (:id project)
team-id (:id team) team-id (:id team)
@ -39,21 +40,6 @@
on-edit on-edit
(mf/use-callback #(swap! local assoc :edition true :menu-open false)) (mf/use-callback #(swap! local assoc :edition true :menu-open false))
on-blur
(mf/use-callback
(mf/deps project)
(fn [event]
(let [name (-> event dom/get-target dom/get-value)]
#_(st/emit! (dd/rename-project (:id project) name))
(swap! local assoc :edition false))))
on-key-down
(mf/use-callback
(mf/deps project)
(fn [event]
(cond
(kbd/enter? event) (on-blur event)
(kbd/esc? event) (swap! local assoc :edition false))))
delete-fn delete-fn
(mf/use-callback (mf/use-callback
@ -65,7 +51,12 @@
on-delete on-delete
(mf/use-callback (mf/use-callback
(mf/deps project) (mf/deps project)
(fn [] (modal/show! :confirm-dialog {:on-accept delete-fn}))) (st/emitf (modal/show
{:type :confirm
:title "Deleting project"
:message "Are you sure you wan't to delete this project?"
:accept-label "Delete project"
:on-accept delete-fn})))
on-create-clicked on-create-clicked
(mf/use-callback (mf/use-callback
@ -77,26 +68,21 @@
[:header.dashboard-header [:header.dashboard-header
(if (:is-default project) (if (:is-default project)
[:h1.dashboard-title (t locale "dashboard.header.draft")] [:div.dashboard-title
[:* [:h1 (t locale "dashboard.header.draft")]]
[:h1.dashboard-title (t locale "dashboard.header.project" (:name project))]
[:div.icon {:on-click on-menu-click} i/actions]
[:& context-menu {:on-close on-menu-close
:show (:menu-open @local)
:options [[(t locale "dashboard.grid.rename") on-edit]
[(t locale "dashboard.grid.delete") on-delete]]}]
(if (:edition @local)
[:input.element-name {:type "text"
:auto-focus true
:on-key-down on-key-down
:on-blur on-blur
:default-value (:name project)}])])
#_[:ul.main-nav
[:li.current
[:a "PROJECTS"]]
[:li
[:a "MEMBERS"]]]
(if (:edition @local)
[:& inline-edition {:content (:name project)
:on-end (fn [name]
(st/emit! (dd/rename-project (assoc project :name name)))
(swap! local assoc :edition false))}]
[:div.dashboard-title
[:h1 (:name project)]
[:div.icon {:on-click on-menu-click} i/actions]
[:& context-menu {:on-close on-menu-close
:show (:menu-open @local)
:options [[(t locale "dashboard.grid.rename") on-edit]
[(t locale "dashboard.grid.delete") on-delete]]}]]))
[:a.btn-secondary.btn-small {:on-click on-create-clicked} [:a.btn-secondary.btn-small {:on-click on-create-clicked}
(t locale "dashboard.new-file")]])) (t locale "dashboard.new-file")]]))
@ -119,7 +105,7 @@
[:* [:*
[:& header {:team team :project project}] [:& header {:team team :project project}]
[:section.dashboard-grid-container [:section.dashboard-container
[:& grid {:id (:id project) [:& grid {:id (:id project)
:files files}]]])) :files files}]]]))

View file

@ -16,9 +16,10 @@
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.main.worker :as wrk] [app.main.worker :as wrk]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [t tr]]
@ -60,21 +61,25 @@
(mf/defc grid-item (mf/defc grid-item
{:wrap [mf/memo]} {:wrap [mf/memo]}
[{:keys [id file] :as props}] [{:keys [id file] :as props}]
(let [local (mf/use-state {:menu-open false :edition false}) (let [local (mf/use-state {:menu-open false :edition false})
locale (mf/deref i18n/locale) locale (mf/deref i18n/locale)
on-close (mf/use-callback #(swap! local assoc :menu-open false))
delete (mf/use-callback (mf/deps id) #(st/emit! (dd/delete-file file))) delete-fn
add-shared (mf/use-callback (mf/deps id) #(st/emit! (dd/set-file-shared id true))) (mf/use-callback
del-shared (mf/use-callback (mf/deps id) #(st/emit! (dd/set-file-shared id false))) (mf/deps file)
on-close (mf/use-callback #(swap! local assoc :menu-open false)) (st/emitf (dd/delete-file file)))
on-delete on-delete
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps file)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(modal/show! :confirm-dialog {:on-accept delete}))) (st/emit! (modal/show {:type :confirm
:title "Deleting file"
:message "Are you sure you want to delete this file?"
:on-accept delete-fn
:accept-label "Delete file"}))))
on-navigate on-navigate
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps id)
@ -84,73 +89,75 @@
qparams {:page-id (first (get-in file [:data :pages]))}] qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav :workspace pparams qparams))))) (st/emit! (rt/nav :workspace pparams qparams)))))
add-shared
(mf/use-callback
(mf/deps file)
(st/emitf (dd/set-file-shared (assoc file :is-shared true))))
del-shared
(mf/use-callback
(mf/deps file)
(st/emitf (dd/set-file-shared (assoc file :is-shared false))))
on-add-shared on-add-shared
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps file)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(modal/show! :confirm-dialog (st/emit! (modal/show
{:message (t locale "dashboard.grid.add-shared-message" (:name file)) {:type :confirm
:hint (t locale "dashboard.grid.add-shared-hint") :message (t locale "dashboard.grid.add-shared-message" (:name file))
:accept-text (t locale "dashboard.grid.add-shared-accept") :title "Adding as shared library"
:not-danger? true :hint (t locale "dashboard.grid.add-shared-hint")
:on-accept add-shared}))) :accept-label (t locale "dashboard.grid.add-shared-accept")
:on-accept add-shared}))))
on-edit
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(swap! local assoc :edition true)))
on-del-shared on-del-shared
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps file)
(fn [event] (fn [event]
(dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(modal/show! :confirm-dialog (modal/show! :confirm
{:message (t locale "dashboard.grid.remove-shared-message" (:name file)) {:title "Unsharing file"
:message (t locale "dashboard.grid.remove-shared-message" (:name file))
:hint (t locale "dashboard.grid.remove-shared-hint") :hint (t locale "dashboard.grid.remove-shared-hint")
:accept-text (t locale "dashboard.grid.remove-shared-accept") :accept-label (t locale "dashboard.grid.remove-shared-accept")
:not-danger? false
:on-accept del-shared}))) :on-accept del-shared})))
on-menu-click on-menu-click
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps file)
(fn [event] (fn [event]
(dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! local assoc :menu-open true))) (swap! local assoc :menu-open true)))
on-blur edit
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps file)
(fn [event] (fn [name]
(let [name (-> event dom/get-target dom/get-value) (st/emit! (dd/rename-file (assoc file :name name)))
file (assoc file :name name)] (swap! local assoc :edition false)))
(st/emit! (dd/rename-file file))
(swap! local assoc :edition false))))
on-key-down on-edit
(mf/use-callback (mf/use-callback
#(cond (mf/deps file)
(kbd/enter? %) (on-blur %) (fn [event]
(kbd/esc? %) (swap! local assoc :edition false))) (dom/stop-propagation event)
(swap! local assoc :edition true)))
] ]
[:div.grid-item.project-th {:on-click on-navigate} [:div.grid-item.project-th {:on-click on-navigate}
[:div.overlay] [:div.overlay]
[:& grid-item-thumbnail {:file file}] [:& grid-item-thumbnail {:file file}]
(when (:is-shared file) (when (:is-shared file)
[:div.item-badge [:div.item-badge i/library])
i/library])
[:div.item-info [:div.item-info
(if (:edition @local) (if (:edition @local)
[:input.element-name {:type "text" [:& inline-edition {:content (:name file)
:auto-focus true :on-end edit}]
:on-key-down on-key-down
:on-blur on-blur
:default-value (:name file)}]
[:h3 (:name file)]) [:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at file)}]] [:& grid-item-metadata {:modified-at (:modified-at file)}]]
[:div.project-th-actions {:class (dom/classnames [:div.project-th-actions {:class (dom/classnames
@ -188,11 +195,12 @@
[:& empty-placeholder])])) [:& empty-placeholder])]))
(mf/defc line-grid-row (mf/defc line-grid-row
[{:keys [locale files] :as props}] [{:keys [locale files on-load-more] :as props}]
(let [rowref (mf/use-ref) (let [rowref (mf/use-ref)
width (mf/use-state 900) width (mf/use-state 900)
limit (mf/use-state 1) limit (mf/use-state 1)
itemsize 290] itemsize 290]
(mf/use-layout-effect (mf/use-layout-effect
@ -229,17 +237,18 @@
:file item :file item
:key (:id item)}]) :key (:id item)}])
(when (> (count files) @limit) (when (> (count files) @limit)
[:div.grid-item.placeholder [:div.grid-item.placeholder {:on-click on-load-more}
[:div.placeholder-icon i/arrow-down] [:div.placeholder-icon i/arrow-down]
[:div.placeholder-label "Show all files"]])])) [:div.placeholder-label
(t locale "dashboard.grid.show-all-files")]])]))
(mf/defc line-grid (mf/defc line-grid
[{:keys [project-id opts files] :as props}] [{:keys [project-id opts files on-load-more] :as props}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)]
click #(st/emit! (dd/create-file project-id))]
[:section.dashboard-grid [:section.dashboard-grid
(if (pos? (count files)) (if (pos? (count files))
[:& line-grid-row {:files files [:& line-grid-row {:files files
:on-load-more on-load-more
:locale locale}] :locale locale}]
[:& empty-placeholder])])) [:& empty-placeholder])]))

View file

@ -0,0 +1,66 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.dashboard.inline-edition
(:require
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.util.dom :as dom]
[rumext.alpha :as mf]))
(mf/defc inline-edition
[{:keys [content on-end] :as props}]
(let [name (mf/use-state content)
input-ref (mf/use-ref)
on-input
(mf/use-callback
(fn [event]
(->> (dom/get-target-val event)
(reset! name))))
on-cancel
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(on-end @name)))
on-click
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)))
on-keyup
(mf/use-callback
(fn [event]
(cond
(kbd/esc? event)
(on-cancel)
(kbd/enter? event)
(let [name (dom/get-target-val event)]
(on-end name)))))]
(mf/use-effect
(fn []
(let [node (mf/ref-val input-ref)]
(dom/focus! node)
(dom/select-text! node))))
[:div.edit-wrapper
[:input.element-title {:value @name
:ref input-ref
:on-click on-click
:on-change on-input
:on-key-down on-keyup}]
[:span.close {:on-click on-cancel} i/close]]))

View file

@ -24,14 +24,13 @@
[app.util.router :as rt] [app.util.router :as rt]
[app.util.time :as dt])) [app.util.time :as dt]))
;; --- Component: Recent files
(mf/defc header (mf/defc header
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [profile locale team] :as props}] [{:keys [locale team] :as props}]
(let [create #(st/emit! (dd/create-project {:team-id (:id team)}))] (let [create #(st/emit! (dd/create-project {:team-id (:id team)}))]
[:header.dashboard-header [:header.dashboard-header
[:h1.dashboard-title "Projects"] [:div.dashboard-title
[:h1 "Projects"]]
[:a.btn-secondary.btn-small {:on-click create} [:a.btn-secondary.btn-small {:on-click create}
(t locale "dashboard.header.new-project")]])) (t locale "dashboard.header.new-project")]]))
@ -63,14 +62,12 @@
on-nav on-nav
(mf/use-callback (mf/use-callback
(mf/deps project) (mf/deps project)
(fn [] (st/emitf (rt/nav :dashboard-files {:team-id (:team-id project)
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project) :project-id (:id project)})))
:project-id (:id project)}))))
toggle-pin toggle-pin
(mf/use-callback (mf/use-callback
(mf/deps project) (mf/deps project)
(fn [] (st/emitf (dd/toggle-project-pin project)))
(st/emit! (dd/toggle-project-pin project))))
on-file-created on-file-created
(mf/use-callback (mf/use-callback
@ -111,6 +108,7 @@
[:& line-grid [:& line-grid
{:project-id (:id project) {:project-id (:id project)
:on-load-more on-nav
:files files}]])) :files files}]]))
(mf/defc projects-section (mf/defc projects-section
@ -129,7 +127,7 @@
[:* [:*
[:& header {:locale locale [:& header {:locale locale
:team team}] :team team}]
[:section.dashboard-grid-container [:section.dashboard-container
(for [project projects] (for [project projects]
[:& project-item {:project project [:& project-item {:project project
:locale locale :locale locale

View file

@ -32,7 +32,7 @@
(st/emitf (dd/search-files {:team-id (:id team) (st/emitf (dd/search-files {:team-id (:id team)
:search-term search-term}))) :search-term search-term})))
[:section.dashboard-grid-container.search [:section.dashboard-container.search
(cond (cond
(empty? search-term) (empty? search-term)
[:div.grid-empty-placeholder [:div.grid-empty-placeholder

View file

@ -11,7 +11,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.main.constants :as c] [app.config :as cfg]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
@ -19,10 +19,12 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.team-form]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [t tr]]
[app.util.object :as obj] [app.util.object :as obj]
@ -35,55 +37,6 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc sidebar-project-edition
[{:keys [item on-end] :as props}]
(let [name (mf/use-state (:name item))
input-ref (mf/use-ref)
on-input
(mf/use-callback
(fn [event]
(->> event
(dom/get-target)
(dom/get-value)
(reset! name))))
on-cancel
(mf/use-callback
(fn []
(st/emit! dd/clear-project-for-edit)
(on-end)))
on-keyup
(mf/use-callback
(fn [event]
(cond
(kbd/esc? event)
(on-cancel)
(kbd/enter? event)
(let [name (-> event
dom/get-target
dom/get-value)]
(st/emit! dd/clear-project-for-edit
(dd/rename-project (assoc item :name name)))
(on-end)))))]
(mf/use-effect
(fn []
(let [node (mf/ref-val input-ref)]
(dom/focus! node)
(dom/select-text! node))))
[:div.edit-wrapper
[:input.element-title {:value @name
:ref input-ref
:on-change on-input
:on-key-down on-keyup}]
[:span.close {:on-click on-cancel} i/close]]))
(mf/defc sidebar-project (mf/defc sidebar-project
[{:keys [item selected?] :as props}] [{:keys [item selected?] :as props}]
(let [dstate (mf/deref refs/dashboard-local) (let [dstate (mf/deref refs/dashboard-local)
@ -97,23 +50,29 @@
(fn [] (fn []
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id item) (st/emit! (rt/nav :dashboard-files {:team-id (:team-id item)
:project-id (:id item)})))) :project-id (:id item)}))))
on-dbl-click on-dbl-click
(mf/use-callback #(reset! edition? true))] (mf/use-callback #(reset! edition? true))
on-edit
(mf/use-callback
(mf/deps item)
(fn [name]
(st/emit! (dd/rename-project (assoc item :name name)))
(reset! edition? false)))]
[:li {:on-click on-click [:li {:on-click on-click
:on-double-click on-dbl-click :on-double-click on-dbl-click
:class (when selected? "current")} :class (when selected? "current")}
(if @edition? (if @edition?
[:& sidebar-project-edition {:item item [:& inline-edition {:content (:name item)
:on-end #(reset! edition? false)}] :on-end on-edit}]
[:span.element-title (:name item)])])) [:span.element-title (:name item)])]))
(mf/defc sidebar-search (mf/defc sidebar-search
[{:keys [search-term team-id locale] :as props}] [{:keys [search-term team-id locale] :as props}]
(let [search-term (or search-term "") (let [search-term (or search-term "")
emit! (mf/use-memo #(f/debounce st/emit! 500))
emit! (mf/use-memo #(f/debounce st/emit! 500))
on-search-focus on-search-focus
(mf/use-callback (mf/use-callback
@ -158,36 +117,223 @@
{:on-click on-clear-click} {:on-click on-clear-click}
i/close]])) i/close]]))
(mf/defc sidebar-team-switch (mf/defc teams-selector-dropdown
[{:keys [team profile] :as props}] [{:keys [team profile locale] :as props}]
(let [show-dropdown? (mf/use-state false) (let [show-dropdown? (mf/use-state false)
teams (mf/use-state [])
show-team-opts-ddwn? (mf/use-state false) on-create-clicked
show-teams-ddwn? (mf/use-state false) (mf/use-callback
teams (mf/use-state []) (st/emitf (modal/show :team-form {})))
on-nav go-projects
(mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %})))]
(mf/use-layout-effect
(mf/deps (:id team))
(fn []
(->> (rp/query! :teams)
(rx/map #(mapv dd/assoc-team-avatar %))
(rx/subs #(reset! teams %)))))
[:ul.dropdown.teams-dropdown
[:li.title (t locale "dashboard.sidebar.switch-team")]
[:hr]
[:li.team-name {:on-click (partial go-projects (:default-team-id profile))}
[:span.team-icon i/logo-icon]
[:span.team-text "Your penpot"]]
(for [team (remove :is-default @teams)]
[:* {:key (:id team)}
[:li.team-name {:on-click (partial go-projects (:id team))}
[:span.team-icon
[:img {:src (cfg/resolve-media-path (:photo team))}]]
[:span.team-text {:title (:name team)} (:name team)]]])
[:hr]
[:li.action {:on-click on-create-clicked}
(t locale "dashboard.sidebar.create-team")]]))
(s/def ::member-id ::us/uuid)
(s/def ::leave-modal-form
(s/keys :req-un [::member-id]))
(mf/defc leave-and-reassign-modal
{::mf/register modal/components
::mf/register-as ::leave-and-reassign
::mf/props-spec ::kaka-de-vaca}
[{:keys [members profile team accept]}]
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
options (into [{:value "" :label "Select a member to promote"}]
(map #(hash-map :name (:name %) :value (str (:id %))) members))
on-cancel
(mf/use-callback (st/emitf (modal/hide)))
on-accept
(mf/use-callback
(mf/deps form)
(fn [event]
(let [member-id (get-in @form [:clean-data :member-id])]
(accept member-id))))]
[:div.modal-overlay
[:div.modal-container.confirm-dialog
[:div.modal-header
[:div.modal-header-title
[:h2 "Before you leave"]]
[:div.modal-close-button
{:on-click on-cancel} i/close]]
[:div.modal-content.generic-form
[:p "You are " (:name team) " owner."]
[:p "Select an other member to promote before leave."]
[:& fm/form {:form form}
[:& fm/select {:name :member-id
:options options}]]]
[:div.modal-footer
[:div.action-buttons
[:input.cancel-button
{:type "button"
:value "Cancel"
:on-click on-cancel}]
[:input.accept-button
{:type "button"
:class (when-not (:valid @form) "btn-disabled")
:disabled (not (:valid @form))
:value "Promoto and Leave"
:on-click on-accept}]]]]]))
(mf/defc team-options-dropdown
[{:keys [team locale profile] :as props}]
(let [members (mf/use-state [])
go-members
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
go-settings
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
go-projects
(mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %}))) (mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %})))
on-create-clicked on-create-clicked
(mf/use-callback #(modal/show! :team-form {}))] (mf/use-callback
(st/emitf (modal/show :team-form {})))
(mf/use-effect on-rename-clicked
(mf/deps (:id teams)) (mf/use-callback
(mf/deps team)
(st/emitf (modal/show :team-form {:team team})))
on-leaved-success
(mf/use-callback
(mf/deps team profile)
(st/emitf (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})))
leave-fn
(mf/use-callback
(mf/deps team)
(st/emitf (dd/leave-team (with-meta team {:on-success on-leaved-success}))))
leave-and-reassign-fn
(mf/use-callback
(mf/deps team)
(fn [member-id]
(let [team (assoc team :reassign-to member-id)]
(st/emit! (dd/leave-team (with-meta team {:on-success on-leaved-success}))))))
on-leave-clicked
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show
{:type :confirm
:title "Leaving team"
:message "Are you sure you want to leave this team?"
:accept-label "Leave team"
:on-accept leave-fn})))
on-leave-as-owner-clicked
(mf/use-callback
(mf/deps team @members)
(st/emitf (modal/show
{:type ::leave-and-reassign
:profile profile
:team team
:accept leave-and-reassign-fn
:members @members})))
delete-fn
(mf/use-callback
(mf/deps team)
(st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success}))))
on-delete-clicked
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show
{:type :confirm
:title "Deleting team"
:message (str "Are you sure you want to delete this team?\n"
"All projects and files associated with team will be permanently deleted.")
:accept-label "Delete team"
:on-accept delete-fn})))]
(mf/use-layout-effect
(mf/deps (:id team))
(fn [] (fn []
(->> (rp/query! :teams) (->> (rp/query! :team-members {:team-id (:id team)})
(rx/subs #(reset! teams %))))) (rx/subs #(reset! members %)))))
[:ul.dropdown.options-dropdown
[:li {:on-click go-members} (t locale "dashboard.sidebar.team-members")]
[:li {:on-click go-settings} (t locale "dashboard.sidebar.settings")]
[:hr]
[:li {:on-click on-rename-clicked} (t locale "dashboard.sidebar.rename-team")]
(cond
(:is-owner team)
[:li {:on-click on-leave-as-owner-clicked} (t locale "dashboard.sidebar.leave-team")]
(> (count @members) 1)
[:li {:on-click on-leave-clicked} (t locale "dashboard.sidebar.leave-team")])
(when (:is-owner team)
[:li {:on-click on-delete-clicked} (t locale "dashboard.sidebar.delete-team")])]))
(mf/defc sidebar-team-switch
[{:keys [team profile locale] :as props}]
(let [show-dropdown? (mf/use-state false)
show-team-opts-ddwn? (mf/use-state false)
show-teams-ddwn? (mf/use-state false)]
[:div.sidebar-team-switch [:div.sidebar-team-switch
[:div.switch-content [:div.switch-content
[:div.current-team [:div.current-team
[:div.team-name (if (:is-default team)
[:span.team-icon i/logo-icon] [:div.team-name
(if (:is-default team) [:span.team-icon i/logo-icon]
[:span.team-text "Your penpot"] [:span.team-text (t locale "dashboard.sidebar.default-team-name")]]
[:span.team-text (:name team)])] [:div.team-name
[:span.team-icon
[:img {:src (cfg/resolve-media-path (:photo team))}]]
[:span.team-text {:title (:name team)} (:name team)]])
[:span.switch-icon {:on-click #(reset! show-teams-ddwn? true)} [:span.switch-icon {:on-click #(reset! show-teams-ddwn? true)}
i/arrow-down]] i/arrow-down]]
(when-not (:is-default team) (when-not (:is-default team)
[:div.switch-options {:on-click #(reset! show-team-opts-ddwn? true)} [:div.switch-options {:on-click #(reset! show-team-opts-ddwn? true)}
i/actions])] i/actions])]
@ -195,86 +341,15 @@
;; Teams Dropdown ;; Teams Dropdown
[:& dropdown {:show @show-teams-ddwn? [:& dropdown {:show @show-teams-ddwn?
:on-close #(reset! show-teams-ddwn? false)} :on-close #(reset! show-teams-ddwn? false)}
[:ul.dropdown.teams-dropdown [:& teams-selector-dropdown {:team team
[:li.title "Switch Team"] :profile profile
[:hr] :locale locale}]]
[:li.team-item {:on-click (partial on-nav (:default-team-id profile))}
[:span.icon i/logo-icon]
[:span.text "Your penpot"]]
(for [team (remove :is-default @teams)]
[:* {:key (:id team)}
[:hr]
[:li.team-item {:on-click (partial on-nav (:id team))}
[:span.icon i/logo-icon]
[:span.text (:name team)]]])
[:hr]
[:li.action {:on-click on-create-clicked}
"+ Create new team"]]]
[:& dropdown {:show @show-team-opts-ddwn? [:& dropdown {:show @show-team-opts-ddwn?
:on-close #(reset! show-team-opts-ddwn? false)} :on-close #(reset! show-team-opts-ddwn? false)}
[:ul.dropdown.options-dropdown [:& team-options-dropdown {:team team
[:li "Members"] :profile profile
[:li "Settings"] :locale locale}]]]))
[:hr]
[:li "Rename"]
[:li "Leave team"]
[:li "Delete team"]]]
]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc team-form-modal
{::mf/register modal/components
::mf/register-as :team-form}
[props]
(let [locale (mf/deref i18n/locale)
on-success
(mf/use-callback
(fn [form response]
(modal/hide!)
(let [msg "Team created successfuly"]
(st/emit!
(dm/success msg)
(rt/nav :dashboard-projects {:team-id (:id response)})))))
on-error
(mf/use-callback
(fn [form response]
(let [msg "Error on creating team."]
(st/emit! (dm/error msg)))))
on-submit
(mf/use-callback
(fn [form]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name (get-in form [:clean-data :name])}]
(st/emit! (dd/create-team (with-meta params mdata))))))]
[:div.modal-overlay
[:div.generic-modal.team-form-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 "CREATE NEW TEAM"]
[:& form {:on-submit on-submit
:spec ::team-form
:initial {}}
[:& input {:type "text"
:name :name
:label "Enter new team name:"}]
[:div.buttons-row
[:& submit-button
{:label "Create team"}]]]]]]))
(mf/defc sidebar-content (mf/defc sidebar-content
[{:keys [locale projects profile section team project search-term] :as props}] [{:keys [locale projects profile section team project search-term] :as props}]
@ -283,15 +358,27 @@
(d/seek :is-default) (d/seek :is-default)
(:id)) (:id))
team-id (:id team)
projects? (= section :dashboard-projects) projects? (= section :dashboard-projects)
libs? (= section :dashboard-libraries) libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files) drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id)) (= (:id project) default-project-id))
go-projects #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})) go-projects
go-default #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id})) (mf/use-callback
go-libs #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})) (mf/deps team)
(st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
go-drafts
(mf/use-callback
(mf/deps team default-project-id)
(fn []
(st/emit! (rt/nav :dashboard-files
{:team-id (:id team)
:project-id default-project-id}))))
go-libs
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-libraries {:team-id (:id team)})))
pinned-projects pinned-projects
(->> (vals projects) (->> (vals projects)
@ -299,8 +386,7 @@
(filter :is-pinned))] (filter :is-pinned))]
[:div.sidebar-content [:div.sidebar-content
[:& sidebar-team-switch {:team team :profile profile}] [:& sidebar-team-switch {:team team :profile profile :locale locale}]
[:hr] [:hr]
[:& sidebar-search {:search-term search-term [:& sidebar-search {:search-term search-term
:team-id (:id team) :team-id (:id team)
@ -313,7 +399,7 @@
i/recent i/recent
[:span.element-title (t locale "dashboard.sidebar.projects")]] [:span.element-title (t locale "dashboard.sidebar.projects")]]
[:li {:on-click go-default [:li {:on-click go-drafts
:class-name (when drafts? "current")} :class-name (when drafts? "current")}
i/file-html i/file-html
[:span.element-title (t locale "dashboard.sidebar.drafts")]] [:span.element-title (t locale "dashboard.sidebar.drafts")]]
@ -337,7 +423,7 @@
:selected? (= (:id item) (:id project))}])] :selected? (= (:id item) (:id project))}])]
[:div.sidebar-empty-placeholder [:div.sidebar-empty-placeholder
[:span.icon i/pin] [:span.icon i/pin]
[:span.text "Pinned projects will appear here"]])]])) [:span.text (t locale "dashboard.sidebar.no-projects-placeholder")]])]]))
(mf/defc profile-section (mf/defc profile-section
@ -365,30 +451,25 @@
[:ul.dropdown [:ul.dropdown
[:li {:on-click (partial on-click :settings-profile)} [:li {:on-click (partial on-click :settings-profile)}
[:span.icon i/user] [:span.icon i/user]
[:span.text (t locale "dashboard.header.profile-menu.profile")]] [:span.text (t locale "dashboard.sidebar.profile")]]
[:hr] [:hr]
[:li {:on-click (partial on-click :settings-password)} [:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock] [:span.icon i/lock]
[:span.text (t locale "dashboard.header.profile-menu.password")]] [:span.text (t locale "dashboard.sidebar.password")]]
[:hr] [:hr]
[:li {:on-click (partial on-click da/logout)} [:li {:on-click (partial on-click da/logout)}
[:span.icon i/exit] [:span.icon i/exit]
[:span.text (t locale "dashboard.header.profile-menu.logout")]]]]])) [:span.text (t locale "dashboard.logout")]]]]]))
(mf/defc sidebar (mf/defc sidebar
{::mf/wrap-props false {::mf/wrap-props false
::mf/wrap [mf/memo]} ::mf/wrap [mf/memo]}
[props] [props]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile) profile (obj/get props "profile")
props (-> (obj/clone props) props (-> (obj/clone props)
(obj/set! "locale" locale) (obj/set! "locale" locale))]
(obj/set! "profile" profile))]
[:div.dashboard-sidebar [:div.dashboard-sidebar
[:div.sidebar-inside [:div.sidebar-inside
[:> sidebar-content props] [:> sidebar-content props]
[:& profile-section {:profile profile [:& profile-section {:profile profile :locale locale}]]]))
:locale locale}]]]))

View file

@ -0,0 +1,301 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.dashboard.team
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.main.constants :as c]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.team-form]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.router :as rt]
[app.util.time :as dt]
[cljs.spec.alpha :as s]
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section locale team] :as props}]
(let [go-members
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
go-settings
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
invite-member
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show {:type ::invite-member
:team team})))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)]
[:header.dashboard-header
[:div.dashboard-title
[:h1 "Projects"]]
[:nav
[:ul
[:li {:class (when members-section? "active")}
[:a {:on-click go-members} "MEMBERS"]]
[:li {:class (when settings-section? "active")}
[:a {:on-click go-settings} "SETTINGS"]]]]
(if members-section?
[:a.btn-secondary.btn-small {:on-click invite-member}
(t locale "dashboard.header.invite-profile")]
[:div])]))
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-member-form
(s/keys :req-un [::role ::email]))
(mf/defc invite-member-modal
{::mf/register modal/components
::mf/register-as ::invite-member}
[{:keys [team] :as props}]
(let [roles [{:value "" :label "Role"}
{:value "admin" :label "Admin"}
{:value "editor" :label "Editor"}
{:value "viewer" :label "Viewer"}]
initial (mf/use-memo (mf/deps team) (constantly {:team-id (:id team)}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
on-success
(mf/use-callback
(mf/deps team)
(st/emitf (dm/success "Invitation sent successfully")))
on-submit
(mf/use-callback
(mf/deps team)
(fn [form]
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)}]
(st/emit! (dd/invite-team-member (with-meta params mdata))))))]
(prn "invite-member-modal" @form)
[:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form}
[:div.title
[:span.text "Invite a new team member"]]
[:div.form-row
[:& fm/input {:name :email
:label "Introduce an email"}]
[:& fm/select {:name :role
:options roles}]]
[:div.action-buttons
[:& fm/submit-button {:label "Send invitation"}]]]]))
(mf/defc team-member
[{:keys [team member profile] :as props}]
(let [show? (mf/use-state false)
set-role
#(st/emit! (dd/update-team-member-role {:team-id (:id team)
:member-id (:id member)
:role %}))
set-owner-fn
(partial set-role :owner)
set-admin
(mf/use-callback (mf/deps team member) (partial set-role :admin))
set-editor
(mf/use-callback (mf/deps team member) (partial set-role :editor))
set-viewer
(mf/use-callback (mf/deps team member) (partial set-role :viewer))
set-owner
(mf/use-callback
(mf/deps team member)
(st/emitf (modal/show
{:type :confirm
:title "Promoto to owner"
:message "Are you sure you wan't to promote this user to owner?"
:accept-label "Promote"
:on-accept set-owner-fn})))
delete-fn
(st/emitf (dd/delete-team-member {:team-id (:id team) :member-id (:id member)}))
delete
(mf/use-callback
(mf/deps team member)
(st/emitf (modal/show
{:type :confirm
:title "Delete team member"
:message "Are you sure wan't to delete this user from team?"
:accept-label "Delete"
:on-accept delete-fn})))]
[:div.table-row
[:div.table-field.name (:name member)]
[:div.table-field.email (:email member)]
[:div.table-field.permissions
[:*
(cond
(:is-owner member)
[:span.label "Owner"]
(:is-admin member)
[:span.label "Admin"]
(:can-edit member)
[:span.label "Editor"]
:else
[:span.label "Viewer"])
(when (and (not (:is-owner member))
(or (:is-admin team)
(:is-owner team)))
[:span.icon {:on-click #(reset! show? true)} i/arrow-down])]
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:ul.dropdown.options-dropdown
[:li {:on-click set-admin} "Admin"]
[:li {:on-click set-editor} "Editor"]
[:li {:on-click set-viewer} "Viewer"]
(when (:is-owner team)
[:*
[:hr]
[:li {:on-click set-owner} "Promote to owner"]])
[:hr]
(when (and (or (:is-owner team)
(:is-admin team))
(not= (:id profile)
(:id member)))
[:li {:on-click delete} "Remove"])]]]]))
(mf/defc team-members
[{:keys [members-map team profile] :as props}]
(let [members (->> (vals members-map)
(sort-by :created-at)
(remove :is-owner))
owner (->> (vals members-map)
(d/seek :is-owner))]
[:div.dashboard-table
[:div.table-header
[:div.table-field.name "Name"]
[:div.table-field.email "Email"]
[:div.table-field.permissions "Permissions"]]
[:div.table-rows
[:& team-member {:member owner :team team :profile profile}]
(for [item members]
[:& team-member {:member item :team team :profile profile :key (:id item)}])]]))
(defn- members-ref
[team-id]
(l/derived (l/in [:team-members team-id]) st/state))
(mf/defc team-members-page
[{:keys [team profile] :as props}]
(let [locale (mf/deref i18n/locale)
members-ref (mf/use-memo (mf/deps team) #(members-ref (:id team)))
members-map (mf/deref members-ref)]
(mf/use-effect
(mf/deps team)
(st/emitf (dd/fetch-team-members team)))
[:*
[:& header {:locale locale
:section :dashboard-team-members
:team team}]
[:section.dashboard-container.dashboard-team-members
[:& team-members {:locale locale
:profile profile
:team team
:members-map members-map}]]]))
(mf/defc team-settings-page
[{:keys [team profile] :as props}]
(let [locale (mf/deref i18n/locale)
finput (mf/use-ref)
members-ref (mf/use-memo (mf/deps team) #(members-ref (:id team)))
members-map (mf/deref members-ref)
on-image-click
(mf/use-callback #(dom/click (mf/ref-val finput)))
on-file-selected
(mf/use-callback
(mf/deps team)
(fn [file]
(st/emit! (dd/update-team-photo {:file file
:team-id (:id team)}))))]
(mf/use-effect
(mf/deps team)
(st/emitf (dd/fetch-team-members team)))
[:*
[:& header {:locale locale
:section :dashboard-team-settings
:team team}]
[:section.dashboard-container.dashboard-team-settings
[:div.team-settings
[:div.horizontal-blocks
[:div.block.info-block
[:div.label "Team info"]
[:div.name (:name team)]
[:div.icon
[:span.update-overlay {:on-click on-image-click} i/exit]
[:img {:src (cfg/resolve-media-path (:photo team))}]
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:input-ref finput
:on-selected on-file-selected}]]]
[:div.block.owner-block
[:div.label "Team members"]
[:div.owner
[:span.icon [:img {:src (cfg/resolve-media-path (:photo-uri profile))}]]
[:span.text (:fullname profile)]]
[:div.summary
[:span.icon i/user]
[:span.text (t locale "dashboard.team.num-of-members" (count members-map))]]]
[:div.block.stats-block
[:div.label "Team projects"]
[:div.projects
[:span.icon i/folder]
[:span.text "4 projects"]]
[:div.files
[:span.icon i/file-html]
[:span.text "4 files"]]]]]]]))

View file

@ -0,0 +1,117 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.dashboard.team-form
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.config :as cfg]
[app.main.data.auth :as da]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(defn- on-create-success
[form response]
(let [msg "Team created successfuly"]
(st/emit! (dm/success msg)
(modal/hide)
(rt/nav :dashboard-projects {:team-id (:id response)}))))
(defn- on-update-success
[form response]
(let [msg "Team created successfuly"]
(st/emit! (dm/success msg)
(modal/hide))))
(defn- on-error
[form response]
(let [id (get-in @form [:clean-data :id])]
(if id
(st/emit! (dm/error "Error on updating team."))
(st/emit! (dm/error "Error on creating team.")))))
;; TODO: check global error handler
(defn- on-create-submit
[form]
(let [mdata {:on-success (partial on-create-success form)
:on-error (partial on-error form)}
params {:name (get-in @form [:clean-data :name])}]
(st/emit! (dd/create-team (with-meta params mdata)))))
(defn- on-update-submit
[form]
(let [mdata {:on-success (partial on-update-success form)
:on-error (partial on-error form)}
team (get @form :clean-data)]
(st/emit! (dd/update-team (with-meta team mdata))
(modal/hide))))
(mf/defc team-form-modal
{::mf/register modal/components
::mf/register-as :team-form}
[{:keys [team] :as props}]
(let [locale (mf/deref i18n/locale)
form (fm/use-form :spec ::team-form
:initial (or team {}))
on-submit
(mf/use-callback
(mf/deps team)
(if team
(partial on-update-submit form)
(partial on-create-submit form)))]
[:div.modal-overlay
[:div.modal-container.team-form-modal
[:div.modal-header
[:div.modal-header-title
(if team
[:h2 "Rename team"]
[:h2 "Create new team"])]
[:div.modal-close-button
{:on-click (st/emitf (modal/hide))} i/close]]
[:div.modal-content.generic-form
[:form
[:& input {:type "text"
:form form
:name :name
:label "Enter new team name:"}]]]
[:div.modal-footer
[:div.action-buttons
[:& submit-button
{:form form
:on-click on-submit
:label (if team
"Update team"
"Create team")}]]]]]))

View file

@ -14,53 +14,33 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.keyboard :as k] [app.main.ui.keyboard :as k]
[app.main.data.modal :as dm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.main.refs :as refs] [app.main.refs :as refs])
[potok.core :as ptk]
[app.main.data.modal :as mdm])
(:import goog.events.EventType)) (:import goog.events.EventType))
(defonce components (atom {}))
(defn show!
[type props]
(let [id (random-uuid)]
(st/emit! (mdm/show-modal id type props))))
(defn allow-click-outside! []
(st/emit! (mdm/update-modal {:allow-click-outside true})))
(defn disallow-click-outside! []
(st/emit! (mdm/update-modal {:allow-click-outside false})))
(defn hide!
[]
(st/emit! (mdm/hide-modal)))
(def hide (mdm/hide-modal))
(defn- on-esc-clicked (defn- on-esc-clicked
[event] [event]
(when (k/esc? event) (when (k/esc? event)
(hide!) (st/emit! (dm/hide))
(dom/stop-propagation event))) (dom/stop-propagation event)))
(defn- on-pop-state (defn- on-pop-state
[event] [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(hide!) (st/emit! (dm/hide))
(.forward js/history)) (.forward js/history))
(defn- on-parent-clicked (defn- on-parent-clicked
[event parent-ref] [event parent-ref]
(let [parent (mf/ref-val parent-ref) (let [parent (mf/ref-val parent-ref)
current (dom/get-target event)] current (dom/get-target event)]
(when (and (dom/equals? (.-firstElementChild ^js parent) current) (when (and (dom/equals? (.-firstElementChild ^js parent) current)
(= (.-className ^js current) "modal-overlay")) (= (.-className ^js current) "modal-overlay"))
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(hide!)))) (st/emit! (dm/hide)))))
(defn- on-click-outside (defn- on-click-outside
[event wrapper-ref allow-click-outside] [event wrapper-ref allow-click-outside]
@ -70,7 +50,7 @@
(when (and wrapper (not allow-click-outside) (not (.contains wrapper current))) (when (and wrapper (not allow-click-outside) (not (.contains wrapper current)))
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(hide!)))) (st/emit! (dm/hide)))))
(mf/defc modal-wrapper (mf/defc modal-wrapper
{::mf/wrap-props false {::mf/wrap-props false
@ -78,6 +58,7 @@
[props] [props]
(let [data (unchecked-get props "data") (let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil) wrapper-ref (mf/use-ref nil)
handle-click-outside handle-click-outside
(fn [event] (fn [event]
(on-click-outside event wrapper-ref (:allow-click-outside data)))] (on-click-outside event wrapper-ref (:allow-click-outside data)))]
@ -89,14 +70,15 @@
(events/listen js/document EventType.CLICK handle-click-outside)]] (events/listen js/document EventType.CLICK handle-click-outside)]]
#(for [key keys] #(for [key keys]
(events/unlistenByKey key))))) (events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref} [:div.modal-wrapper {:ref wrapper-ref}
(mf/element (mf/element
(get @components (:type data)) (get @dm/components (:type data))
(:props data))])) (:props data))]))
(def modal-ref (def modal-ref
(l/derived ::mdm/modal st/state)) (l/derived ::dm/modal st/state))
(mf/defc modal (mf/defc modal
[] []

View file

@ -9,30 +9,46 @@
(ns app.main.ui.settings (ns app.main.ui.settings
(:require (:require
[cuerdas.core :as str]
[potok.core :as ptk]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.util.router :as rt]
[app.main.ui.settings.header :refer [header]]
[app.main.ui.settings.password :refer [password-page]]
[app.main.ui.settings.options :refer [options-page]] [app.main.ui.settings.options :refer [options-page]]
[app.main.ui.settings.profile :refer [profile-page]])) [app.main.ui.settings.password :refer [password-page]]
[app.main.ui.settings.profile :refer [profile-page]]
[app.main.ui.settings.sidebar :refer [sidebar]]
[app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account]
[app.util.i18n :as i18n :refer [t]]
[rumext.alpha :as mf]))
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [locale] :as props}]
(let [logout (constantly nil)]
[:header.dashboard-header
[:h1.dashboard-title (t locale "dashboard.header.your-account")]
[:a.btn-secondary.btn-small {:on-click logout}
(t locale "dashboard.logout")]]))
(mf/defc settings (mf/defc settings
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
profile (mf/deref refs/profile)] profile (mf/deref refs/profile)
[:main.settings-main locale (mf/deref i18n/locale)]
[:div.settings-content [:section.dashboard-layout
[:& header {:section section :profile profile}] [:& sidebar {:profile profile
(case section :locale locale
:settings-profile (mf/element profile-page) :section section}]
:settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))
[:div.dashboard-content
[:& header {:locale locale}]
[:section.dashboard-container
(case section
:settings-profile
[:& profile-page {:locale locale}]
:settings-password
[:& password-page {:locale locale}]
:settings-options
[:& options-page {:locale locale}])]]]))

View file

@ -12,14 +12,15 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.modal :as modal]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -47,55 +48,65 @@
(assoc-in data [:errors :email-1] error)))) (assoc-in data [:errors :email-1] error))))
:else :else
(let [msg (tr "errors.unexpected-error")] (rx/throw error)))
(st/emit! (dm/error msg)))))
(defn- on-success (defn- on-success
[profile data] [form data]
(let [msg (tr "auth.notifications.validation-email-sent" (:email profile))] (let [email (get-in @form [:clean-data :email-1])
(st/emit! (dm/info msg) modal/hide))) message (tr "auth.notifications.validation-email-sent" email)]
(st/emit! (dm/info message)
(modal/hide))))
(defn- on-submit (defn- on-submit
[profile form event] [form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])} (let [params {:email (get-in @form [:clean-data :email-1])}
{:on-error (partial on-error form) mdata {:on-error (partial on-error form)
:on-success (partial on-success profile)})] :on-success (partial on-success form)}]
(st/emit! (du/request-email-change data)))) (st/emit! (du/request-email-change (with-meta params mdata)))))
(mf/defc change-email-form
[{:keys [locale profile] :as props}]
[:section.modal-content.generic-form
[:h2 (t locale "settings.change-email-title")]
[:& msgs/inline-banner
{:type :info
:content (t locale "settings.change-email-info" (:email profile))}]
[:& form {:on-submit (partial on-submit profile)
:spec ::email-change-form
:validators [email-equality]
:initial {}}
[:& input {:type "text"
:name :email-1
:label (t locale "settings.new-email-label")
:trim true}]
[:& input {:type "text"
:name :email-2
:label (t locale "settings.confirm-email-label")
:trim true}]
[:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-modal (mf/defc change-email-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :change-email} ::mf/register-as :change-email}
[props] []
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)] profile (mf/deref refs/profile)
[:div.modal-overlay form (fm/use-form :spec ::email-change-form
[:div.generic-modal.change-email-modal :validators [email-equality]
[:span.close {:on-click #(modal/hide!)} i/close] :initial profile)
[:& change-email-form {:locale locale :profile profile}]]])) on-close
(mf/use-callback (st/emitf (modal/hide)))]
[:div.modal-overlay
[:div.modal-container.change-email-modal.form-container
[:& fm/form {:form form
:on-submit on-submit}
[:div.modal-header
[:div.modal-header-title
[:h2 (t locale "dashboard.settings.change-email-title")]]
[:div.modal-close-button
{:on-click on-close} i/close]]
[:div.modal-content
[:& msgs/inline-banner
{:type :info
:content (t locale "dashboard.settings.change-email-info" (:email profile))}]
[:div.fields-row
[:& fm/input {:type "text"
:name :email-1
:label (t locale "dashboard.settings.new-email-label")
:trim true}]]
[:div.fields-row
[:& fm/input {:type "text"
:name :email-2
:label (t locale "dashboard.settings.confirm-email-label")
:trim true}]]]
[:div.modal-footer
[:div.action-buttons
[:& fm/submit-button
{:label (t locale "dashboard.settings.change-email-submit-label")}]]]]]]))

View file

@ -10,37 +10,61 @@
(ns app.main.ui.settings.delete-account (ns app.main.ui.settings.delete-account
(:require (:require
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.util.router :as rt]
[app.util.i18n :as i18n :refer [tr t]])) [app.util.i18n :as i18n :refer [tr t]]))
(defn on-error
[{:keys [code] :as error}]
(if (= :owner-teams-with-people code)
(let [msg (tr "dashboard.notifications.profile-deletion-not-allowed")]
(rx/of (dm/error msg)))
(rx/throw error)))
(defn on-success
[x]
(st/emit! (rt/nav :auth-goodbye)))
(mf/defc delete-account-modal (mf/defc delete-account-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :delete-account} ::mf/register-as :delete-account}
[props] [props]
(let [locale (mf/deref i18n/locale)] (let [locale (mf/deref i18n/locale)
on-close
(mf/use-callback (st/emitf (modal/hide)))
on-accept
(mf/use-callback
(st/emitf (modal/hide)
(da/request-account-deletion
(with-meta {} {:on-error on-error
:on-success on-success}))))]
[:div.modal-overlay [:div.modal-overlay
[:section.generic-modal.change-email-modal [:div.modal-container.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close] [:div.modal-header
[:div.modal-header-title
[:section.modal-content.generic-form [:h2 (t locale "dashboard.settings.delete-account-title")]]
[:h2 (t locale "settings.delete-account-title")] [:div.modal-close-button
{:on-click on-close} i/close]]
[:div.modal-content
[:& msgs/inline-banner [:& msgs/inline-banner
{:type :warning {:type :warning
:content (t locale "settings.delete-account-info")}] :content (t locale "dashboard.settings.delete-account-info")}]]
[:div.modal-footer
[:div.action-buttons
[:button.btn-warning.btn-large {:on-click on-accept}
(t locale "dashboard.settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large {:on-click on-close}
(t locale "dashboard.settings.cancel-and-keep-my-account")]]]]]))
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]]))

View file

@ -1,59 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.settings.header
(:require
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.main.data.auth :as da]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt]))
(mf/defc header
[{:keys [section profile] :as props}]
(let [profile? (= section :settings-profile)
password? (= section :settings-password)
options? (= section :settings-options)
team-id (:default-team-id profile)
go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
logout #(st/emit! da/logout)
locale (mf/deref i18n/locale)
team-id (:default-team-id profile)]
[:header
[:section.secondary-menu
[:div.left {:on-click go-back}
[:span.icon i/arrow-slide]
[:span.label "Dashboard"]]
[:div.right {:on-click logout}
[:span.label "Log out"]
[:span.icon i/logout]]]
[:h1 "Your account"]
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]
[:a.nav-item
{:class (when options? "current")
:on-click #(st/emit! (rt/nav :settings-options))}
(t locale "settings.options")]]]))
;; [:a.nav-item
;; {:class "foobar"
;; :on-click #(st/emit! (rt/nav :settings-profile))}
;; (t locale "settings.teams")]]]))

View file

@ -9,69 +9,72 @@
(ns app.main.ui.settings.options (ns app.main.ui.settings.options
(:require (:require
[rumext.alpha :as mf] [app.common.spec :as us]
[cljs.spec.alpha :as s]
[app.main.ui.icons :as i]
[app.main.data.users :as udu]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.ui.components.forms :refer [select submit-button form]] [app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.i18n :as i18n :refer [t tr]]
[app.util.i18n :as i18n :refer [t tr]])) [cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(s/def ::lang (s/nilable ::fm/not-empty-string)) (s/def ::lang (s/nilable ::us/not-empty-string))
(s/def ::theme (s/nilable ::fm/not-empty-string)) (s/def ::theme (s/nilable ::us/not-empty-string))
(s/def ::options-form (s/def ::options-form
(s/keys :opt-un [::lang ::theme])) (s/keys :opt-un [::lang ::theme]))
(defn- on-error (defn- on-error
[form error]) [form error]
(st/emit! (dm/error (tr "errors.generic"))))
(defn- on-success
[form]
(st/emit! (dm/success (tr "dashboard.notifications.profile-saved"))))
(defn- on-submit (defn- on-submit
[form event] [form event]
(dom/prevent-default event) (let [data (:clean-data @form)
(let [data (:clean-data form) mdata {:on-success (partial on-success form)
on-success #(st/emit! (dm/success (tr "settings.notifications.profile-saved"))) :on-error (partial on-error form)}]
on-error #(on-error % form)] (st/emit! (du/update-profile (with-meta data mdata)))))
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
(mf/defc options-form (mf/defc options-form
[{:keys [locale profile] :as props}] [{:keys [locale] :as props}]
[:& form {:class "options-form" (let [profile (mf/deref refs/profile)
:on-submit on-submit form (fm/use-form :spec ::options-form
:spec ::options-form :initial profile)]
:initial profile} [:& fm/form {:class "options-form"
:on-submit on-submit
:form form}
[:h2 (t locale "settings.language-change-title")] [:h2 (t locale "dashboard.settings.language-change-title")]
[:& select {:options [{:label "English" :value "en"} [:div.fields-row
{:label "Français" :value "fr"} [:& fm/select {:options [{:label "English" :value "en"}
{:label "Español" :value "es"} {:label "Français" :value "fr"}
{:label "Русский" :value "ru"}] {:label "Español" :value "es"}
:label (t locale "settings.language-label") {:label "Русский" :value "ru"}]
:default "en" :label (t locale "dashboard.settings.language-label")
:name :lang}] :default "en"
:name :lang}]]
[:h2 (t locale "settings.theme-change-title")] [:h2 (t locale "dashboard.settings.theme-change-title")]
[:& select {:label (t locale "settings.theme-label") [:div.fields-row
:name :theme [:& fm/select {:label (t locale "dashboard.settings.theme-label")
:default "default" :name :theme
:options [{:label "Default" :value "default"}]}] :default "default"
:options [{:label "Default" :value "default"}]}]]
[:& submit-button [:& fm/submit-button
{:label (t locale "settings.profile-submit-label")}]]) {:label (t locale "dashboard.settings.profile-submit-label")}]]))
;; --- Password Page ;; --- Password Page
(mf/defc options-page (mf/defc options-page
[props] [{:keys [locale]}]
(let [locale (mf/deref i18n/locale) [:div.dashboard-settings
profile (mf/deref refs/profile)] [:div.form-container
[:section.settings-options.generic-form [:& options-form {:locale locale}]]])
[:div.forms-container
[:& options-form {:locale locale :profile profile}]]]))

View file

@ -9,16 +9,16 @@
(ns app.main.ui.settings.password (ns app.main.ui.settings.password
(:require (:require
[rumext.alpha :as mf] [app.common.spec :as us]
[cljs.spec.alpha :as s]
[app.main.ui.icons :as i]
[app.main.data.users :as udu]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.data.users :as udu]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.i18n :as i18n :refer [t tr]]
[app.util.i18n :as i18n :refer [t tr]])) [cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(defn- on-error (defn- on-error
[form error] [form error]
@ -33,20 +33,20 @@
(defn- on-success (defn- on-success
[form] [form]
(let [msg (tr "settings.notifications.password-saved")] (let [msg (tr "dashboard.notifications.password-saved")]
(st/emit! (dm/success msg)))) (st/emit! (dm/success msg))))
(defn- on-submit (defn- on-submit
[form event] [form event]
(dom/prevent-default event) (dom/prevent-default event)
(let [params (with-meta (:clean-data form) (let [params (with-meta (:clean-data @form)
{:on-success (partial on-success form) {:on-success (partial on-success form)
:on-error (partial on-error form)})] :on-error (partial on-error form)})]
(st/emit! (udu/update-password params)))) (st/emit! (udu/update-password params))))
(s/def ::password-1 ::fm/not-empty-string) (s/def ::password-1 ::us/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string) (s/def ::password-2 ::us/not-empty-string)
(s/def ::password-old ::fm/not-empty-string) (s/def ::password-old ::us/not-empty-string)
(defn- password-equality (defn- password-equality
[data] [data]
@ -67,36 +67,38 @@
(mf/defc password-form (mf/defc password-form
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
[:& form {:class "password-form" (let [form (fm/use-form :spec ::password-form
:on-submit on-submit :validators [password-equality]
:spec ::password-form :initial {})]
:validators [password-equality] [:& fm/form {:class "password-form"
:initial {}} :on-submit on-submit
[:h2 (t locale "settings.password-change-title")] :form form}
[:h2 (t locale "dashboard.settings.password-change-title")]
[:div.fields-row
[:& fm/input
{:type "password"
:name :password-old
:label (t locale "dashboard.settings.old-password-label")}]]
[:& input [:div.fields-row
{:type "password" [:& fm/input
:name :password-old {:type "password"
:label (t locale "settings.old-password-label")}] :name :password-1
:label (t locale "dashboard.settings.new-password-label")}]]
[:& input [:div.fields-row
{:type "password" [:& fm/input
:name :password-1 {:type "password"
:label (t locale "settings.new-password-label")}] :name :password-2
:label (t locale "dashboard.settings.confirm-password-label")}]]
[:& input [:& fm/submit-button
{:type "password" {:label (t locale "dashboard.settings.profile-submit-label")}]]))
:name :password-2
:label (t locale "settings.confirm-password-label")}]
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page ;; --- Password Page
(mf/defc password-page (mf/defc password-page
[props] [{:keys [locale]}]
(let [locale (mf/deref i18n/locale)] [:section.dashboard-settings.form-container
[:section.settings-password.generic-form [:div.form-container
[:div.forms-container [:& password-form {:locale locale}]]])
[:& password-form {:locale locale}]]]))

View file

@ -9,91 +9,80 @@
(ns app.main.ui.settings.profile (ns app.main.ui.settings.profile
(:require (:require
[cljs.spec.alpha :as s] [app.common.spec :as us]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as udu] [app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]]
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.modal :as modal]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr t]]
[app.util.i18n :as i18n :refer [tr t]])) [cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(s/def ::fullname ::fm/not-empty-string) (s/def ::fullname ::us/not-empty-string)
(s/def ::email ::fm/email) (s/def ::email ::us/email)
(s/def ::profile-form (s/def ::profile-form
(s/keys :req-un [::fullname ::lang ::theme ::email])) (s/keys :req-un [::fullname ::lang ::theme ::email]))
(defn- on-success
[form]
(st/emit! (dm/success (tr "dashboard.notifications.profile-saved"))))
(defn- on-error (defn- on-error
[error form] [form error]
(st/emit! (dm/error (tr "errors.generic")))) (st/emit! (dm/error (tr "errors.generic"))))
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [data (:clean-data form) (let [data (:clean-data @form)
on-success #(st/emit! (dm/success (tr "settings.notifications.profile-saved"))) mdata {:on-success (partial on-success form)
on-error #(on-error % form)] :on-error (partial on-error form)}]
(st/emit! (udu/update-profile (with-meta data (st/emit! (du/update-profile (with-meta data mdata)))))
{:on-success on-success
:on-error on-error})))))
;; --- Profile Form ;; --- Profile Form
(mf/defc profile-form (mf/defc profile-form
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
(let [prof (mf/deref refs/profile)] (let [profile (mf/deref refs/profile)
[:& form {:on-submit on-submit form (fm/use-form :spec ::profile-form
:class "profile-form" :initial profile)]
:spec ::profile-form [:& fm/form {:on-submit on-submit
:initial prof} :form form
[:& input :class "profile-form"}
{:type "text" [:div.fields-row
:name :fullname [:& fm/input
:label (t locale "settings.fullname-label") {:type "text"
:trim true}] :name :fullname
:label (t locale "dashboard.settings.fullname-label")}]]
[:& input [:div.fields-row
{:type "email" [:& fm/input
:name :email {:type "email"
:disabled true :name :email
:help-icon i/at :disabled true
:label (t locale "settings.email-label")}] :help-icon i/at
:label (t locale "dashboard.settings.email-label")}]
(cond [:div.options
(nil? (:pending-email prof))
[:div.change-email [:div.change-email
[:a {:on-click #(modal/show! :change-email {})} [:a {:on-click #(modal/show! :change-email {})}
(t locale "settings.change-email-label")]] (t locale "dashboard.settings.change-email-label")]]]]
(not= (:pending-email prof) (:email prof)) [:& fm/submit-button
[:& msgs/inline-banner {:label (t locale "dashboard.settings.profile-submit-label")}]
{:type :info
:content (t locale "settings.change-email-info3" (:pending-email prof))
:actions [{:label (t locale "settings.cancel-email-change")
:callback #(st/emit! udu/cancel-email-change)}]}]
;; [:div.btn-secondary.btn-small
;; {:on-click #(st/emit! udu/cancel-email-change)}
;; (t locale "settings.cancel-email-change")]]
:else
[:& msgs/inline-banner
{:type :info
:content (t locale "settings.email-verification-pending")}])
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]
[:div.links [:div.links
[:div.link-item [:div.link-item
[:a {:on-click #(modal/show! :delete-account {})} [:a {:on-click #(modal/show! :delete-account {})}
(t locale "settings.remove-account-label")]]]])) (t locale "dashboard.settings.remove-account-label")]]]]))
;; --- Profile Photo Form ;; --- Profile Photo Form
@ -110,11 +99,11 @@
on-file-selected on-file-selected
(fn [file] (fn [file]
(st/emit! (udu/update-photo file)))] (st/emit! (du/update-photo file)))]
[:form.avatar-form [:form.avatar-form
[:div.image-change-field [:div.image-change-field
[:span.update-overlay {:on-click on-image-click} (t locale "settings.update-photo-label")] [:span.update-overlay {:on-click on-image-click} (t locale "dashboard.settings.update-photo-label")]
[:img {:src photo}] [:img {:src photo}]
[:& file-uploader {:accept "image/jpeg,image/png" [:& file-uploader {:accept "image/jpeg,image/png"
:multi false :multi false
@ -124,10 +113,9 @@
;; --- Profile Page ;; --- Profile Page
(mf/defc profile-page (mf/defc profile-page
{::mf/wrap-props false} [{:keys [locale]}]
[props] [:div.dashboard-settings
(let [locale (i18n/use-locale)] [:div.form-container.two-columns
[:section.settings-profile.generic-form [:& profile-photo-form {:locale locale}]
[:div.forms-container [:& profile-form {:locale locale}]]])
[:& profile-photo-form {:locale locale}]
[:& profile-form {:locale locale}]]]))

View file

@ -0,0 +1,91 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.settings.sidebar
(:require
[app.common.spec :as us]
[app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.sidebar :refer [profile-section]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[goog.functions :as f]
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc sidebar-content
[{:keys [locale profile section] :as props}]
(let [profile? (= section :settings-profile)
password? (= section :settings-password)
options? (= section :settings-options)
go-dashboard
(mf/use-callback
(mf/deps profile)
(st/emitf (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})))
go-settings-profile
(mf/use-callback
(mf/deps profile)
(st/emitf (rt/nav :settings-profile)))
go-settings-password
(mf/use-callback
(mf/deps profile)
(st/emitf (rt/nav :settings-password)))
go-settings-options
(mf/use-callback
(mf/deps profile)
(st/emitf (rt/nav :settings-options)))]
[:div.sidebar-content
[:div.sidebar-content-section
[:div.back-to-dashboard {:on-click go-dashboard}
[:span.icon i/arrow-down]
[:span.text "Dashboard"]]]
[:hr]
[:div.sidebar-content-section
[:ul.sidebar-nav.no-overflow
[:li {:class (when profile? "current")
:on-click go-settings-profile}
i/user
[:span.element-title (t locale "dashboard.sidebar.profile")]]
[:li {:class (when password? "current")
:on-click go-settings-password}
i/lock
[:span.element-title (t locale "dashboard.sidebar.password")]]
[:li {:class (when options? "current")
:on-click go-settings-options}
i/tree
[:span.element-title (t locale "dashboard.sidebar.settings")]]]]]))
(mf/defc sidebar
{::mf/wrap [mf/memo]}
[{:keys [profile locale section]}]
[:div.dashboard-sidebar.settings
[:div.sidebar-inside
[:& sidebar-content {:locale locale
:profile profile
:section section}]
[:& profile-section {:profile profile
:locale locale}]]])

View file

@ -20,9 +20,11 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.workspace.colorpalette :refer [colorpalette]] [app.main.ui.workspace.colorpalette :refer [colorpalette]]
[app.main.ui.workspace.colorpicker]
[app.main.ui.workspace.context-menu :refer [context-menu]] [app.main.ui.workspace.context-menu :refer [context-menu]]
[app.main.ui.workspace.header :refer [header]] [app.main.ui.workspace.header :refer [header]]
[app.main.ui.workspace.left-toolbar :refer [left-toolbar]] [app.main.ui.workspace.left-toolbar :refer [left-toolbar]]
[app.main.ui.workspace.libraries]
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
[app.main.ui.workspace.scroll :as scroll] [app.main.ui.workspace.scroll :as scroll]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]

View file

@ -19,7 +19,7 @@
[app.common.uuid :refer [uuid]] [app.common.uuid :refer [uuid]]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.colors :as dwc] [app.main.data.colors :as dwc]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[okulary.core :as l] [okulary.core :as l]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.util.i18n :as i18n :refer [t]])) [app.util.i18n :as i18n :refer [t]]))
@ -335,7 +335,7 @@
[:select {:on-change (fn [e] [:select {:on-change (fn [e]
(let [val (-> e dom/get-target dom/get-value)] (let [val (-> e dom/get-target dom/get-value)]
(reset! selected-library val))) (reset! selected-library val)))
:value @selected-library} :value @selected-library}
[:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")] [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")]
[:option {:value "file"} (t locale "workspace.libraries.colors.file-library")] [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")]
(for [[_ {:keys [name id]}] shared-libs] (for [[_ {:keys [name id]}] shared-libs]

View file

@ -18,7 +18,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.main.ui.workspace.presence :as presence] [app.main.ui.workspace.presence :as presence]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.util.i18n :as i18n :refer [t]] [app.util.i18n :as i18n :refer [t]]

View file

@ -18,7 +18,7 @@
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.modal :as modal])) [app.main.data.modal :as modal]))
(def workspace-file (def workspace-file
(l/derived :workspace-file st/state)) (l/derived :workspace-file st/state))

View file

@ -30,7 +30,7 @@
[app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]] [app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.main.ui.shapes.icon :as icon] [app.main.ui.shapes.icon :as icon]
[app.util.data :refer [matches-search]] [app.util.data :refer [matches-search]]
[app.util.dom :as dom] [app.util.dom :as dom]

View file

@ -14,7 +14,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.data :refer [classnames]] [app.util.data :refer [classnames]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.common.data :as d] [app.common.data :as d]
[app.main.refs :as refs])) [app.main.refs :as refs]))

View file

@ -16,7 +16,7 @@
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]] [app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt] [app.util.router :as rt]

View file

@ -18,7 +18,7 @@
[promesa.core :as p] [promesa.core :as p]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.cursors :as cur] [app.main.ui.cursors :as cur]
[app.main.ui.modal :as modal] [app.main.data.modal :as modal]
[app.common.data :as d] [app.common.data :as d]
[app.main.constants :as c] [app.main.constants :as c]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]

View file

@ -10,34 +10,20 @@
(ns app.util.forms (ns app.util.forms
(:refer-clojure :exclude [uuid]) (:refer-clojure :exclude [uuid])
(:require (:require
[app.common.spec :as us]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.timers :as tm]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf] [rumext.alpha :as mf]))
[app.common.spec :as us]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]))
;; --- Handlers Helpers ;; --- Handlers Helpers
(defn- impl-mutator
[v update-fn]
(specify v
IReset
(-reset! [_ new-value]
(update-fn new-value))
ISwap
(-swap!
([self f] (update-fn f))
([self f x] (update-fn #(f % x)))
([self f x y] (update-fn #(f % x y)))
([self f x y more] (update-fn #(apply f % x y more))))))
(defn- interpret-problem (defn- interpret-problem
[acc {:keys [path pred val via in] :as problem}] [acc {:keys [path pred val via in] :as problem}]
;; (prn "interpret-problem" problem)
(cond (cond
(and (empty? path) (and (empty? path)
(list? pred) (list? pred)
@ -51,45 +37,100 @@
:else acc)) :else acc))
(declare create-form-mutator)
(defn use-form (defn use-form
[& {:keys [spec validators initial]}] [& {:keys [spec validators initial] :as opts}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) (let [state (mf/useState 0)
:errors {} render (aget state 1)
:touched {}}) state-ref (mf/use-ref {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
form (mf/use-memo #(create-form-mutator state-ref render opts))]
cleaned (s/conform spec (:data state)) (mf/use-effect
problems (when (= ::s/invalid cleaned) (mf/deps initial)
(::s/problems (s/explain-data spec (:data state)))) (fn []
(if (fn? initial)
(swap! form update :data merge (initial))
(swap! form update :data merge initial))))
errors (merge (reduce interpret-problem {} problems) form))
(reduce (fn [errors vf]
(merge errors (vf (:data state))))
{} validators) (defn- wrap-update-fn
(:errors state))] [f {:keys [spec validators]}]
(-> (assoc state (fn [& args]
:errors errors (let [state (apply f args)
:clean-data (when (not= cleaned ::s/invalid) cleaned) cleaned (s/conform spec (:data state))
:valid (and (empty? errors) problems (when (= ::s/invalid cleaned)
(not= cleaned ::s/invalid))) (::s/problems (s/explain-data spec (:data state))))
(impl-mutator update-state))))
errors (merge (reduce interpret-problem {} problems)
(reduce (fn [errors vf]
(merge errors (vf (:data state))))
{} validators)
(:errors state))]
(assoc state
:errors errors
:clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
(not= cleaned ::s/invalid))))))
(defn- create-form-mutator
[state-ref render opts]
(reify
IDeref
(-deref [_]
(mf/ref-val state-ref))
IReset
(-reset! [it new-value]
(mf/set-ref-val! state-ref new-value)
(render inc))
ISwap
(-swap! [self f]
(let [f (wrap-update-fn f opts)]
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref)))
(render inc)))
(-swap! [self f x]
(let [f (wrap-update-fn f opts)]
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x))
(render inc)))
(-swap! [self f x y]
(let [f (wrap-update-fn f opts)]
(mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x y))
(render inc)))
(-swap! [self f x y more]
(let [f (wrap-update-fn f opts)]
(mf/set-ref-val! state-ref (apply f (mf/ref-val state-ref) x y more))
(render inc)))))
(defn on-input-change (defn on-input-change
([{:keys [data] :as form} field] ([form field]
(on-input-change form field false)) (on-input-change form field false))
([form field trim?]
([{:keys [data] :as form} field trim?]
(fn [event] (fn [event]
(let [target (dom/get-target event) (let [target (dom/get-target event)
value (dom/get-value target)] value (dom/get-value target)]
(swap! form (fn [state] (swap! form (fn [state]
(-> state (-> state
(assoc-in [:data field] (if trim? (str/trim value) value)) (assoc-in [:data field] (if trim? (str/trim value) value))
(update :errors dissoc field)))))))) (update :errors dissoc field))))))))
(defn on-input-blur (defn on-input-blur
[{:keys [touched] :as form} field] [form field]
(fn [event] (fn [event]
(let [target (dom/get-target event)] (let [target (dom/get-target event)
touched (get @form :touched)]
(when-not (get touched field) (when-not (get touched field)
(swap! form assoc-in [:touched field] true))))) (swap! form assoc-in [:touched field] true)))))

View file

@ -16,9 +16,10 @@
[app.util.transit :as t])) [app.util.transit :as t]))
(defn- conditional-decode (defn- conditional-decode
[{:keys [body headers] :as response}] [{:keys [body headers status] :as response}]
(let [contentype (get headers "content-type")] (let [contentype (get headers "content-type")]
(if (str/starts-with? contentype "application/transit+json") (if (and (str/starts-with? contentype "application/transit+json")
(pos? (count body)))
(assoc response :body (t/decode body)) (assoc response :body (t/decode body))
response))) response)))

View file

@ -5,34 +5,43 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(ns app.util.storage (ns app.util.storage
(:require [app.util.transit :as t])) (:require
[app.util.transit :as t]
[app.util.timers :as tm]
[app.common.exceptions :as ex]))
(defn- ^boolean is-worker? (defn- ^boolean is-worker?
[] []
(or (= *target* "nodejs") (or (= *target* "nodejs")
(not (exists? js/window)))) (not (exists? js/window))))
(defn- decode
[v]
(ex/ignoring (t/decode v)))
(def local
{:get #(decode (.getItem ^js js/localStorage (name %)))
:set #(.setItem ^js js/localStorage (name %1) (t/encode %2))})
(def session
{:get #(decode (.getItem ^js js/sessionStorage (name %)))
:set #(.setItem ^js js/sessionStorage (name %1) (t/encode %2))})
(defn- persist (defn- persist
[alias value] [alias storage value]
(when-not (is-worker?) (when-not (is-worker?)
(let [key (name alias) (tm/schedule-on-idle
value (t/encode value)] (fn [] ((:set storage) alias value)))))
(.setItem js/localStorage key value))))
(defn- load (defn- load
[alias] [alias storage]
(when-not (is-worker?) (when-not (is-worker?)
(let [data (.getItem js/localStorage (name alias))] ((:get storage) alias)))
(try
(t/decode data)
(catch :default e
(js/console.error "Error on loading data from local storage." e)
nil)))))
(defn- make-storage (defn- make-storage
[alias] [alias storage]
(let [data (atom (load alias))] (let [data (atom (load alias storage))]
(add-watch data :sub #(persist alias %4)) (add-watch data :sub #(persist alias storage %4))
(reify (reify
Object Object
(toString [_] (toString [_]
@ -66,5 +75,9 @@
(-lookup [_ key not-found] (-lookup [_ key not-found]
(get @data key not-found))))) (get @data key not-found)))))
(def storage
(make-storage "app")) (defonce storage
(make-storage "app" local))
(defonce cache
(make-storage "cache" session))

View file

@ -8,7 +8,9 @@
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.util.timers (ns app.util.timers
(:require [beicon.core :as rx])) (:require
[beicon.core :as rx]
[promesa.core :as p]))
(defn schedule (defn schedule
([func] ([func]
@ -19,6 +21,11 @@
(-dispose [_] (-dispose [_]
(js/clearTimeout sem)))))) (js/clearTimeout sem))))))
(defn asap
[f]
(-> (p/resolved nil)
(p/then f)))
(defn interval (defn interval
[ms func] [ms func]
(let [sem (js/setInterval #(func) ms)] (let [sem (js/setInterval #(func) ms)]