diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 085835c08a..f0fae97de4 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -1,6 +1,6 @@ { "auth.already-have-account" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:114" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:127" ], "translations" : { "en" : "Already have an account?", "fr" : "Vous avez déjà un compte?", @@ -9,7 +9,7 @@ } }, "auth.confirm-password-label" : { - "used-in" : [ "src/app/main/ui/auth/recovery.cljs:76" ], + "used-in" : [ "src/app/main/ui/auth/recovery.cljs:77" ], "translations" : { "en" : "Confirm password", "fr" : "Confirmez mot de passe", @@ -18,7 +18,7 @@ } }, "auth.create-demo-profile" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:135", "src/app/main/ui/auth/register.cljs:123" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:136", "src/app/main/ui/auth/login.cljs:147" ], "translations" : { "en" : "Create demo account", "fr" : "Créer un compte de démonstration", @@ -27,7 +27,7 @@ } }, "auth.create-demo-profile-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:132", "src/app/main/ui/auth/register.cljs:120" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:133", "src/app/main/ui/auth/login.cljs:144" ], "translations" : { "en" : "Just wanna try it?", "fr" : "Vous voulez juste essayer?", @@ -45,7 +45,7 @@ } }, "auth.email-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:81", "src/app/main/ui/auth/register.cljs:89", "src/app/main/ui/auth/recovery_request.cljs:45" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:101", "src/app/main/ui/auth/recovery_request.cljs:47", "src/app/main/ui/auth/login.cljs:92" ], "translations" : { "en" : "Email", "fr" : "Adresse email", @@ -54,7 +54,7 @@ } }, "auth.forgot-password" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:110" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:122" ], "translations" : { "en" : "Forgot your password?", "fr" : "Mot de passe oublié?", @@ -63,7 +63,7 @@ } }, "auth.fullname-label" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:83" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:94" ], "translations" : { "en" : "Full Name", "fr" : "Nom complet", @@ -72,7 +72,7 @@ } }, "auth.go-back-to-login" : { - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:66" ], + "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:68" ], "translations" : { "en" : "Go back!", "fr" : "Retour!", @@ -81,7 +81,7 @@ } }, "auth.goodbye-title" : { - "used-in" : [ "src/app/main/ui/auth.cljs:33" ], + "used-in" : [ "src/app/main/ui/auth.cljs:35" ], "translations" : { "en" : "Goodbye!", "fr" : "Au revoir!", @@ -90,7 +90,7 @@ } }, "auth.login-here" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:117" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:130" ], "translations" : { "en" : "Login here", "fr" : "Se connecter ici", @@ -99,7 +99,7 @@ } }, "auth.login-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:89" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:101" ], "translations" : { "en" : "Sign in", "fr" : "Se connecter", @@ -108,7 +108,7 @@ } }, "auth.login-subtitle" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:102" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:114" ], "translations" : { "en" : "Enter your details below", "fr" : "Entrez vos informations ci-dessous", @@ -117,7 +117,7 @@ } }, "auth.login-title" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:101" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:113" ], "translations" : { "en" : "Great to see you again!", "fr" : "Ravi de vous revoir!", @@ -126,7 +126,7 @@ } }, "auth.login-with-gitlab-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:128" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:140" ], "translations" : { "en" : "Login with Gitlab", "fr" : "Se connecter via Gitlab", @@ -135,7 +135,7 @@ } }, "auth.login-with-ldap-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:93" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:105" ], "translations" : { "en" : "Sign in with LDAP", "fr" : "Se connecter via LDAP", @@ -153,7 +153,7 @@ } }, "auth.notifications.invalid-token-error" : { - "used-in" : [ "src/app/main/ui/auth/recovery.cljs:48" ], + "used-in" : [ "src/app/main/ui/auth/recovery.cljs:47" ], "translations" : { "en" : "The recovery token is invalid.", "fr" : "Le code de récupération n'est pas valide.", @@ -162,7 +162,7 @@ } }, "auth.notifications.password-changed-succesfully" : { - "used-in" : [ "src/app/main/ui/auth/recovery.cljs:52" ], + "used-in" : [ "src/app/main/ui/auth/recovery.cljs:51" ], "translations" : { "en" : "Password successfully changed", "fr" : "Mot de passe changé avec succès", @@ -171,7 +171,7 @@ } }, "auth.notifications.recovery-token-sent" : { - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:32" ], + "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:30" ], "translations" : { "en" : "Password recovery link sent to your inbox.", "fr" : "Lien de récupération de mot de passe envoyé.", @@ -179,14 +179,20 @@ "es" : "Hemos enviado a tu buzón un enlace para recuperar tu contraseña." } }, + "auth.notifications.team-invitation-accepted" : { + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:55", "src/app/main/ui/auth/register.cljs:50" ], + "translations" : { + "en" : "Joined the team succesfully" + } + }, "auth.notifications.validation-email-sent" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:55", "src/app/main/ui/auth/register.cljs:58" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:54", "src/app/main/ui/settings/change_email.cljs:56" ], "translations" : { "en" : "Verification email sent to %s; check your email!" } }, "auth.password-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:87", "src/app/main/ui/auth/register.cljs:93" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:106", "src/app/main/ui/auth/login.cljs:99" ], "translations" : { "en" : "Password", "fr" : "Mot de passe", @@ -195,7 +201,7 @@ } }, "auth.password-length-hint" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:92" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:105" ], "translations" : { "en" : "At least 8 characters", "fr" : "Au moins 8 caractères", @@ -204,7 +210,7 @@ } }, "auth.recovery-request-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:50" ], + "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:52" ], "translations" : { "en" : "Recover Password", "fr" : "Récupérer le mot de passe", @@ -213,7 +219,7 @@ } }, "auth.recovery-request-subtitle" : { - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:59" ], + "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:62" ], "translations" : { "en" : "We'll send you an email with instructions", "fr" : "Nous vous enverrons un e-mail avec des instructions", @@ -222,7 +228,7 @@ } }, "auth.recovery-request-title" : { - "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:58" ], + "used-in" : [ "src/app/main/ui/auth/recovery_request.cljs:61" ], "translations" : { "en" : "Forgot your password?", "fr" : "Vous avez oublié votre mot de passe?", @@ -231,7 +237,7 @@ } }, "auth.recovery-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/recovery.cljs:79" ], + "used-in" : [ "src/app/main/ui/auth/recovery.cljs:80" ], "translations" : { "en" : "Change your password", "fr" : "Changez votre mot de passe", @@ -240,7 +246,7 @@ } }, "auth.register" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:116" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:128" ], "translations" : { "en" : "Sign up here", "fr" : "Inscrivez-vous ici", @@ -249,7 +255,7 @@ } }, "auth.register-label" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:113" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:125" ], "translations" : { "en" : "No account yet?", "fr" : "Pas encore de compte?", @@ -258,7 +264,7 @@ } }, "auth.register-submit-label" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:97" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:110" ], "translations" : { "en" : "Create an account", "fr" : "Créer un compte", @@ -267,7 +273,7 @@ } }, "auth.register-subtitle" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:106" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:118" ], "translations" : { "en" : "It's free, it's Open Source", "fr" : "C'est gratuit, c'est Open Source", @@ -276,7 +282,7 @@ } }, "auth.register-title" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:105" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:117" ], "translations" : { "en" : "Create an account", "fr" : "Créer un compte", @@ -285,7 +291,7 @@ } }, "auth.sidebar-tagline" : { - "used-in" : [ "src/app/main/ui/auth.cljs:42" ], + "used-in" : [ "src/app/main/ui/auth.cljs:46" ], "translations" : { "en" : "The open-source solution for design and prototyping.", "fr" : "La solution Open Source pour la conception et le prototypage.", @@ -294,7 +300,7 @@ } }, "dashboard.grid.add-shared" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:210", "src/app/main/ui/dashboard/grid.cljs:167" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:210", "src/app/main/ui/dashboard/grid.cljs:174" ], "translations" : { "en" : "Add as Shared Library", "fr" : "", @@ -303,7 +309,7 @@ } }, "dashboard.grid.add-shared-accept" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:104", "src/app/main/ui/dashboard/grid.cljs:95" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:104", "src/app/main/ui/dashboard/grid.cljs:113" ], "translations" : { "en" : "Add as Shared Library", "fr" : "", @@ -312,7 +318,7 @@ } }, "dashboard.grid.add-shared-hint" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:103", "src/app/main/ui/dashboard/grid.cljs:94" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:103", "src/app/main/ui/dashboard/grid.cljs:112" ], "translations" : { "en" : "Once added as Shared Library, the assets of this file library will be available to be used among the rest of your files.", "fr" : "", @@ -321,7 +327,7 @@ } }, "dashboard.grid.add-shared-message" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:102", "src/app/main/ui/dashboard/grid.cljs:93" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:102", "src/app/main/ui/dashboard/grid.cljs:110" ], "translations" : { "en" : "Add “%s” as Shared Library", "fr" : "", @@ -330,7 +336,7 @@ } }, "dashboard.grid.delete" : { - "used-in" : [ "src/app/main/ui/dashboard/files.cljs:87", "src/app/main/ui/dashboard/grid.cljs:164" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:171", "src/app/main/ui/dashboard/files.cljs:85" ], "translations" : { "en" : "Delete", "fr" : "Supprimer", @@ -339,7 +345,7 @@ } }, "dashboard.grid.empty-files" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:174" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:181" ], "translations" : { "en" : "You still have no files here", "fr" : "Vous n'avez encore aucun fichier ici", @@ -348,7 +354,7 @@ } }, "dashboard.grid.remove-shared" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:208", "src/app/main/ui/dashboard/grid.cljs:166" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:208", "src/app/main/ui/dashboard/grid.cljs:173" ], "translations" : { "en" : "Remove as Shared Library", "fr" : "", @@ -357,7 +363,7 @@ } }, "dashboard.grid.remove-shared-accept" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:113", "src/app/main/ui/dashboard/grid.cljs:114" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:113", "src/app/main/ui/dashboard/grid.cljs:126" ], "translations" : { "en" : "Remove as Shared Library", "fr" : "", @@ -366,7 +372,7 @@ } }, "dashboard.grid.remove-shared-hint" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:112", "src/app/main/ui/dashboard/grid.cljs:113" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:112", "src/app/main/ui/dashboard/grid.cljs:125" ], "translations" : { "en" : "Once removed as Shared Library, the File Library of this file will stop being available to be used among the rest of your files.", "fr" : "", @@ -375,7 +381,7 @@ } }, "dashboard.grid.remove-shared-message" : { - "used-in" : [ "src/app/main/ui/workspace/header.cljs:111", "src/app/main/ui/dashboard/grid.cljs:112" ], + "used-in" : [ "src/app/main/ui/workspace/header.cljs:111", "src/app/main/ui/dashboard/grid.cljs:124" ], "translations" : { "en" : "Remove “%s” as Shared Library", "fr" : "", @@ -384,7 +390,7 @@ } }, "dashboard.grid.rename" : { - "used-in" : [ "src/app/main/ui/dashboard/files.cljs:86", "src/app/main/ui/dashboard/grid.cljs:163" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:170", "src/app/main/ui/dashboard/files.cljs:84" ], "translations" : { "en" : "Rename", "fr" : "Renommer", @@ -392,8 +398,14 @@ "es" : "Renombrar" } }, + "dashboard.grid.show-all-files" : { + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:243" ], + "translations" : { + "en" : "Show all files" + } + }, "dashboard.header.draft" : { - "used-in" : [ "src/app/main/ui/dashboard/files.cljs:80" ], + "used-in" : [ "src/app/main/ui/dashboard/files.cljs:72" ], "translations" : { "en" : "Draft", "fr" : "Brouillon", @@ -401,6 +413,12 @@ "es" : "Borrador" } }, + "dashboard.header.invite-profile" : { + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:69" ], + "translations" : { + "en" : "Invite to team" + } + }, "dashboard.header.libraries" : { "used-in" : [ "src/app/main/ui/dashboard/libraries.cljs:39" ], "translations" : { @@ -411,7 +429,7 @@ } }, "dashboard.header.new-project" : { - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs:36" ], + "used-in" : [ "src/app/main/ui/dashboard/projects.cljs:35" ], "translations" : { "en" : "+ New project", "fr" : "+ Nouveau projet", @@ -419,40 +437,19 @@ "es" : "+ Nuevo proyecto" } }, - "dashboard.header.profile-menu.logout" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:376" ], - "translations" : { - "en" : "Exit", - "fr" : "Quitter", - "ru" : "Выход", - "es" : "Salir" - } - }, - "dashboard.header.profile-menu.password" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:372" ], - "translations" : { - "en" : "Password", - "fr" : "Mot de passe", - "ru" : "Пароль", - "es" : "Contraseña" - } - }, - "dashboard.header.profile-menu.profile" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:368" ], - "translations" : { - "en" : "Profile", - "fr" : "Profil", - "ru" : "Профиль", - "es" : "Perfil" - } - }, "dashboard.header.project" : { - "used-in" : [ "src/app/main/ui/dashboard/files.cljs:82" ], "translations" : { "en" : "Project %s", "fr" : "Projet %s", "ru" : "Проект %s", "es" : "Proyecto %s" + }, + "unused" : true + }, + "dashboard.header.your-account" : { + "used-in" : [ "src/app/main/ui/settings.cljs:28" ], + "translations" : { + "en" : "Your account" } }, "dashboard.library.add-item.icons" : { @@ -536,8 +533,17 @@ }, "unused" : true }, + "dashboard.logout" : { + "used-in" : [ "src/app/main/ui/settings.cljs:30", "src/app/main/ui/dashboard/sidebar.cljs:462" ], + "translations" : { + "en" : "Logout", + "fr" : "Quitter", + "ru" : "Выход", + "es" : "Salir" + } + }, "dashboard.new-file" : { - "used-in" : [ "src/app/main/ui/dashboard/projects.cljs:110", "src/app/main/ui/dashboard/files.cljs:101" ], + "used-in" : [ "src/app/main/ui/dashboard/projects.cljs:107", "src/app/main/ui/dashboard/files.cljs:87" ], "translations" : { "en" : "+ New File", "fr" : "+ Nouveau fichier", @@ -545,6 +551,51 @@ "es" : "+ Nuevo Archivo" } }, + "dashboard.notifications.email-changed-successfully" : { + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:42" ], + "translations" : { + "en" : "Your email address has been updated successfully", + "fr" : "Votre adresse e-mail a été mise à jour avec succès", + "ru" : "Ваш email адрес успешно обновлен", + "es" : "Tu dirección de correo ha sido actualizada" + } + }, + "dashboard.notifications.email-verified-successfully" : { + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:36" ], + "translations" : { + "en" : "Your email address has been verified successfully", + "fr" : "Votre adresse e-mail a été vérifiée avec succès", + "ru" : "Ваш email адрес успешно подтвержден", + "es" : "Tu dirección de correo ha sido verificada" + } + }, + "dashboard.notifications.password-saved" : { + "used-in" : [ "src/app/main/ui/settings/password.cljs:36" ], + "translations" : { + "en" : "Password saved successfully!", + "fr" : "Mot de passe enregistré avec succès!", + "ru" : "Пароль успешно сохранен!", + "es" : "¡Contraseña guardada!" + } + }, + "dashboard.notifications.profile-deletion-not-allowed" : { + "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:28" ], + "translations" : { + "en" : "You can't delete you profile. Reasign your teams before proceed.", + "fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.", + "ru" : "Вы не можете удалить профиль. Сначала смените команду.", + "es" : "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir." + } + }, + "dashboard.notifications.profile-saved" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:36", "src/app/main/ui/settings/options.cljs:36" ], + "translations" : { + "en" : "Profile saved successfully!", + "fr" : "Profil enregistré avec succès!", + "ru" : "Профиль успешно сохранен!", + "es" : "Perfil guardado correctamente!" + } + }, "dashboard.search.no-matches-for" : { "used-in" : [ "src/app/main/ui/dashboard/search.cljs:50" ], "translations" : { @@ -572,8 +623,251 @@ "es" : "Escribe algo para buscar" } }, + "dashboard.settings.cancel-and-keep-my-account" : { + "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:69" ], + "translations" : { + "en" : "Cancel and keep my account", + "fr" : "Annuler et conserver mon compte", + "ru" : "Отменить и сохранить мой аккаунт", + "es" : "Cancelar y mantener mi cuenta" + } + }, + "dashboard.settings.cancel-email-change" : { + "translations" : { + "en" : "Cancel", + "fr" : "Annuler", + "ru" : "Отмена", + "es" : "Cancelar" + }, + "unused" : true + }, + "dashboard.settings.change-email-info" : { + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:93" ], + "translations" : { + "en" : "We'll send you an email to your current email “%s” to verify your identity.", + "fr" : "Nous vous enverrons un e-mail à votre adresse actuelle “%s” pour vérifier votre identité.", + "ru" : "Мы отправим письмо для подтверждения подлиности на текущий email адрес “%s”.", + "es" : "Enviaremos un mensaje a tu correo actual “%s” para verificar tu identidad." + } + }, + "dashboard.settings.change-email-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:77" ], + "translations" : { + "en" : "Change email", + "fr" : "Changer adresse e-mail", + "ru" : "Сменить email адрес", + "es" : "Cambiar correo" + } + }, + "dashboard.settings.change-email-submit-label" : { + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:109" ], + "translations" : { + "en" : "Change email", + "fr" : "Changer adresse e-mail", + "ru" : "Сменить email адрес", + "es" : "Cambiar correo" + } + }, + "dashboard.settings.change-email-title" : { + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:86" ], + "translations" : { + "en" : "Change your email", + "fr" : "Changer adresse e-mail", + "ru" : "Сменить email адрес", + "es" : "Cambiar tu correo" + } + }, + "dashboard.settings.confirm-email-label" : { + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:103" ], + "translations" : { + "en" : "Verify new email", + "fr" : "Vérifier la nouvelle adresse e-mail", + "ru" : "Подтвердить новый email адрес", + "es" : "Verificar el nuevo correo" + } + }, + "dashboard.settings.confirm-password-label" : { + "used-in" : [ "src/app/main/ui/settings/password.cljs:93" ], + "translations" : { + "en" : "Confirm password", + "fr" : "Confirmez mot de passe", + "ru" : "Подтвердите пароль", + "es" : "Confirmar contraseña" + } + }, + "dashboard.settings.delete-account-info" : { + "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:62" ], + "translations" : { + "en" : "By removing your account you’ll lose all your current projects and archives.", + "fr" : "En supprimant votre compte, vous perdrez tous vos projets et archives actuels.", + "ru" : "Удалив аккаунт Вы потеряете все прокты и архивы.", + "es" : "Si borras tu cuenta perderás todos tus proyectos y archivos." + } + }, + "dashboard.settings.delete-account-title" : { + "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:55" ], + "translations" : { + "en" : "Are you sure you want to delete your account?", + "fr" : "Voulez-vous vraiment supprimer votre compte?", + "ru" : "Вы уверены, что хотите удалить аккаунт?", + "es" : "¿Seguro que quieres borrar tu cuenta?" + } + }, + "dashboard.settings.email-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:72" ], + "translations" : { + "en" : "Email", + "fr" : "E-mail", + "ru" : "Email", + "es" : "Correo" + } + }, + "dashboard.settings.fullname-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:64" ], + "translations" : { + "en" : "Your name", + "fr" : "Votre nom complet", + "ru" : "Ваше имя", + "es" : "Tu nombre" + } + }, + "dashboard.settings.language-change-title" : { + "used-in" : [ "src/app/main/ui/settings/options.cljs:54" ], + "translations" : { + "en" : "Language", + "fr" : "Langue", + "ru" : "Язык", + "es" : "Idioma" + } + }, + "dashboard.settings.language-label" : { + "used-in" : [ "src/app/main/ui/settings/options.cljs:61" ], + "translations" : { + "en" : "Select UI language", + "fr" : "Sélectionner la langue de l'interface", + "ru" : "Выберите язык интерфейса", + "es" : "Cambiar el idioma de la interfaz" + } + }, + "dashboard.settings.new-email-label" : { + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:98" ], + "translations" : { + "en" : "New email", + "fr" : "Nouvel e-mail", + "ru" : "Новый email адрес", + "es" : "Nuevo correo" + } + }, + "dashboard.settings.new-password-label" : { + "used-in" : [ "src/app/main/ui/settings/password.cljs:87" ], + "translations" : { + "en" : "New password", + "fr" : "Nouveau mot de passe", + "ru" : "Новый пароль", + "es" : "Nueva contraseña" + } + }, + "dashboard.settings.old-password-label" : { + "used-in" : [ "src/app/main/ui/settings/password.cljs:81" ], + "translations" : { + "en" : "Old password", + "fr" : "Ancien mot de passe", + "ru" : "Старый пароль", + "es" : "Contraseña anterior" + } + }, + "dashboard.settings.password" : { + "translations" : { + "en" : "PASSWORD", + "fr" : "MOT DE PASSE", + "ru" : "ПАРОЛЬ", + "es" : "CONTRASEÑA" + }, + "unused" : true + }, + "dashboard.settings.password-change-title" : { + "used-in" : [ "src/app/main/ui/settings/password.cljs:76" ], + "translations" : { + "en" : "Change password", + "fr" : "Changement de mot de passe", + "ru" : "Изменить пароль", + "es" : "Cambiar contraseña" + } + }, + "dashboard.settings.profile-submit-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:80", "src/app/main/ui/settings/password.cljs:96", "src/app/main/ui/settings/options.cljs:72" ], + "translations" : { + "en" : "Update settings", + "fr" : "Mettre à jour les paramètres", + "ru" : "Обновить настройки", + "es" : "Actualizar opciones" + } + }, + "dashboard.settings.remove-account-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:85" ], + "translations" : { + "en" : "Want to remove your account?", + "fr" : "Vous souhaitez supprimer votre compte?", + "ru" : "Хотите удалить свой аккаунт?", + "es" : "¿Quieres borrar tu cuenta?" + } + }, + "dashboard.settings.theme-change-title" : { + "used-in" : [ "src/app/main/ui/settings/options.cljs:65" ], + "translations" : { + "en" : "UI theme", + "fr" : "Thème de l'interface", + "ru" : "Тема интерфейса пользователя", + "es" : "Tema visual" + } + }, + "dashboard.settings.theme-label" : { + "used-in" : [ "src/app/main/ui/settings/options.cljs:67" ], + "translations" : { + "en" : "Select theme", + "fr" : "Sélectionnez un thème", + "ru" : "Выберите тему", + "es" : "Selecciona un tema" + } + }, + "dashboard.settings.update-photo-label" : { + "used-in" : [ "src/app/main/ui/settings/profile.cljs:106" ], + "translations" : { + "en" : "UPDATE", + "fr" : "METTRE A JOUR", + "ru" : "ОБНОВИТЬ", + "es" : "ACTUALIZAR" + } + }, + "dashboard.settings.yes-delete-my-account" : { + "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:67" ], + "translations" : { + "en" : "Yes, delete my account", + "fr" : "Oui, supprimez mon compte", + "ru" : "Да, удалить мой аккаунт", + "es" : "Si, borrar mi cuenta" + } + }, + "dashboard.sidebar.create-team" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:155" ], + "translations" : { + "en" : "+ Create new team" + } + }, + "dashboard.sidebar.default-team-name" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:328" ], + "translations" : { + "en" : "Your penpot" + } + }, + "dashboard.sidebar.delete-team" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:312" ], + "translations" : { + "en" : "Delete team" + } + }, "dashboard.sidebar.drafts" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:319" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:405" ], "translations" : { "en" : "Drafts", "fr" : "Brouillons", @@ -581,8 +875,14 @@ "es" : "Borradores" } }, + "dashboard.sidebar.leave-team" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:305", "src/app/main/ui/dashboard/sidebar.cljs:308" ], + "translations" : { + "en" : "Leave team" + } + }, "dashboard.sidebar.libraries" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:325" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:411" ], "translations" : { "en" : "Shared Libraries", "fr" : "", @@ -590,8 +890,38 @@ "es" : "Bibliotecas Compartidas" } }, + "dashboard.sidebar.no-projects-placeholder" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:426" ], + "translations" : { + "en" : "Pinned projects will appear here" + } + }, + "dashboard.sidebar.passowrd" : { + "translations" : { + "en" : "Password" + }, + "unused" : true + }, + "dashboard.sidebar.password" : { + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:75", "src/app/main/ui/dashboard/sidebar.cljs:458" ], + "translations" : { + "en" : "Password", + "fr" : "Mot de passe", + "ru" : "Пароль", + "es" : "Contraseña" + } + }, + "dashboard.sidebar.profile" : { + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:70", "src/app/main/ui/dashboard/sidebar.cljs:454" ], + "translations" : { + "en" : "Profile", + "fr" : "Profil", + "ru" : "Профиль", + "es" : "Perfil" + } + }, "dashboard.sidebar.projects" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:314" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:400" ], "translations" : { "en" : "Projects", "fr" : "Projetes", @@ -608,6 +938,39 @@ }, "unused" : true }, + "dashboard.sidebar.rename-team" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:301" ], + "translations" : { + "en" : "Rename" + } + }, + "dashboard.sidebar.settings" : { + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/sidebar.cljs:299" ], + "translations" : { + "en" : "Settings", + "fr" : "Settings", + "ru" : "ПАРАМЕТРЫ", + "es" : "Configuración" + } + }, + "dashboard.sidebar.switch-team" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:140" ], + "translations" : { + "en" : "Switch Team" + } + }, + "dashboard.sidebar.team-members" : { + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:298" ], + "translations" : { + "en" : "Members" + } + }, + "dashboard.team.num-of-members" : { + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:292" ], + "translations" : { + "en" : "%s members" + } + }, "ds.accept" : { "translations" : { "en" : "Accept", @@ -654,7 +1017,7 @@ "unused" : true }, "ds.confirm-cancel" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:22" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:28" ], "translations" : { "en" : "Cancel", "fr" : "Annuler", @@ -663,7 +1026,7 @@ } }, "ds.confirm-ok" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:23" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:29" ], "translations" : { "en" : "Ok", "fr" : "Ok", @@ -672,7 +1035,7 @@ } }, "ds.confirm-title" : { - "used-in" : [ "src/app/main/ui/confirm.cljs:21" ], + "used-in" : [ "src/app/main/ui/confirm.cljs:27", "src/app/main/ui/confirm.cljs:30" ], "translations" : { "en" : "Are you sure?", "fr" : "Êtes-vous sûr?", @@ -680,26 +1043,8 @@ "es" : "¿Seguro?" } }, - "ds.history.pinned" : { - "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null - }, - "unused" : true - }, - "ds.history.versions" : { - "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null - }, - "unused" : true - }, "ds.search.placeholder" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:151" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:110" ], "translations" : { "en" : "Search...", "fr" : "Rechercher...", @@ -707,17 +1052,8 @@ "es" : "Buscar..." } }, - "ds.settings.document-history" : { - "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null - }, - "unused" : true - }, "ds.updated-at" : { - "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:58" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:59" ], "translations" : { "en" : "Updated: %s", "fr" : "Mis à jour: %s", @@ -726,7 +1062,7 @@ } }, "errors.auth.unauthorized" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:70" ], + "used-in" : [ "src/app/main/ui/auth/login.cljs:82" ], "translations" : { "en" : "Username or password seems to be wrong.", "fr" : "Le nom d'utilisateur ou le mot de passe semble être faux.", @@ -735,7 +1071,7 @@ } }, "errors.email-already-exists" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:46", "src/app/main/ui/auth.cljs:89" ], + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:80", "src/app/main/ui/settings/change_email.cljs:47" ], "translations" : { "en" : "Email already used", "fr" : "Adresse e-mail déjà utilisée", @@ -744,7 +1080,7 @@ } }, "errors.email-already-validated" : { - "used-in" : [ "src/app/main/ui/auth.cljs:94" ], + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:85" ], "translations" : { "en" : "Email already validated.", "fr" : "Adresse e-mail déjà validé.", @@ -753,7 +1089,7 @@ } }, "errors.email-invalid-confirmation" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:36" ], + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:37" ], "translations" : { "en" : "Confirmation email must match", "fr" : "L'adresse e-mail de confirmation doit correspondre", @@ -762,7 +1098,7 @@ } }, "errors.generic" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:36", "src/app/main/ui/auth.cljs:98" ], + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:89", "src/app/main/ui/settings/profile.cljs:40", "src/app/main/ui/settings/options.cljs:32" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé.", @@ -789,7 +1125,7 @@ } }, "errors.media-type-mismatch" : { - "used-in" : [ "src/app/main/data/media.cljs:61", "src/app/main/data/workspace/persistence.cljs:413" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:413", "src/app/main/data/media.cljs:61" ], "translations" : { "en" : "Seems that the contents of the image does not match the file extension.", "fr" : "", @@ -798,7 +1134,7 @@ } }, "errors.media-type-not-allowed" : { - "used-in" : [ "src/app/main/data/media.cljs:58", "src/app/main/data/workspace/persistence.cljs:410" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:410", "src/app/main/data/media.cljs:58" ], "translations" : { "en" : "Seems that this is not a valid image.", "fr" : "", @@ -834,7 +1170,7 @@ } }, "errors.registration-disabled" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:48" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:39" ], "translations" : { "en" : "The registration is currently disabled.", "fr" : "L'enregistrement est actuellement désactivé.", @@ -843,7 +1179,7 @@ } }, "errors.unexpected-error" : { - "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/settings/change_email.cljs:50", "src/app/main/ui/auth/register.cljs:54", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66" ], + "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:45" ], "translations" : { "en" : "An unexpected error occurred.", "fr" : "Une erreur inattendue c'est produite", @@ -888,7 +1224,7 @@ "unused" : true }, "media.loading" : { - "used-in" : [ "src/app/main/data/media.cljs:43", "src/app/main/data/workspace/persistence.cljs:394" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:394", "src/app/main/data/media.cljs:43" ], "translations" : { "en" : "Loading image...", "fr" : "Chargement de l'image...", @@ -908,158 +1244,14 @@ "profile.recovery.go-to-login" : { "used-in" : [ "src/app/main/ui/auth/recovery.cljs:95" ], "translations" : { - "en" : null, + "en" : "Go to login", "fr" : null, "ru" : null, "es" : null } }, - "settings.cancel-and-keep-my-account" : { - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:46" ], - "translations" : { - "en" : "Cancel and keep my account", - "fr" : "Annuler et conserver mon compte", - "ru" : "Отменить и сохранить мой аккаунт", - "es" : "Cancelar y mantener mi cuenta" - } - }, - "settings.cancel-email-change" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:79" ], - "translations" : { - "en" : "Cancel", - "fr" : "Annuler", - "ru" : "Отмена", - "es" : "Cancelar" - } - }, - "settings.change-email-info" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:72" ], - "translations" : { - "en" : "We'll send you an email to your current email “%s” to verify your identity.", - "fr" : "Nous vous enverrons un e-mail à votre adresse actuelle “%s” pour vérifier votre identité.", - "ru" : "Мы отправим письмо для подтверждения подлиности на текущий email адрес “%s”.", - "es" : "Enviaremos un mensaje a tu correo actual “%s” para verificar tu identidad." - } - }, - "settings.change-email-info3" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:78" ], - "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null - } - }, - "settings.change-email-label" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:73" ], - "translations" : { - "en" : "Change email", - "fr" : "Changer adresse e-mail", - "ru" : "Сменить email адрес", - "es" : "Cambiar correo" - } - }, - "settings.change-email-submit-label" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:89" ], - "translations" : { - "en" : "Change email", - "fr" : "Changer adresse e-mail", - "ru" : "Сменить email адрес", - "es" : "Cambiar correo" - } - }, - "settings.change-email-title" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:68" ], - "translations" : { - "en" : "Change your email", - "fr" : "Changer adresse e-mail", - "ru" : "Сменить email адрес", - "es" : "Cambiar tu correo" - } - }, - "settings.confirm-email-label" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:85" ], - "translations" : { - "en" : "Verify new email", - "fr" : "Vérifier la nouvelle adresse e-mail", - "ru" : "Подтвердить новый email адрес", - "es" : "Verificar el nuevo correo" - } - }, - "settings.confirm-password-label" : { - "used-in" : [ "src/app/main/ui/settings/password.cljs:90" ], - "translations" : { - "en" : "Confirm password", - "fr" : "Confirmez mot de passe", - "ru" : "Подтвердите пароль", - "es" : "Confirmar contraseña" - } - }, - "settings.delete-account-info" : { - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:36" ], - "translations" : { - "en" : "By removing your account you’ll lose all your current projects and archives.", - "fr" : "En supprimant votre compte, vous perdrez tous vos projets et archives actuels.", - "ru" : "Удалив аккаунт Вы потеряете все прокты и архивы.", - "es" : "Si borras tu cuenta perderás todos tus proyectos y archivos." - } - }, - "settings.delete-account-title" : { - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:32" ], - "translations" : { - "en" : "Are you sure you want to delete your account?", - "fr" : "Voulez-vous vraiment supprimer votre compte?", - "ru" : "Вы уверены, что хотите удалить аккаунт?", - "es" : "¿Seguro que quieres borrar tu cuenta?" - } - }, - "settings.email-label" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:67" ], - "translations" : { - "en" : "Email", - "fr" : "E-mail", - "ru" : "Email", - "es" : "Correo" - } - }, - "settings.email-verification-pending" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:88" ], - "translations" : { - "en" : "There is a pending email validation.", - "fr" : "Une validation par e-mail est en attente.", - "ru" : "Подтверждение email адреса не выполнено.", - "es" : "Hay una validación pendiente del correo." - } - }, - "settings.fullname-label" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:59" ], - "translations" : { - "en" : "Your name", - "fr" : "Votre nom complet", - "ru" : "Ваше имя", - "es" : "Tu nombre" - } - }, - "settings.language-change-title" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:50" ], - "translations" : { - "en" : "Language", - "fr" : "Langue", - "ru" : "Язык", - "es" : "Idioma" - } - }, - "settings.language-label" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:56" ], - "translations" : { - "en" : "Select UI language", - "fr" : "Sélectionner la langue de l'interface", - "ru" : "Выберите язык интерфейса", - "es" : "Cambiar el idioma de la interfaz" - } - }, "settings.multiple" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:138", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:147", "src/app/main/ui/workspace/sidebar/options/typography.cljs:98", "src/app/main/ui/workspace/sidebar/options/typography.cljs:148", "src/app/main/ui/workspace/sidebar/options/typography.cljs:161", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:145", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162" ], "translations" : { "en" : "Mixed", "fr" : null, @@ -1067,140 +1259,14 @@ "es" : "Varios" } }, - "settings.new-email-label" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:80" ], - "translations" : { - "en" : "New email", - "fr" : "Nouvel e-mail", - "ru" : "Новый email адрес", - "es" : "Nuevo correo" - } - }, - "settings.new-password-label" : { - "used-in" : [ "src/app/main/ui/settings/password.cljs:85" ], - "translations" : { - "en" : "New password", - "fr" : "Nouveau mot de passe", - "ru" : "Новый пароль", - "es" : "Nueva contraseña" - } - }, - "settings.notifications.email-changed-successfully" : { - "used-in" : [ "src/app/main/ui/auth.cljs:63" ], - "translations" : { - "en" : "Your email address has been updated successfully", - "fr" : "Votre adresse e-mail a été mise à jour avec succès", - "ru" : "Ваш email адрес успешно обновлен", - "es" : "Tu dirección de correo ha sido actualizada" - } - }, - "settings.notifications.email-not-verified" : { - "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null - }, - "unused" : true - }, - "settings.notifications.email-verified-successfully" : { - "used-in" : [ "src/app/main/ui/auth.cljs:57" ], - "translations" : { - "en" : "Your email address has been verified successfully", - "fr" : "Votre adresse e-mail a été vérifiée avec succès", - "ru" : "Ваш email адрес успешно подтвержден", - "es" : "Tu dirección de correo ha sido verificada" - } - }, - "settings.notifications.password-saved" : { - "used-in" : [ "src/app/main/ui/settings/password.cljs:36" ], - "translations" : { - "en" : "Password saved successfully!", - "fr" : "Mot de passe enregistré avec succès!", - "ru" : "Пароль успешно сохранен!", - "es" : "¡Contraseña guardada!" - } - }, - "settings.notifications.profile-deletion-not-allowed" : { - "used-in" : [ "src/app/main/data/auth.cljs:157" ], - "translations" : { - "en" : "You can't delete you profile. Reasign your teams before proceed.", - "fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.", - "ru" : "Вы не можете удалить профиль. Сначала смените команду.", - "es" : "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir." - } - }, - "settings.notifications.profile-saved" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:37", "src/app/main/ui/settings/profile.cljs:41" ], - "translations" : { - "en" : "Profile saved successfully!", - "fr" : "Profil enregistré avec succès!", - "ru" : "Профиль успешно сохранен!", - "es" : "Perfil guardado correctamente!" - } - }, - "settings.old-password-label" : { - "used-in" : [ "src/app/main/ui/settings/password.cljs:80" ], - "translations" : { - "en" : "Old password", - "fr" : "Ancien mot de passe", - "ru" : "Старый пароль", - "es" : "Contraseña anterior" - } - }, - "settings.options" : { - "used-in" : [ "src/app/main/ui/settings/header.cljs:54" ], - "translations" : { - "en" : "OPTIONS", - "fr" : "OPTIONS", - "ru" : "ПАРАМЕТРЫ", - "es" : "OPCIONES" - } - }, - "settings.password" : { - "used-in" : [ "src/app/main/ui/settings/header.cljs:49" ], - "translations" : { - "en" : "PASSWORD", - "fr" : "MOT DE PASSE", - "ru" : "ПАРОЛЬ", - "es" : "CONTRASEÑA" - } - }, - "settings.password-change-title" : { - "used-in" : [ "src/app/main/ui/settings/password.cljs:75" ], - "translations" : { - "en" : "Change password", - "fr" : "Changement de mot de passe", - "ru" : "Изменить пароль", - "es" : "Cambiar contraseña" - } - }, "settings.profile" : { - "used-in" : [ "src/app/main/ui/settings/header.cljs:44" ], "translations" : { "en" : "PROFILE", "fr" : "PROFIL", "ru" : "ПРОФИЛЬ", "es" : "PERFIL" - } - }, - "settings.profile-submit-label" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:67", "src/app/main/ui/settings/profile.cljs:91", "src/app/main/ui/settings/password.cljs:93" ], - "translations" : { - "en" : "Update settings", - "fr" : "Mettre à jour les paramètres", - "ru" : "Обновить настройки", - "es" : "Actualizar opciones" - } - }, - "settings.remove-account-label" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:96" ], - "translations" : { - "en" : "Want to remove your account?", - "fr" : "Vous souhaitez supprimer votre compte?", - "ru" : "Хотите удалить свой аккаунт?", - "es" : "¿Quieres borrar tu cuenta?" - } + }, + "unused" : true }, "settings.teams" : { "translations" : { @@ -1211,42 +1277,6 @@ }, "unused" : true }, - "settings.theme-change-title" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:60" ], - "translations" : { - "en" : "UI theme", - "fr" : "Thème de l'interface", - "ru" : "Тема интерфейса пользователя", - "es" : "Tema visual" - } - }, - "settings.theme-label" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:61" ], - "translations" : { - "en" : "Select theme", - "fr" : "Sélectionnez un thème", - "ru" : "Выберите тему", - "es" : "Selecciona un tema" - } - }, - "settings.update-photo-label" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:117" ], - "translations" : { - "en" : "UPDATE", - "fr" : "METTRE A JOUR", - "ru" : "ОБНОВИТЬ", - "es" : "ACTUALIZAR" - } - }, - "settings.yes-delete-my-account" : { - "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:43" ], - "translations" : { - "en" : "Yes, delete my account", - "fr" : "Oui, supprimez mon compte", - "ru" : "Да, удалить мой аккаунт", - "es" : "Si, borrar mi cuenta" - } - }, "viewer.empty-state" : { "used-in" : [ "src/app/main/ui/viewer.cljs:42" ], "translations" : { @@ -1446,7 +1476,7 @@ } }, "workspace.assets.assets" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:613" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:615" ], "translations" : { "en" : "Assets", "fr" : "", @@ -1455,7 +1485,7 @@ } }, "workspace.assets.box-filter-all" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:633" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:635" ], "translations" : { "en" : "All assets", "fr" : "", @@ -1482,7 +1512,7 @@ "unused" : true }, "workspace.assets.colors" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:320", "src/app/main/ui/workspace/sidebar/assets.cljs:636" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:320", "src/app/main/ui/workspace/sidebar/assets.cljs:638" ], "translations" : { "en" : "Colors", "fr" : "", @@ -1491,7 +1521,7 @@ } }, "workspace.assets.components" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:82", "src/app/main/ui/workspace/sidebar/assets.cljs:634" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:82", "src/app/main/ui/workspace/sidebar/assets.cljs:636" ], "translations" : { "en" : "Components", "fr" : "", @@ -1518,7 +1548,7 @@ } }, "workspace.assets.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:517" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:518" ], "translations" : { "en" : "File library", "fr" : "", @@ -1527,7 +1557,7 @@ } }, "workspace.assets.graphics" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:163", "src/app/main/ui/workspace/sidebar/assets.cljs:635" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:163", "src/app/main/ui/workspace/sidebar/assets.cljs:637" ], "translations" : { "en" : "Graphics", "fr" : "", @@ -1536,7 +1566,7 @@ } }, "workspace.assets.libraries" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:616" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:618" ], "translations" : { "en" : "Libraries", "fr" : "", @@ -1545,7 +1575,7 @@ } }, "workspace.assets.not-found" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:577" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:579" ], "translations" : { "en" : "No assets found", "fr" : "", @@ -1563,7 +1593,7 @@ } }, "workspace.assets.search" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:620" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:622" ], "translations" : { "en" : "Search assets", "fr" : "", @@ -1572,7 +1602,7 @@ } }, "workspace.assets.shared" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:519" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:520" ], "translations" : { "en" : "SHARED", "fr" : "", @@ -1581,11 +1611,59 @@ } }, "workspace.assets.typography" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:406", "src/app/main/ui/workspace/sidebar/assets.cljs:637" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:406", "src/app/main/ui/workspace/sidebar/assets.cljs:639" ], "translations" : { "en" : "Typographies" } }, + "workspace.assets.typography.font-id" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:277" ], + "translations" : { + "en" : "Font" + } + }, + "workspace.assets.typography.font-size" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:285" ], + "translations" : { + "en" : "Size" + } + }, + "workspace.assets.typography.font-variant-id" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:281" ], + "translations" : { + "en" : "Variant" + } + }, + "workspace.assets.typography.go-to-edit" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:302" ], + "translations" : { + "en" : "Go to style library file to edit" + } + }, + "workspace.assets.typography.letter-spacing" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:293" ], + "translations" : { + "en" : "Letter Spacing" + } + }, + "workspace.assets.typography.line-height" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:289" ], + "translations" : { + "en" : "Line Height" + } + }, + "workspace.assets.typography.sample" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:255" ], + "translations" : { + "en" : "Ag" + } + }, + "workspace.assets.typography.text-transform" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:297" ], + "translations" : { + "en" : "Text Transform" + } + }, "workspace.header.menu.disable-dynamic-alignment" : { "used-in" : [ "src/app/main/ui/workspace/header.cljs:202" ], "translations" : { @@ -1770,19 +1848,19 @@ } }, "workspace.libraries.colors.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:338", "src/app/main/ui/workspace/colorpalette.cljs:149" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:340", "src/app/main/ui/workspace/colorpalette.cljs:149" ], "translations" : { "en" : "File library" } }, "workspace.libraries.colors.recent-colors" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:337", "src/app/main/ui/workspace/colorpalette.cljs:159" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:339", "src/app/main/ui/workspace/colorpalette.cljs:159" ], "translations" : { "en" : "Recent colors" } }, "workspace.libraries.colors.save-color" : { - "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:373" ], + "used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:375" ], "translations" : { "en" : "Save color" } @@ -1902,13 +1980,13 @@ } }, "workspace.libraries.text.multiple-typography" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:264" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:266" ], "translations" : { "en" : "Multiple typographies" } }, "workspace.libraries.text.multiple-typography-tooltip" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:266" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:268" ], "translations" : { "en" : "Unlink all typographies" } @@ -2265,7 +2343,7 @@ } }, "workspace.options.position" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:126", "src/app/main/ui/workspace/sidebar/options/measures.cljs:146" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:146", "src/app/main/ui/workspace/sidebar/options/frame.cljs:126" ], "translations" : { "en" : "Position", "fr" : "Position", @@ -2379,7 +2457,7 @@ } }, "workspace.options.size" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:99", "src/app/main/ui/workspace/sidebar/options/measures.cljs:116" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:116", "src/app/main/ui/workspace/sidebar/options/frame.cljs:99" ], "translations" : { "en" : "Size", "fr" : "Taille", @@ -2559,7 +2637,7 @@ } }, "workspace.options.text-options.letter-spacing" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:153" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:154" ], "translations" : { "en" : "Letter Spacing", "fr" : "Espacement de caractères", @@ -2568,7 +2646,7 @@ } }, "workspace.options.text-options.line-height" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:140" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:141" ], "translations" : { "en" : "Line height", "fr" : "Hauteur de ligne", @@ -2577,7 +2655,7 @@ } }, "workspace.options.text-options.lowercase" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:187" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:188" ], "translations" : { "en" : "Lowercase", "fr" : "Minuscule", @@ -2586,7 +2664,7 @@ } }, "workspace.options.text-options.none" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:177", "src/app/main/ui/workspace/sidebar/options/text.cljs:153" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:153", "src/app/main/ui/workspace/sidebar/options/typography.cljs:178" ], "translations" : { "en" : "None", "fr" : "Aucune", @@ -2604,7 +2682,7 @@ } }, "workspace.options.text-options.text-case" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:174" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:175" ], "translations" : { "en" : "Case", "fr" : "Casse", @@ -2638,7 +2716,7 @@ } }, "workspace.options.text-options.titlecase" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:192" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:193" ], "translations" : { "en" : "Titlecase", "fr" : "Titre", @@ -2656,7 +2734,7 @@ } }, "workspace.options.text-options.uppercase" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:182" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:183" ], "translations" : { "en" : "Uppercase", "fr" : "Majuscule", @@ -2791,7 +2869,7 @@ } }, "workspace.updates.dismiss" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:519" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:521" ], "translations" : { "en" : "Dismiss", "fr" : "", @@ -2800,7 +2878,7 @@ } }, "workspace.updates.there-are-updates" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:515" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:517" ], "translations" : { "en" : "There are updates in shared libraries", "fr" : "", @@ -2809,7 +2887,7 @@ } }, "workspace.updates.update" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:517" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:519" ], "translations" : { "en" : "Update", "fr" : "", @@ -2825,14 +2903,5 @@ "ru" : "Кликни чтобы закончить фигуру", "es" : "Pulsar para cerrar la ruta" } - }, - - "workspace.assets.typography.sample": "Ag", - "workspace.assets.typography.font-id": "Font", - "workspace.assets.typography.font-size": "Size", - "workspace.assets.typography.font-variant-id": "Variant", - "workspace.assets.typography.line-height": "Line Height", - "workspace.assets.typography.letter-spacing": "Letter Spacing", - "workspace.assets.typography.text-transform": "Text Transform", - "workspace.assets.typography.go-to-edit": "Go to style library file to edit" + } } diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 23d46901cf..134d14d655 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -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'; diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index e8d89e8464..601b2a9d84 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -10,7 +10,7 @@ .auth { display: grid; grid-template-rows: auto; - grid-template-columns: 388px auto; + grid-template-columns: 510px auto; } .auth-sidebar { diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index cedf415fd1..76cda8a6b6 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -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 { diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 43648c8e76..30fae12c71 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -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 { + display: flex; + width: 300px; + justify-content: center; + z-index: 1; + margin-top: 39px; + ul { - align-items: center; - bottom: 0; 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,11 +74,18 @@ } .dashboard-title { - color: $color-black; display: flex; - flex-shrink: 0; - font-size: $fs18; - z-index: 10; + h1 { + color: $color-black; + display: flex; + flex-shrink: 0; + font-size: $fs18; + z-index: 10; + } + + .context-menu.is-open { + margin-top: 10px; + } } .icon { diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss new file mode 100644 index 0000000000..b84fa5c6cc --- /dev/null +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -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; + } + } + +} diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 5c5efa5ce4..48fff7bed0 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -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,28 +84,36 @@ padding: 0px 10px; display: flex; flex-grow: 1; + } - .team-name { - flex-grow: 1; + .team-name { + flex-grow: 1; + display: flex; + align-items: center; + + .team-icon { display: flex; align-items: center; + padding-right: 10px; - .team-text { - color: $color-gray-60; + svg { + width: 23px; + height: 23px; + fill: $color-gray-60; + } + + img { + border-radius: 50%; + flex-shrink: 0; + height: 23px; + width: 23px; } } - } - - .team-icon { - display: flex; - align-items: center; - padding-right: 10px; - - svg { - width: 23px; - height: 23px; - fill: $color-gray-60; + .team-text { + color: $color-gray-60; + @include text-ellipsis; + width: 100px; } } @@ -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 { diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss new file mode 100644 index 0000000000..bbbe89feb2 --- /dev/null +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -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; + } + } + } + } +} diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 1fb8d86179..862b8a7361 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -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; + } + } +} diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss new file mode 100644 index 0000000000..e77a53b0a5 --- /dev/null +++ b/frontend/resources/styles/main/partials/dropdown.scss @@ -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; + } + } +} diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss index dedf5ef6b0..3b84219c7c 100644 --- a/frontend/resources/styles/main/partials/forms.scss +++ b/frontend/resources/styles/main/partials/forms.scss @@ -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 { } } } + diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 45ef29d24e..4564f54a0b 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -56,84 +56,140 @@ } } -.change-email-modal { - h2 { - font-size: $fs14; - margin-bottom: 20px; +// 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; } - .confirmation { - .btn-primary { - margin-bottom: 30px; - } + .modal-header-title { + display: flex; + align-items: center; + font-size: $fs24; + padding-left: 16px; - .featured-note .icon svg { - fill: $color-success; + 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: $fs18 + } + + 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 { diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss index 7b64072708..a06c5b1506 100644 --- a/frontend/resources/styles/main/partials/user-settings.scss +++ b/frontend/resources/styles/main/partials/user-settings.scss @@ -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; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index e882e54b84..6281a10f5b 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -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)))) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index a3a094d3d5..9b113d339d 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -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) diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index 41bb2cbd37..2209cdd995 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -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)))] - (ptk/reify ::request-account-deletion - ptk/WatchEvent - (watch [_ state stream] - (rx/concat - (->> (rp/mutation :delete-profile {}) - (rx/map #(rt/nav :auth-goodbye)) - (rx/catch on-error))))))) +(defn request-account-deletion + [params] + (ptk/reify ::request-account-deletion + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-error on-success] + :or {on-error identity + on-success identity}} (meta params)] + (->> (rp/mutation :delete-profile {}) + (rx/tap on-success) + (rx/catch on-error)))))) ;; --- Recovery Request diff --git a/frontend/src/app/main/data/colors.cljs b/frontend/src/app/main/data/colors.cljs index ce119886fc..217f28aad1 100644 --- a/frontend/src/app/main/data/colors.cljs +++ b/frontend/src/app/main/data/colors.cljs @@ -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] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index e580cd203d..52ae6f79ab 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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] - (->> (rp/query :team params) - (rx/map #(partial fetched %))))))) + (let [profile (:profile state)] + (->> (rp/query :team params) + (rx/map assoc-team-avatar) + (rx/map #(partial fetched %)))))))) +(defn fetch-team-members + [{:keys [id] :as params}] + (us/assert ::us/uuid id) + (letfn [(fetched [members state] + (assoc-in state [:team-members id] (d/index-by :id members)))] + (ptk/reify ::fetch-team-members + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :team-members {:team-id id}) + (rx/map #(partial fetched %))))))) ;; --- Fetch Projects @@ -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] diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index 42f02a97e7..0b12b21f65 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -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] - (ptk/reify ::show-modal - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc ::modal {:id id - :type type - :props props - :allow-click-outside false}))))) +(defonce components (atom {})) -(defn hide-modal [] +(defn show + ([props] + (show (uuid/next) (:type props) props)) + ([type props] (show (uuid/next) type props)) + ([id type props] + (ptk/reify ::show-modal + ptk/UpdateEvent + (update [_ state] + (assoc state ::modal {:id id + :type type + :props props + :allow-click-outside false}))))) + +(defn hide + [] (ptk/reify ::hide-modal ptk/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))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index b068345bf3..2e990f7d09 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -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] diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index bce56562cc..49e308de43 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -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")] diff --git a/frontend/src/app/main/store.clj b/frontend/src/app/main/store.clj new file mode 100644 index 0000000000..f8510983d0 --- /dev/null +++ b/frontend/src/app/main/store.clj @@ -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))) + diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 281e74369f..ac303d734b 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -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] diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index ff002f454e..3e599f6514 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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" - ["/projects" :dashboard-projects] - ["/search" :dashboard-search] - ["/libraries" :dashboard-libraries] - ["/projects/:project-id" :dashboard-files]]] + ["/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]] ["/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)) diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 8d636175e5..143e0ad67b 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -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])) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 31114f80f2..f2c74801e8 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -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] - (reset! error? false) - (let [params (with-meta (:clean-data form) - {:on-error on-error})] - (st/emit! (@submit-event params))))] + (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 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 - {:name :email - :type "text" - :tab-index "2" - :help-icon i/at - :label (t locale "auth.email-label")}] - [:& input - {:type "password" - :name :password - :tab-index "3" - :help-icon i/eye - :label (t locale "auth.password-label")}] - [:& submit-button + [:& 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")}]] + [:div.fields-row + [:& fm/input + {:type "password" + :name :password + :tab-index "3" + :help-icon i/eye + :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}] diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index 87d19fbf47..69a83f21cd 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -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 - :validators [password-equality] - :initial params} + (let [form (fm/use-form :spec ::recovery-form + :validators [password-equality] + :initial params)] + [:& fm/form {:on-submit on-submit + :form form} + [:div.fields-row + [:& fm/input {:type "password" + :name :password-1 + :label (t locale "auth.new-password-label")}]] - [:& input {:type "password" - :name :password-1 - :label (t locale "auth.new-password-label")}] + [:div.fields-row + [:& fm/input {:type "password" + :name :password-2 + :label (t locale "auth.confirm-password-label")}]] - [:& input {:type "password" - :name :password-2 - :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 diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index cccb74e465..6abc4ebfd0 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -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) - {:on-success on-success})] + (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 {}} + (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"}]] - [:& input {:name :email - :label (t locale "auth.email-label") - :help-icon i/at - :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 diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 5511f3ab1a..04becb6736 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -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))] - (st/emit! (rt/nav :auth-login) - (dm/success msg)))) + (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 message))))) (defn- validate [data] @@ -67,57 +63,74 @@ (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 - :validators [validate] - :initial {}} - [:& input {:name :fullname - :tab-index "1" - :label (t locale "auth.fullname-label") - :type "text"}] - [:& input {:type "email" - :name :email - :tab-index "2" - :help-icon i/at - :label (t locale "auth.email-label")}] - [:& input {:name :password - :tab-index "3" - :hint (t locale "auth.password-length-hint") - :label (t locale "auth.password-label") - :type "password"}] + [{: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 initial)] - [:& submit-button - {:label (t locale "auth.register-submit-label")}]]) + [:& 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"}]] + [:div.fields-row + [:& fm/input {:type "email" + :name :email + :tab-index "2" + :help-icon i/at + :label (t locale "auth.email-label")}]] + [:div.fields-row + [:& fm/input {:name :password + :tab-index "3" + :hint (t locale "auth.password-length-hint") + :label (t locale "auth.password-label") + :type "password"}]] + + [:& fm/submit-button + {:label (t locale "auth.register-submit-label")}]])) ;; --- Register Page (mf/defc register-page - [{:keys [locale] :as props}] - [:section.generic-form - [:div.form-container - [:h1 (t locale "auth.register-title")] - [:div.subtitle (t locale "auth.register-subtitle")] - (when cfg/demo-warning - [:& demo-warning]) + [{: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 - [:span (t locale "auth.already-have-account") " "] - [:a {:on-click #(st/emit! (rt/nav :auth-login)) - :tab-index "4"} - (t locale "auth.login-here")]] + [:div.links + [:div.link-entry + [:span (t locale "auth.already-have-account") " "] + [:a {:on-click #(st/emit! (rt/nav :auth-login)) + :tab-index "4"} + (t locale "auth.login-here")]] - [:div.link-entry - [: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")]]]]]) + [:div.link-entry + [: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")]]]]) diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs new file mode 100644 index 0000000000..670a1d4df7 --- /dev/null +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -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])) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index a6793674fa..3af9230766 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -20,21 +20,22 @@ [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 + help-icon' (cond (and (= type "password") (= @type' "password")) i/eye @@ -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,33 +81,33 @@ :type @type') (obj/clj->props))] - [:div.field.custom-input - {:class klass} - [:* - [:label label] - [:> :input props] - (when help-icon' - [:div.help-icon - {:style {:cursor "pointer"} - :on-click (when (= "password" type) - swap-text-password)} - help-icon']) - (cond - (and touched? (:message error)) - [:span.error (t locale (:message error))] + [:div.custom-input + {:class klass} + [:* + [:label label] + [:> :input props] + (when help-icon' + [:div.help-icon + {:style {:cursor "pointer"} + :on-click (when (= "password" type) + swap-text-password)} + help-icon']) + (cond + (and touched? (:message error)) + [:span.error (t locale (:message error))] - (string? hint) - [:span.hint hint])]])) + (string? hint) + [:span.hint hint])]])) (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]])) - - - diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index 11c2891979..0b86606467 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -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 -;; Copyright (c) 2016 Juan de la Cruz +;; 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 - (fn [event] - (dom/prevent-default event) - (modal/hide!) - (on-accept (dissoc ctx :on-accept :on-cancel))) + 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) + (st/emit! (modal/hide)) + (on-accept props))) + + cancel-fn + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (on-cancel props)))] - cancel - (fn [event] - (dom/prevent-default event) - (modal/hide!) - (when on-cancel - (on-cancel (dissoc ctx :on-accept :on-cancel))))] [:div.modal-overlay - [:div.modal.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}]]]]])) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 85b7f21791..d1abccf4e7 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 51e0e09c3d..c50ba969bd 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -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] @@ -24,9 +25,9 @@ (mf/defc header [{:keys [team project] :as props}] - (let [local (mf/use-state {:menu-open false - :edition false}) - locale (mf/deref i18n/locale) + (let [local (mf/use-state {:menu-open false + :edition false}) + locale (mf/deref i18n/locale) project-id (:id project) team-id (:id team) @@ -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.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"]]] + [: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]]}]])) [: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}]]])) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 37fc89a600..f74964336d 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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]] @@ -60,21 +61,25 @@ (mf/defc grid-item {:wrap [mf/memo]} [{:keys [id file] :as props}] - (let [local (mf/use-state {:menu-open false :edition false}) - locale (mf/deref i18n/locale) + (let [local (mf/use-state {:menu-open false :edition false}) + locale (mf/deref i18n/locale) + on-close (mf/use-callback #(swap! local assoc :menu-open false)) - delete (mf/use-callback (mf/deps id) #(st/emit! (dd/delete-file file))) - 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)) - :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))) + (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-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])])) diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.cljs b/frontend/src/app/main/ui/dashboard/inline_edition.cljs new file mode 100644 index 0000000000..e376ebcd6e --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/inline_edition.cljs @@ -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]])) + + diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 027d71dd8a..907c63b0ba 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index d32652d6bd..981fcd7336 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 0c2968331d..24ababdb52 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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,23 +50,29 @@ (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)) + emit! (mf/use-memo #(f/debounce st/emit! 500)) on-search-focus (mf/use-callback @@ -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) + teams (mf/use-state []) - show-team-opts-ddwn? (mf/use-state false) - show-teams-ddwn? (mf/use-state false) - teams (mf/use-state []) + on-create-clicked + (mf/use-callback + (st/emitf (modal/show :team-form {}))) - on-nav + go-projects + (mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %})))] + + (mf/use-layout-effect + (mf/deps (:id team)) + (fn [] + (->> (rp/query! :teams) + (rx/map #(mapv dd/assoc-team-avatar %)) + (rx/subs #(reset! teams %))))) + + [:ul.dropdown.teams-dropdown + [:li.title (t locale "dashboard.sidebar.switch-team")] + [:hr] + [:li.team-name {:on-click (partial go-projects (:default-team-id profile))} + [:span.team-icon i/logo-icon] + [:span.team-text "Your penpot"]] + + (for [team (remove :is-default @teams)] + [:* {:key (:id team)} + [:li.team-name {:on-click (partial go-projects (:id team))} + [:span.team-icon + [:img {:src (cfg/resolve-media-path (:photo team))}]] + [:span.team-text {:title (:name team)} (:name team)]]]) + + [:hr] + [:li.action {:on-click on-create-clicked} + (t locale "dashboard.sidebar.create-team")]])) + +(s/def ::member-id ::us/uuid) +(s/def ::leave-modal-form + (s/keys :req-un [::member-id])) + +(mf/defc leave-and-reassign-modal + {::mf/register modal/components + ::mf/register-as ::leave-and-reassign + ::mf/props-spec ::kaka-de-vaca} + [{:keys [members profile team accept]}] + (let [form (fm/use-form :spec ::leave-modal-form :initial {}) + options (into [{:value "" :label "Select a member to promote"}] + (map #(hash-map :name (:name %) :value (str (:id %))) members)) + + on-cancel + (mf/use-callback (st/emitf (modal/hide))) + + on-accept + (mf/use-callback + (mf/deps form) + (fn [event] + (let [member-id (get-in @form [:clean-data :member-id])] + (accept member-id))))] + + [:div.modal-overlay + [:div.modal-container.confirm-dialog + [:div.modal-header + [:div.modal-header-title + [:h2 "Before you leave"]] + [:div.modal-close-button + {:on-click on-cancel} i/close]] + + [:div.modal-content.generic-form + [:p "You are " (:name team) " owner."] + [:p "Select an other member to promote before leave."] + + [:& fm/form {:form form} + [:& fm/select {:name :member-id + :options options}]]] + + [:div.modal-footer + [:div.action-buttons + [:input.cancel-button + {:type "button" + :value "Cancel" + :on-click on-cancel}] + + [:input.accept-button + {:type "button" + :class (when-not (:valid @form) "btn-disabled") + :disabled (not (:valid @form)) + :value "Promoto and Leave" + :on-click on-accept}]]]]])) + + +(mf/defc team-options-dropdown + [{:keys [team locale profile] :as props}] + (let [members (mf/use-state []) + + go-members + (mf/use-callback + (mf/deps team) + (st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)}))) + + go-settings + (mf/use-callback + (mf/deps team) + (st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)}))) + + go-projects (mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %}))) 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 - [:div.team-name - [:span.team-icon i/logo-icon] - (if (:is-default team) - [:span.team-text "Your penpot"] - [:span.team-text (:name team)])] + (if (:is-default team) + [:div.team-name + [:span.team-icon i/logo-icon] + [: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}]]])) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs new file mode 100644 index 0000000000..e7af9c9fdb --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -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"]]]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs new file mode 100644 index 0000000000..0e263e5516 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -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")}]]]]])) + + diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index f8288d25f3..e3894e042b 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -14,53 +14,33 @@ [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 [event parent-ref] - (let [parent (mf/ref-val parent-ref) + (let [parent (mf/ref-val parent-ref) current (dom/get-target event)] (when (and (dom/equals? (.-firstElementChild ^js parent) current) (= (.-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 [] diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index d709fec774..2c33b29296 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -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}] - (case section - :settings-profile (mf/element profile-page) - :settings-password (mf/element password-page) - :settings-options (mf/element options-page))]])) + 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 + [:& profile-page {:locale locale}] + :settings-password + [:& password-page {:locale locale}] + :settings-options + [:& options-page {:locale locale}])]]])) diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index d1258f77ed..ad39482260 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -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")}]]]]]])) + + diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs index 7c79a8c0f2..0b9e4b02d3 100644 --- a/frontend/src/app/main/ui/settings/delete_account.cljs +++ b/frontend/src/app/main/ui/settings/delete_account.cljs @@ -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")]]]]])) diff --git a/frontend/src/app/main/ui/settings/header.cljs b/frontend/src/app/main/ui/settings/header.cljs deleted file mode 100644 index 9f35e59985..0000000000 --- a/frontend/src/app/main/ui/settings/header.cljs +++ /dev/null @@ -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")]]])) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index abc36957b8..8ffd1b1906 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -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" - :on-submit on-submit - :spec ::options-form - :initial profile} + [{: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 + :form form} - [:h2 (t locale "settings.language-change-title")] + [:h2 (t locale "dashboard.settings.language-change-title")] - [:& 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") - :default "en" - :name :lang}] + [: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 "dashboard.settings.language-label") + :default "en" + :name :lang}]] - [:h2 (t locale "settings.theme-change-title")] - [:& select {:label (t locale "settings.theme-label") - :name :theme - :default "default" - :options [{:label "Default" :value "default"}]}] - - [:& submit-button - {:label (t locale "settings.profile-submit-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"}]}]] + [:& 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}]]]) diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 77b0305888..c54118dcdd 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -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 - :validators [password-equality] - :initial {}} - [:h2 (t locale "settings.password-change-title")] + (let [form (fm/use-form :spec ::password-form + :validators [password-equality] + :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 "dashboard.settings.old-password-label")}]] - [:& input - {:type "password" - :name :password-old - :label (t locale "settings.old-password-label")}] + [:div.fields-row + [:& fm/input + {:type "password" + :name :password-1 + :label (t locale "dashboard.settings.new-password-label")}]] - [:& input - {:type "password" - :name :password-1 - :label (t locale "settings.new-password-label")}] + [:div.fields-row + [:& fm/input + {:type "password" + :name :password-2 + :label (t locale "dashboard.settings.confirm-password-label")}]] - [:& input - {:type "password" - :name :password-2 - :label (t locale "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}]]]) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index b391e15ee5..8d76498ea2 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -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 - {:type "text" - :name :fullname - :label (t locale "settings.fullname-label") - :trim true}] + (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 "dashboard.settings.fullname-label")}]] - [:& input - {:type "email" - :name :email - :disabled true - :help-icon i/at - :label (t locale "settings.email-label")}] + [:div.fields-row + [:& fm/input + {:type "email" + :name :email + :disabled true + :help-icon i/at + :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 - [:& profile-photo-form {:locale locale}] - [:& profile-form {:locale locale}]]])) + [{:keys [locale]}] + [:div.dashboard-settings + [:div.form-container.two-columns + [:& profile-photo-form {:locale locale}] + [:& profile-form {:locale locale}]]]) + diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs new file mode 100644 index 0000000000..cc754a47d1 --- /dev/null +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -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}]]]) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index e7ff4857f4..f0d827ce96 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -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]] diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index b2d357217f..85d58aeb34 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -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]])) @@ -335,7 +335,7 @@ [:select {:on-change (fn [e] (let [val (-> e dom/get-target dom/get-value)] (reset! selected-library val))) - :value @selected-library} + :value @selected-library} [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")] [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")] (for [[_ {:keys [name id]}] shared-libs] diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index c5b4e90f8c..bdb7209358 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -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]] diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 00a27dc171..2f4aea9181 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index d38bf6d573..c2c638a7f9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 3965cb3376..021fb8bc18 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -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])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 9015f4ec3a..5bfb3952ff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 1144686629..ce0ef64f9e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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] diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index e9a6382fc7..17e80068d1 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -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,45 +37,100 @@ :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) - :errors {} - :touched {}}) + [& {: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))] - cleaned (s/conform spec (:data state)) - problems (when (= ::s/invalid cleaned) - (::s/problems (s/explain-data spec (:data state)))) + (mf/use-effect + (mf/deps initial) + (fn [] + (if (fn? initial) + (swap! form update :data merge (initial)) + (swap! form update :data merge initial)))) - errors (merge (reduce interpret-problem {} problems) - (reduce (fn [errors vf] - (merge errors (vf (:data state)))) - {} validators) - (:errors state))] - (-> (assoc state - :errors errors - :clean-data (when (not= cleaned ::s/invalid) cleaned) - :valid (and (empty? errors) - (not= cleaned ::s/invalid))) - (impl-mutator update-state)))) + 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)))) + + errors (merge (reduce interpret-problem {} problems) + (reduce (fn [errors vf] + (merge errors (vf (:data state)))) + {} validators) + (:errors state))] + + (assoc state + :errors errors + :clean-data (when (not= cleaned ::s/invalid) cleaned) + :valid (and (empty? errors) + (not= cleaned ::s/invalid)))))) + +(defn- create-form-mutator + [state-ref render opts] + (reify + IDeref + (-deref [_] + (mf/ref-val state-ref)) + + IReset + (-reset! [it new-value] + (mf/set-ref-val! state-ref new-value) + (render inc)) + + + ISwap + (-swap! [self f] + (let [f (wrap-update-fn f opts)] + (mf/set-ref-val! state-ref (f (mf/ref-val state-ref))) + (render inc))) + + + (-swap! [self f x] + (let [f (wrap-update-fn f opts)] + (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x)) + (render inc))) + + + (-swap! [self f x y] + (let [f (wrap-update-fn f opts)] + (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x y)) + (render inc))) + + (-swap! [self f x y more] + (let [f (wrap-update-fn f opts)] + (mf/set-ref-val! state-ref (apply f (mf/ref-val state-ref) x y more)) + (render inc))))) (defn on-input-change - ([{: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)] + value (dom/get-value target)] (swap! form (fn [state] (-> state (assoc-in [:data field] (if trim? (str/trim value) value)) (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))))) diff --git a/frontend/src/app/util/http_api.cljs b/frontend/src/app/util/http_api.cljs index 99b1787d28..73cb69a50b 100644 --- a/frontend/src/app/util/http_api.cljs +++ b/frontend/src/app/util/http_api.cljs @@ -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))) diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index 5ce01128c3..6950cda6c9 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -5,34 +5,43 @@ ;; Copyright (c) 2016 Andrey Antukh (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)) diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index 7b405da7f5..ef6f39e299 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -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)]