🎉 Add full teams administration.

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,54 +25,8 @@
padding: 0;
hr {
margin: 10px 15px;
border-color: $color-gray-10;
}
}
.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;
}
margin: 1rem 15px;
}
}
@ -87,6 +41,10 @@
z-index: 12;
max-height: 30rem;
min-width: 189px;
li {
height: 35px;
}
}
.options-dropdown {
@ -126,19 +84,13 @@
padding: 0px 10px;
display: flex;
flex-grow: 1;
}
.team-name {
flex-grow: 1;
display: flex;
align-items: center;
.team-text {
color: $color-gray-60;
}
}
}
.team-icon {
display: flex;
align-items: center;
@ -149,6 +101,20 @@
height: 23px;
fill: $color-gray-60;
}
img {
border-radius: 50%;
flex-shrink: 0;
height: 23px;
width: 23px;
}
}
.team-text {
color: $color-gray-60;
@include text-ellipsis;
width: 100px;
}
}
.switch-icon {
@ -396,13 +362,13 @@
align-items: center;
cursor: pointer;
display: flex;
padding: $small;
padding: 10px 15px;
position: relative;
span {
@include text-ellipsis;
color: $color-black;
margin: $small;
margin: 10px 5px;
font-size: $fs12;
max-width: 135px;
}
@ -416,22 +382,14 @@
.dropdown {
left: 15px;
bottom: 50px;
z-index: 12;
max-height: 30rem;
min-width: 189px;
position: absolute;
bottom: 45px;
z-index: 12;
min-width: 189px;
width: 170px;
@include animation(0,.2s,fadeInUp);
li {
display: flex;
align-items: center;
font-size: $fs13;
font-size: $fs12;
padding: 5px 10px;
svg {

View file

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

View file

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

View file

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

View file

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

View file

@ -56,84 +56,140 @@
}
}
// NEW GEN MODALS
.modal-container {
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;
}
.modal-header-title {
display: flex;
align-items: center;
font-size: $fs24;
padding-left: 16px;
h2 {
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: $fs14;
margin-bottom: 20px;
font-size: $fs18
}
.confirmation {
.btn-primary {
margin-bottom: 30px;
}
.featured-note .icon svg {
fill: $color-success;
h3 {
margin-bottom: 15px;
}
.modal-footer .action-buttons {
justify-content: space-around;
}
}
.confirm-dialog {
background-color: $color-white;
width: 23rem;
.modal-content {
padding: 20px 40px;
p {
font-size: $fs14;
color: $color-gray-40;
}
.dialog-title {
font-size: 24px;
color: $color-black;
font-weight: normal;
text-align: center;
}
.dialog-buttons {
.action-buttons {
display: flex;
flex-direction: row;
margin-top: 3rem;
width: 100%;
font-size: $fs14;
}
.dialog-cancel-button {
.cancel-button {
border: 1px solid $color-gray-30;
background: $color-canvas;
border-radius: 2px;
padding: 0.5rem;
margin-right: 1rem;
justify-content: space-evenly;
margin-bottom: 0;
width: 100%;
border-radius: 3px;
padding: 0.5rem 1rem;
cursor: pointer;
margin-right: 8px;
&:hover {
background: $color-gray-20;
}
}
.dialog-accept-button {
width: 100%;
padding: 0.5rem;
.accept-button {
border: 1px solid $color-danger;
border-radius: 3px;
background: $color-danger;
color: $color-white;
margin-bottom: 0;
cursor: pointer;
padding: 0.5rem 1rem;
&:hover {
background: $color-danger-dark;
}
&.not-danger {
background: $color-primary;
color: $color-gray-60;
}
&.not-danger:hover {
background: $color-primary-dark;
}
}
}
.libraries-dialog {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,9 @@
[app.util.router :as rt]
[app.util.time :as dt]
[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]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
@ -29,10 +32,12 @@
(s/def ::created-at ::us/inst)
(s/def ::modified-at ::us/inst)
(s/def ::is-pinned ::us/boolean)
(s/def ::photo ::us/string)
(s/def ::team
(s/keys :req-un [::id
::name
::photo
::created-at
::modified-at]))
@ -59,6 +64,13 @@
;; --- 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
[{:keys [id] :as params}]
(letfn [(fetched [team state]
@ -66,9 +78,21 @@
(ptk/reify ::fetch-team
ptk/WatchEvent
(watch [_ state stream]
(let [profile (:profile state)]
(->> (rp/query :team params)
(rx/map #(partial fetched %)))))))
(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
@ -83,6 +107,31 @@
(->> (rp/query :projects {:team-id team-id})
(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
@ -181,6 +230,114 @@
(rx/tap on-success)
(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
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
@ -289,12 +446,12 @@
;; --- Set File shared
(defn set-file-shared
[id is-shared]
{:pre [(uuid? id) (boolean? is-shared)]}
[{:keys [id project-id is-shared] :as params}]
(us/assert ::file params)
(ptk/reify ::set-file-shared
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:files id :is-shared] is-shared))
(assoc-in state [:files project-id id :is-shared] is-shared))
ptk/WatchEvent
(watch [_ state stream]

View file

@ -8,30 +8,54 @@
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.modal
(:refer-clojure :exclude [update])
(: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 {}))
(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]
(-> state
(assoc ::modal {:id id
(assoc state ::modal {:id id
:type type
:props props
:allow-click-outside false})))))
(defn hide-modal []
(defn hide
[]
(ptk/reify ::hide-modal
ptk/UpdateEvent
(update [_ state]
(-> state
(dissoc ::modal)))))
(dissoc state ::modal))))
(defn update-modal [options]
(defn update
[options]
(ptk/reify ::update-modal
ptk/UpdateEvent
(update [_ state]
(-> state
(update ::modal merge options)))))
(c/update state ::modal merge options))))
(defn show!
[type props]
(st/emit! (show type props)))
(defn allow-click-outside!
[]
(st/emit! (update {:allow-click-outside true})))
(defn disallow-click-outside!
[]
(st/emit! (update {:allow-click-outside false})))
(defn hide!
[]
(st/emit! (hide)))

View file

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

View file

@ -98,6 +98,14 @@
(seq params))
(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
[id params]
(let [uri (str cfg/public-uri "/api/login")]

View file

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

View file

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

View file

@ -9,11 +9,6 @@
(ns app.main.ui
(: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.exceptions :as ex]
[app.common.uuid :as uuid]
@ -21,18 +16,22 @@
[app.main.data.messages :as dm]
[app.main.refs :as refs]
[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.icons :as i]
[app.main.ui.cursors :as c]
[app.main.ui.messages :as msgs]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings]
[app.main.ui.static :refer [not-found-page not-authorized-page]]
[app.main.ui.viewer :refer [viewer-page]]
[app.main.ui.render :as render]
[app.main.ui.workspace :as workspace]
[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
@ -60,12 +59,13 @@
;; Used for export
["/render-object/:file-id/:page-id/:object-id" :render-object]
["/dashboard"
["/team/:team-id"
["/dashboard/team/:team-id"
["/members" :dashboard-team-members]
["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects]
["/search" :dashboard-search]
["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]]]
["/projects/:project-id" :dashboard-files]]
["/workspace/:project-id/:file-id" :workspace]])
@ -109,7 +109,9 @@
(:dashboard-search
:dashboard-projects
:dashboard-files
:dashboard-libraries)
:dashboard-libraries
:dashboard-team-members
:dashboard-team-settings)
[:& dashboard {:route route}]
:viewer
@ -186,3 +188,9 @@
(ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened."
:type :error
:timeout 5000}))))))
;; (defonce foo
;; (do
;; (prn "attach listener")
;; (.addEventListener js/window "error" (fn [err] (ptk/handle-error (unchecked-get err "error"))))
;; 1))

View file

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

View file

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

View file

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

View file

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

View file

@ -9,16 +9,16 @@
(ns app.main.ui.auth.register
(:require
[app.common.spec :as us]
[app.config :as cfg]
[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.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.messages :as msgs]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr t]]
[app.util.router :as rt]
[app.util.timers :as tm]
@ -32,15 +32,6 @@
{:type :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
[form error]
(case (:code error)
@ -55,9 +46,14 @@
(defn- on-success
[form data]
(let [msg (tr "auth.notifications.validation-email-sent" (:email data))]
(if (and (:is-active data) (:claims data))
(let [message (tr "auth.notifications.team-invitation-accepted")]
(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 msg))))
(dm/success message)))))
(defn- validate
[data]
@ -67,47 +63,64 @@
(defn- on-submit
[form event]
(let [data (with-meta (:clean-data form)
(let [data (with-meta (:clean-data @form)
{:on-error (partial on-error 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
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::register-form
[{:keys [locale params] :as props}]
(let [initial (mf/use-memo (mf/deps params) (constantly params))
form (fm/use-form :spec ::register-form
:validators [validate]
:initial {}}
[:& input {:name :fullname
:initial initial)]
[:& fm/form {:on-submit on-submit
:form form}
[:div.fields-row
[:& fm/input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:type "text"}]]
[:div.fields-row
[:& fm/input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
: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"}]
:type "password"}]]
[:& submit-button
{:label (t locale "auth.register-submit-label")}]])
[:& fm/submit-button
{:label (t locale "auth.register-submit-label")}]]))
;; --- Register Page
(mf/defc register-page
[{:keys [locale] :as props}]
[:section.generic-form
[{:keys [locale params] :as props}]
[:div.form-container
[:h1 (t locale "auth.register-title")]
[:div.subtitle (t locale "auth.register-subtitle")]
(when cfg/demo-warning
[:& demo-warning])
[:& register-form {:locale locale}]
[:& register-form {:locale locale
:params params}]
[:div.links
[:div.link-entry
@ -120,4 +133,4 @@
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(t locale "auth.create-demo-profile")]]]]])
(t locale "auth.create-demo-profile")]]]])

View file

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

View file

@ -20,19 +20,20 @@
[app.util.dom :as dom]))
(def form-ctx (mf/create-context nil))
(def use-form fm/use-form)
(mf/defc input
[{: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)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in form [:touched name])
error (get-in form [:errors name])
touched? (get-in @form [:touched name])
error (get-in @form [:errors name])
value (get-in form [:data name] "")
value (get-in @form [:data name] "")
help-icon' (cond
(and (= type "password")
@ -67,7 +68,7 @@
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in form [:touched name])
(when-not (get-in @form [:touched name])
(swap! form assoc-in [:touched name] true)))
props (-> props
@ -80,7 +81,7 @@
:type @type')
(obj/clj->props))]
[:div.field.custom-input
[:div.custom-input
{:class klass}
[:*
[:label label]
@ -101,12 +102,12 @@
(mf/defc select
[{:keys [options label name form default]
:or {default ""}}]
(let [form (mf/use-ctx form-ctx)
value (get-in form [:data name] default)
(let [form (or form (mf/use-ctx form-ctx))
value (get-in @form [:data name] default)
cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)]
[:div.field.custom-select
[:div.custom-select
[:select {:value value
:on-change on-change}
(for [item options]
@ -122,34 +123,21 @@
(mf/defc submit-button
[{: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
{:name "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:class (when-not (:valid @form) "btn-disabled")
:disabled (not (:valid @form))
:on-click on-click
:value label
:type "submit"}]))
(mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}]
(let [frm (fm/use-form :spec spec
:validators validators
: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}
[{:keys [on-submit form children class] :as props}]
(let [on-submit (or on-submit (constantly nil))]
[:& (mf/provider form-ctx) {:value form}
[:form {:class class
:on-submit (fn [event]
(dom/prevent-default event)
(on-submit frm event))}
(on-submit form event))}
children]]))

View file

@ -2,53 +2,69 @@
;; 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) 2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; 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.confirm
(:require
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.icons :as i]
[rumext.alpha :as mf]
[app.main.ui.modal :as modal]
[app.util.i18n :refer (tr)]
[app.util.data :refer [classnames]]
[app.util.dom :as dom]))
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr t]]
[rumext.alpha :as mf]))
(mf/defc confirm-dialog
{::mf/register modal/components
::mf/register-as :confirm-dialog}
[{:keys [message on-accept on-cancel hint cancel-text accept-text not-danger?] :as ctx}]
(let [message (or message (tr "ds.confirm-title"))
cancel-text (or cancel-text (tr "ds.confirm-cancel"))
accept-text (or accept-text (tr "ds.confirm-ok"))
::mf/register-as :confirm}
[{:keys [message title on-accept on-cancel hint cancel-label accept-label] :as props}]
(let [locale (mf/deref i18n/locale)
accept
on-accept (or on-accept identity)
on-cancel (or on-cancel identity)
message (or message (t locale "ds.confirm-title"))
cancel-label (or cancel-label (tr "ds.confirm-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)
(modal/hide!)
(on-accept (dissoc ctx :on-accept :on-cancel)))
(st/emit! (modal/hide))
(on-accept props)))
cancel
cancel-fn
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(modal/hide!)
(when on-cancel
(on-cancel (dissoc ctx :on-accept :on-cancel))))]
(st/emit! (modal/hide))
(on-cancel props)))]
[:div.modal-overlay
[:div.modal.confirm-dialog
[:a.close {:on-click cancel} i/close]
[:div.modal-container.confirm-dialog
[:div.modal-header
[:div.modal-header-title
[:h2 title]]
[:div.modal-close-button
{:on-click cancel-fn} i/close]]
[:div.modal-content
[:h3.dialog-title message]
(if hint [:span hint])
[:div.dialog-buttons
[:input.dialog-cancel-button
{:type "button"
:value cancel-text
:on-click cancel}]
[:h3 message]
(when (string? hint)
[:p hint])]
[:input.dialog-accept-button
[:div.modal-footer
[:div.action-buttons
[:input.cancel-button
{:type "button"
:class (classnames :not-danger not-danger?)
:value accept-text
:on-click accept}]]]]]))
:value cancel-label
:on-click cancel-fn}]
[:input.accept-button
{:type "button"
:value accept-label
:on-click accept-fn}]]]]]))

View file

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

View file

@ -10,12 +10,13 @@
(ns app.main.ui.dashboard.files
(:require
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[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.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt]
@ -39,21 +40,6 @@
on-edit
(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
(mf/use-callback
@ -65,7 +51,12 @@
on-delete
(mf/use-callback
(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
(mf/use-callback
@ -77,26 +68,21 @@
[:header.dashboard-header
(if (:is-default project)
[:h1.dashboard-title (t locale "dashboard.header.draft")]
[:*
[:h1.dashboard-title (t locale "dashboard.header.project" (:name project))]
[:div.dashboard-title
[:h1 (t locale "dashboard.header.draft")]]
(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]]}]
(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"]]]
[(t locale "dashboard.grid.delete") on-delete]]}]]))
[:a.btn-secondary.btn-small {:on-click on-create-clicked}
(t locale "dashboard.new-file")]]))
@ -119,7 +105,7 @@
[:*
[:& header {:team team :project project}]
[:section.dashboard-grid-container
[:section.dashboard-container
[:& grid {:id (:id project)
:files files}]]]))

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.main.constants :as c]
[app.config :as cfg]
[app.main.data.auth :as da]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
@ -19,10 +19,12 @@
[app.main.repo :as rp]
[app.main.store :as st]
[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.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.main.data.modal :as modal]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.object :as obj]
@ -35,55 +37,6 @@
[okulary.core :as l]
[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
[{:keys [item selected?] :as props}]
(let [dstate (mf/deref refs/dashboard-local)
@ -97,22 +50,28 @@
(fn []
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id item)
:project-id (:id item)}))))
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
:on-double-click on-dbl-click
:class (when selected? "current")}
(if @edition?
[:& sidebar-project-edition {:item item
:on-end #(reset! edition? false)}]
[:& inline-edition {:content (:name item)
:on-end on-edit}]
[:span.element-title (:name item)])]))
(mf/defc sidebar-search
[{:keys [search-term team-id locale] :as props}]
(let [search-term (or search-term "")
emit! (mf/use-memo #(f/debounce st/emit! 500))
on-search-focus
@ -158,36 +117,223 @@
{:on-click on-clear-click}
i/close]]))
(mf/defc sidebar-team-switch
[{:keys [team profile] :as props}]
(mf/defc teams-selector-dropdown
[{: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)
teams (mf/use-state [])
on-nav
on-create-clicked
(mf/use-callback
(st/emitf (modal/show :team-form {})))
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 %})))
on-create-clicked
(mf/use-callback #(modal/show! :team-form {}))]
(mf/use-callback
(st/emitf (modal/show :team-form {})))
(mf/use-effect
(mf/deps (:id teams))
on-rename-clicked
(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 []
(->> (rp/query! :teams)
(rx/subs #(reset! teams %)))))
(->> (rp/query! :team-members {:team-id (:id team)})
(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.switch-content
[:div.current-team
(if (:is-default team)
[:div.team-name
[:span.team-icon i/logo-icon]
(if (:is-default team)
[:span.team-text "Your penpot"]
[:span.team-text (:name team)])]
[:span.team-text (t locale "dashboard.sidebar.default-team-name")]]
[: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)}
i/arrow-down]]
(when-not (:is-default team)
[:div.switch-options {:on-click #(reset! show-team-opts-ddwn? true)}
i/actions])]
@ -195,86 +341,15 @@
;; Teams Dropdown
[:& dropdown {:show @show-teams-ddwn?
:on-close #(reset! show-teams-ddwn? false)}
[:ul.dropdown.teams-dropdown
[:li.title "Switch Team"]
[:hr]
[: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"]]]
[:& teams-selector-dropdown {:team team
:profile profile
:locale locale}]]
[:& dropdown {:show @show-team-opts-ddwn?
:on-close #(reset! show-team-opts-ddwn? false)}
[:ul.dropdown.options-dropdown
[:li "Members"]
[:li "Settings"]
[: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"}]]]]]]))
[:& team-options-dropdown {:team team
:profile profile
:locale locale}]]]))
(mf/defc sidebar-content
[{:keys [locale projects profile section team project search-term] :as props}]
@ -283,15 +358,27 @@
(d/seek :is-default)
(:id))
team-id (:id team)
projects? (= section :dashboard-projects)
libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
go-projects #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}))
go-default #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id}))
go-libs #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}))
go-projects
(mf/use-callback
(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
(->> (vals projects)
@ -299,8 +386,7 @@
(filter :is-pinned))]
[:div.sidebar-content
[:& sidebar-team-switch {:team team :profile profile}]
[:& sidebar-team-switch {:team team :profile profile :locale locale}]
[:hr]
[:& sidebar-search {:search-term search-term
:team-id (:id team)
@ -313,7 +399,7 @@
i/recent
[:span.element-title (t locale "dashboard.sidebar.projects")]]
[:li {:on-click go-default
[:li {:on-click go-drafts
:class-name (when drafts? "current")}
i/file-html
[:span.element-title (t locale "dashboard.sidebar.drafts")]]
@ -337,7 +423,7 @@
:selected? (= (:id item) (:id project))}])]
[:div.sidebar-empty-placeholder
[:span.icon i/pin]
[:span.text "Pinned projects will appear here"]])]]))
[:span.text (t locale "dashboard.sidebar.no-projects-placeholder")]])]]))
(mf/defc profile-section
@ -365,30 +451,25 @@
[:ul.dropdown
[:li {:on-click (partial on-click :settings-profile)}
[:span.icon i/user]
[:span.text (t locale "dashboard.header.profile-menu.profile")]]
[:span.text (t locale "dashboard.sidebar.profile")]]
[:hr]
[:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock]
[:span.text (t locale "dashboard.header.profile-menu.password")]]
[:span.text (t locale "dashboard.sidebar.password")]]
[:hr]
[:li {:on-click (partial on-click da/logout)}
[:span.icon i/exit]
[:span.text (t locale "dashboard.header.profile-menu.logout")]]]]]))
[:span.text (t locale "dashboard.logout")]]]]]))
(mf/defc sidebar
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)
profile (obj/get props "profile")
props (-> (obj/clone props)
(obj/set! "locale" locale)
(obj/set! "profile" profile))]
(obj/set! "locale" locale))]
[:div.dashboard-sidebar
[:div.sidebar-inside
[:> sidebar-content props]
[:& profile-section {:profile profile
:locale locale}]]]))
[:& profile-section {:profile profile :locale locale}]]]))

View file

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

View file

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

View file

@ -14,42 +14,22 @@
[rumext.alpha :as mf]
[app.main.store :as st]
[app.main.ui.keyboard :as k]
[app.main.data.modal :as dm]
[app.util.dom :as dom]
[app.main.refs :as refs]
[potok.core :as ptk]
[app.main.data.modal :as mdm])
[app.main.refs :as refs])
(: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
[event]
(when (k/esc? event)
(hide!)
(st/emit! (dm/hide))
(dom/stop-propagation event)))
(defn- on-pop-state
[event]
(dom/prevent-default event)
(dom/stop-propagation event)
(hide!)
(st/emit! (dm/hide))
(.forward js/history))
(defn- on-parent-clicked
@ -60,7 +40,7 @@
(= (.-className ^js current) "modal-overlay"))
(dom/stop-propagation event)
(dom/prevent-default event)
(hide!))))
(st/emit! (dm/hide)))))
(defn- on-click-outside
[event wrapper-ref allow-click-outside]
@ -70,7 +50,7 @@
(when (and wrapper (not allow-click-outside) (not (.contains wrapper current)))
(dom/stop-propagation event)
(dom/prevent-default event)
(hide!))))
(st/emit! (dm/hide)))))
(mf/defc modal-wrapper
{::mf/wrap-props false
@ -78,6 +58,7 @@
[props]
(let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil)
handle-click-outside
(fn [event]
(on-click-outside event wrapper-ref (:allow-click-outside data)))]
@ -89,14 +70,15 @@
(events/listen js/document EventType.CLICK handle-click-outside)]]
#(for [key keys]
(events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref}
(mf/element
(get @components (:type data))
(get @dm/components (:type data))
(:props data))]))
(def modal-ref
(l/derived ::mdm/modal st/state))
(l/derived ::dm/modal st/state))
(mf/defc modal
[]

View file

@ -9,30 +9,46 @@
(ns app.main.ui.settings
(: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.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.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
[{:keys [route] :as props}]
(let [section (get-in route [:data :name])
profile (mf/deref refs/profile)]
[:main.settings-main
[:div.settings-content
[:& header {:section section :profile profile}]
profile (mf/deref refs/profile)
locale (mf/deref i18n/locale)]
[:section.dashboard-layout
[:& sidebar {:profile profile
:locale locale
:section section}]
[:div.dashboard-content
[:& header {:locale locale}]
[:section.dashboard-container
(case section
:settings-profile (mf/element profile-page)
:settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))
:settings-profile
[:& profile-page {:locale locale}]
:settings-password
[:& password-page {:locale locale}]
:settings-options
[:& options-page {:locale locale}])]]]))

View file

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

View file

@ -10,37 +10,61 @@
(ns app.main.ui.settings.delete-account
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[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]]))
(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/register modal/components
::mf/register-as :delete-account}
[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
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 (t locale "settings.delete-account-title")]
[:div.modal-container.change-email-modal
[:div.modal-header
[:div.modal-header-title
[:h2 (t locale "dashboard.settings.delete-account-title")]]
[:div.modal-close-button
{:on-click on-close} i/close]]
[:div.modal-content
[:& msgs/inline-banner
{:type :warning
:content (t locale "settings.delete-account-info")}]
:content (t locale "dashboard.settings.delete-account-info")}]]
[:div.modal-footer
[:div.action-buttons
[:button.btn-warning.btn-large {:on-click on-accept}
(t locale "dashboard.settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large {:on-click on-close}
(t locale "dashboard.settings.cancel-and-keep-my-account")]]]]]))
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]]))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,9 +20,11 @@
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[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.header :refer [header]]
[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.scroll :as scroll]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]

View file

@ -19,7 +19,7 @@
[app.common.uuid :refer [uuid]]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.colors :as dwc]
[app.main.ui.modal :as modal]
[app.main.data.modal :as modal]
[okulary.core :as l]
[app.main.refs :as refs]
[app.util.i18n :as i18n :refer [t]]))

View file

@ -18,7 +18,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[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.keyboard :as kbd]
[app.util.i18n :as i18n :refer [t]]

View file

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

View file

@ -30,7 +30,7 @@
[app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]]
[app.main.ui.icons :as i]
[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.util.data :refer [matches-search]]
[app.util.dom :as dom]

View file

@ -14,7 +14,7 @@
[app.util.dom :as dom]
[app.util.data :refer [classnames]]
[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.main.refs :as refs]))

View file

@ -16,7 +16,7 @@
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[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.i18n :as i18n :refer [t]]
[app.util.router :as rt]

View file

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

View file

@ -10,34 +10,20 @@
(ns app.util.forms
(:refer-clojure :exclude [uuid])
(: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]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]
[rumext.alpha :as mf]
[app.common.spec :as us]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]))
[rumext.alpha :as mf]))
;; --- 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
[acc {:keys [path pred val via in] :as problem}]
;; (prn "interpret-problem" problem)
(cond
(and (empty? path)
(list? pred)
@ -51,12 +37,31 @@
:else acc))
(declare create-form-mutator)
(defn use-form
[& {:keys [spec validators initial]}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
[& {:keys [spec validators initial] :as opts}]
(let [state (mf/useState 0)
render (aget state 1)
state-ref (mf/use-ref {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
form (mf/use-memo #(create-form-mutator state-ref render opts))]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! form update :data merge (initial))
(swap! form update :data merge initial))))
form))
(defn- wrap-update-fn
[f {:keys [spec validators]}]
(fn [& args]
(let [state (apply f args)
cleaned (s/conform spec (:data state))
problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state))))
@ -66,18 +71,53 @@
(merge errors (vf (:data state))))
{} validators)
(:errors state))]
(-> (assoc state
(assoc state
:errors errors
:clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
(not= cleaned ::s/invalid)))
(impl-mutator update-state))))
(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
([{:keys [data] :as form} field]
([form field]
(on-input-change form field false))
([{:keys [data] :as form} field trim?]
([form field trim?]
(fn [event]
(let [target (dom/get-target event)
value (dom/get-value target)]
@ -87,9 +127,10 @@
(update :errors dissoc field))))))))
(defn on-input-blur
[{:keys [touched] :as form} field]
[form field]
(fn [event]
(let [target (dom/get-target event)]
(let [target (dom/get-target event)
touched (get @form :touched)]
(when-not (get touched field)
(swap! form assoc-in [:touched field] true)))))

View file

@ -16,9 +16,10 @@
[app.util.transit :as t]))
(defn- conditional-decode
[{:keys [body headers] :as response}]
[{:keys [body headers status] :as response}]
(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))
response)))

View file

@ -5,34 +5,43 @@
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
(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?
[]
(or (= *target* "nodejs")
(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
[alias value]
[alias storage value]
(when-not (is-worker?)
(let [key (name alias)
value (t/encode value)]
(.setItem js/localStorage key value))))
(tm/schedule-on-idle
(fn [] ((:set storage) alias value)))))
(defn- load
[alias]
[alias storage]
(when-not (is-worker?)
(let [data (.getItem js/localStorage (name alias))]
(try
(t/decode data)
(catch :default e
(js/console.error "Error on loading data from local storage." e)
nil)))))
((:get storage) alias)))
(defn- make-storage
[alias]
(let [data (atom (load alias))]
(add-watch data :sub #(persist alias %4))
[alias storage]
(let [data (atom (load alias storage))]
(add-watch data :sub #(persist alias storage %4))
(reify
Object
(toString [_]
@ -66,5 +75,9 @@
(-lookup [_ key not-found]
(get @data key not-found)))))
(def storage
(make-storage "app"))
(defonce storage
(make-storage "app" local))
(defonce cache
(make-storage "cache" session))

View file

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