mirror of
https://github.com/penpot/penpot.git
synced 2025-05-20 15:56:11 +02:00
🎉 Add full teams administration.
This commit is contained in:
parent
f6830b4b85
commit
142036891a
62 changed files with 3175 additions and 1606 deletions
File diff suppressed because it is too large
Load diff
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
124
frontend/resources/styles/main/partials/dashboard-settings.scss
Normal file
124
frontend/resources/styles/main/partials/dashboard-settings.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
240
frontend/resources/styles/main/partials/dashboard-team.scss
Normal file
240
frontend/resources/styles/main/partials/dashboard-team.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
frontend/resources/styles/main/partials/dropdown.scss
Normal file
32
frontend/resources/styles/main/partials/dropdown.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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))))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
13
frontend/src/app/main/store.clj
Normal file
13
frontend/src/app/main/store.clj
Normal 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)))
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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]))
|
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")]]]])
|
||||||
|
|
94
frontend/src/app/main/ui/auth/verify_token.cljs
Normal file
94
frontend/src/app/main/ui/auth/verify_token.cljs
Normal 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]))
|
|
@ -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]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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}]]]]]))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}]]]))
|
||||||
|
|
||||||
|
|
|
@ -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])]))
|
||||||
|
|
||||||
|
|
66
frontend/src/app/main/ui/dashboard/inline_edition.cljs
Normal file
66
frontend/src/app/main/ui/dashboard/inline_edition.cljs
Normal 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]]))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}]]]))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
301
frontend/src/app/main/ui/dashboard/team.cljs
Normal file
301
frontend/src/app/main/ui/dashboard/team.cljs
Normal 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"]]]]]]]))
|
117
frontend/src/app/main/ui/dashboard/team_form.cljs
Normal file
117
frontend/src/app/main/ui/dashboard/team_form.cljs
Normal 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")}]]]]]))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -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}])]]]))
|
||||||
|
|
||||||
|
|
|
@ -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")}]]]]]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")]]]]]))
|
|
||||||
|
|
|
@ -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")]]]))
|
|
|
@ -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}]]]))
|
|
||||||
|
|
|
@ -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}]]]))
|
|
||||||
|
|
|
@ -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}]]]))
|
|
||||||
|
|
91
frontend/src/app/main/ui/settings/sidebar.cljs
Normal file
91
frontend/src/app/main/ui/settings/sidebar.cljs
Normal 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}]]])
|
|
@ -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]]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)))))
|
||||||
|
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue