0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-09 13:31:23 -05:00

🎉 Backport questions form integration.

Among other related that need to be ported.
This commit is contained in:
Andrey Antukh 2021-11-04 15:17:12 +01:00
parent a2d3616171
commit eb1bcfba83
45 changed files with 983 additions and 529 deletions

View file

@ -58,7 +58,9 @@
(assoc response :cookies {cookie-name {:path "/"
:http-only true
:value id
:same-site (if cors? :none :strict)
:same-site (cond (not secure?) :lax
cors? :none
:else :strict)
:secure secure?}})))
(defn- clear-cookies

View file

@ -36,7 +36,8 @@
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
:props {:onboarding-viewed true}}]
:props {}
}]
(when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation

View file

@ -335,9 +335,9 @@
;; --- MUTATION: Logout
(s/def ::logout
(s/keys :req-un [::profile-id]))
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout
(sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))

View file

@ -104,24 +104,53 @@
;; --- Mutation: Leave Team
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req-un [::profile-id ::id]))
(s/keys :req-un [::profile-id ::id]
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
[{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)]
(when (:is-owner perms)
(cond
;; we can only proceed if there are more members in the team
;; besides the current profile
(<= (count members) 1)
(ex/raise :type :validation
:code :no-enough-members-for-leave
:context {:members (count members)})
;; if the `reassign-to` is filled and has a different value
;; than the current profile-id, we proceed to reassing the
;; owner role to profile identified by the `reassign-to`.
(and reassign-to (not= reassign-to profile-id))
(let [member (d/seek #(= reassign-to (:id %)) members)]
(when-not member
(ex/raise :type :not-found :code :member-does-not-exist))
;; unasign owner role to current profile
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id id
:profile-id profile-id})
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(role->params :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
;; current profile is owner, we dont allow it because there
;; must always be an owner.
(:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "reasing owner before leave"))
(when-not (> (count members) 1)
(ex/raise :type :validation
:code :cant-leave-team
:context {:members (count members)}))
:hint "releasing owner before leave"))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
@ -129,7 +158,6 @@
nil)))
;; --- Mutation: Delete Team
(s/def ::delete-team
@ -156,7 +184,6 @@
;; --- Mutation: Team Update Role
(declare retrieve-team-member)
(declare role->params)
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)

View file

@ -37,10 +37,15 @@
(sv/defmethod ::profile {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
(if profile-id
(retrieve-profile pool profile-id)
{:id uuid/zero
:fullname "Anonymous User"}))
;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception.
(or (ex/try*
#(some->> profile-id (retrieve-profile pool))
#(when (not= :not-found (:type (ex-data %))) (throw %)))
{:id uuid/zero
:fullname "Anonymous User"}))
(def ^:private sql:default-profile-team
"select t.id, name

View file

@ -21,8 +21,10 @@
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?")
and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]

View file

@ -6,6 +6,7 @@
(ns app.services-profile-test
(:require
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.mutations.profile :as profile]
[app.test-helpers :as th]
@ -153,11 +154,8 @@
:profile-id (:id prof)}
out (th/query! params)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]

View file

@ -33,7 +33,6 @@
:role :editor
:profile-id (:id profile1)}]
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
@ -136,9 +135,10 @@
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})]

Binary file not shown.

After

(image error) Size: 30 KiB

Binary file not shown.

After

(image error) Size: 25 KiB

Binary file not shown.

After

(image error) Size: 52 KiB

Binary file not shown.

After

(image error) Size: 199 KiB

Binary file not shown.

After

(image error) Size: 38 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

After

(image error) Size: 18 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 20 KiB

Binary file not shown.

After

(image error) Size: 19 KiB

View file

@ -89,3 +89,4 @@
@import "main/partials/handoff";
@import "main/partials/exception-page";
@import "main/partials/share-link";
@import "main/partials/af-signup-questions";

View file

@ -0,0 +1,197 @@
// 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) UXBOX Labs SL
.af-form {
background-color: $color-white;
color: $color-gray-60 !important;
max-width: 760px !important;
overflow-y: auto;
padding: 3rem;
width: 100% !important;
h1, h3 {
font-family: 'worksans', sans-serif !important;
margin-bottom: .8rem;
font-weight: 500 !important;
}
h1 {
font-size: $fs38;
}
strong {
font-weight: 500;
}
p, label {
font-family: 'worksans', sans-serif !important;
font-size: $fs14;
}
form {
max-width: 760px;
width: 100%;
}
button {
font-family: 'worksans', sans-serif !important;
}
.af-choice,
.af-choice-multiple {
display: flex;
flex-wrap: wrap;
}
.af-choice-option {
max-width: 33%;
width: 100%;
label {
font-family: 'worksans', sans-serif !important;
font-size: $fs14;
padding-left: 0;
}
}
.af-choice-multiple {
.af-choice-option {
max-width: 50%;
width: 100%;
}
}
.af-divider-block {
/* margin-bottom: 2rem; */
p {
&::after,
&::before {
border-color: transparent;
}
}
}
.af-dropdown-text,
.text {
font-family: 'worksans', sans-serif !important;
}
.af-step-next {
display: flex;
margin-top: 2rem;
}
.af-step-next button {
color: $color-black;
background-color: $color-primary;
max-width: 180px;
margin-left: auto;
}
.af-step-previous {
margin-top: -40px;
}
.af-step-button {
text-align: left;
}
.af-field-input {
margin: 0.5rem 0;
}
.af-choice-option input:checked+label:before,
.af-legal input:checked+label:before {
background-color: $color-primary;
}
.af-field-use_of_penpot .af-choice-option input:checked+label,
.af-field-previous_design_tool .af-choice-option input:checked+label {
&::before {
background-color: transparent;
border: 2px solid $color-primary;
}
}
.af-field-use_of_penpot .af-choice-option label {
padding-top: 6rem;
background-size: 120px;
min-height: 150px;
}
.af-field-use_of_penpot .af-choice-option:nth-child(1) label {
background-image: url("../images/form/use-for-1.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(2) label {
background-image: url("../images/form/use-for-2.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(3) label {
background-image: url("../images/form/use-for-3.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(4) label {
background-image: url("../images/form/use-for-4.jpg");
}
.af-field-use_of_penpot label,
.af-field-previous_design_tool label {
display: flex;
padding-top: 5rem;
justify-content: center;
background-size: 50px;
background-repeat: no-repeat;
background-position: center 1rem;
margin: 1rem !important;
min-height: 130px;
position: relative;
text-align: center;
&:hover {
background-color: transparent;
box-shadow: 0px 10px 20px rgba(0,0,0,.2);
}
&::before {
background-color: transparent;
border-radius: 4px;
min-width: 100%;
min-height: 100%;
position: absolute;
top: 0;
left: 0;
margin: 0;
}
&::after {
display: none !important;
}
}
.af-field-previous_design_tool .af-choice-option:nth-child(1) label {
background-image: url("../images/form/figma.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(2) label {
background-image: url("../images/form/sketch.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(3) label {
background-image: url("../images/form/adobe-xd.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(4) label {
background-image: url("../images/form/uxpin.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(5) label {
background-image: url("../images/form/invision.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(6) label {
background-image: url("../images/form/never-used.png");
}
}

View file

@ -12,6 +12,7 @@
display: flex;
align-items: center;
padding: 32px;
z-index: 1000;
cursor: pointer;

View file

@ -859,23 +859,23 @@
background-position: left top;
background-size: 11%;
}
.modal-left:hover {
background-image: url("/images/on-solo-hover.svg");
background-size: 15%;
}
.modal-right {
background-image: url("/images/on-teamup.svg");
background-position: right top;
background-size: 28%;
}
.modal-right:hover {
background-image: url("/images/on-teamup-hover.svg");
background-size: 32%;
}
.modal-right,
.modal-left {
background-repeat: no-repeat;
@ -1001,17 +1001,17 @@
.template-item {
width: 275px;
border: 1px solid $color-gray-10;
display: flex;
flex-direction: column;
text-align: left;
border-radius: $br-small;
&:not(:last-child) {
margin-bottom: 22px;
}
}
.template-item-content {
// height: 144px;
flex-grow: 1;
@ -1020,7 +1020,7 @@
border-radius: $br-small $br-small 0 0;
}
}
.template-item-title {
padding: 6px 12px;
height: 64px;
@ -1135,3 +1135,49 @@
}
}
.questions-form {
.modal-overlay {
z-index: 2001;
}
.modal-container {
background-image: url("../images/deco-left.png"), url("../images/deco-right.png");
background-repeat: no-repeat;
background-position: 10% 50px, 90% 50px;
background-size: 65px;
display: flex;
flex-direction: row;
height: 100vh;
justify-content: center;
width: 100vw;
.af-form {
--primary-color: #00C38B;
--input-background-color: #ffffff;
--label-font-size: $fs16;
--field-error-font-color: #E65244;
--message-success-font-color: #49D793;
--message-fail-font-color: #E65244;
--invalid-field-border-color: #E65244;
--dropdown-background-color: #ffffff;
--primary-font-color: #000;
--input-border-color: rgb(224, 230, 240);
--input-border-radius: 3px;
--button-border-radius: 3px;
--message-border-radius: 3px;
--checkbox-border-radius: 3px;
--dropdown-option-background-color: rgba(0,195,139,1);
--dropdown-option-active-background-color: rgba(0,138,98,1);
--invalid-field-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
--message-fail-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
--message-success-background-color: rgba(171,232,197,1);
}
}
.modal-overlay {
background-color: rgba(0,0,0,0.9);
}
}

View file

@ -78,6 +78,7 @@
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes"))
(def sentry-dsn (obj/get global "penpotSentryDsn"))
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
(def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))

View file

@ -16,6 +16,7 @@
[app.main.repo :as rp]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as tm]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
@ -60,6 +61,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects)
(declare fetch-team-members)
(defn initialize
[{:keys [id] :as params}]
@ -84,6 +86,7 @@
(rx/merge
(ptk/watch (df/load-team-fonts id) state stream)
(ptk/watch (fetch-projects) state stream)
(ptk/watch (fetch-team-members) state stream)
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))
@ -237,13 +240,14 @@
(update :dashboard-files d/merge files))))))
(defn fetch-recent-files
[]
(ptk/reify ::fetch-recent-files
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/query :team-recent-files {:team-id team-id})
(rx/map recent-files-fetched))))))
([] (fetch-recent-files nil))
([team-id]
(ptk/reify ::fetch-recent-files
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))]
(->> (rp/query :team-recent-files {:team-id team-id})
(rx/map recent-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
@ -396,16 +400,13 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
team-id (:current-team-id state)]
(rx/concat
(when (uuid? reassign-to)
(->> (rp/mutation! :update-team-member-role {:team-id team-id
:role :owner
:member-id reassign-to})
(rx/ignore)))
(->> (rp/mutation! :leave-team {:id team-id})
(rx/tap on-success)
(rx/catch on-error)))))))
team-id (:current-team-id state)
params (cond-> {:id team-id}
(uuid? reassign-to)
(assoc :reassign-to reassign-to))]
(->> (rp/mutation! :leave-team params)
(rx/tap #(tm/schedule on-success))
(rx/catch on-error))))))
(defn invite-team-member
[{:keys [email role] :as params}]

View file

@ -7,12 +7,12 @@
(ns app.main.data.users
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.media :as di]
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.util.i18n :as i18n]
[app.util.router :as rt]
@ -93,6 +93,8 @@
;; --- EVENT: fetch-profile
(declare logout)
(def profile-fetched?
(ptk/type? ::profile-fetched))
@ -105,18 +107,18 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :profile-id id)
(assoc :profile profile)))
(cond-> state
(is-authenticated? profile)
(-> (assoc :profile-id id)
(assoc :profile profile))))
ptk/EffectEvent
(effect [_ state _]
(let [profile (:profile state)]
(when (not= uuid/zero (:id profile))
(swap! storage assoc :profile profile)
(i18n/set-locale! (:lang profile))
(some-> (:theme profile)
(theme/set-current-theme!)))))))
(when-let [profile (:profile state)]
(swap! storage assoc :profile profile)
(i18n/set-locale! (:lang profile))
(some-> (:theme profile)
(theme/set-current-theme!))))))
(defn fetch-profile
[]
@ -145,55 +147,84 @@
(rx/mapcat (fn [profile]
(if (= uuid/zero (:id profile))
(rx/empty)
(rx/of (fetch-teams))))))))))
(rx/of (fetch-teams)))))
(rx/observe-on :async))))))
;; --- EVENT: login
(defn- logged-in
"This is the main event that is executed once we have logged in
profile. The profile can proceed from standard login or from
accepting invitation, or third party auth signup or singin."
[profile]
(ptk/reify ::logged-in
IDeref
(-deref [_] profile)
(letfn [(get-redirect-event []
(let [team-id (:default-team-id profile)]
(rt/nav' :dashboard-projects {:team-id team-id})))]
ptk/WatchEvent
(watch [_ _ _]
(let [team-id (get-current-team-id profile)]
(->> (rx/concat
(rx/of (profile-fetched profile)
(fetch-teams))
(ptk/reify ::logged-in
IDeref
(-deref [_] profile)
(->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id}))
(rx/delay 1000))
(when-not (get-in profile [:props :onboarding-viewed])
(->> (rx/of (modal/show {:type :onboarding}))
(rx/delay 1000))))
(rx/observe-on :async))))))
ptk/WatchEvent
(watch [_ _ _]
(when (is-authenticated? profile)
(->> (rx/of (profile-fetched profile)
(fetch-teams)
(get-redirect-event))
(rx/observe-on :async)))))))
(s/def ::login-params
(s/keys :req-un [::email ::password]))
(declare login-from-register)
(defn login
[{:keys [email password] :as data}]
(us/verify ::login-params data)
(ptk/reify ::login
ptk/WatchEvent
(watch [_ _ _]
(watch [_ _ stream]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)
params {:email email
:password password
:scope "webapp"}]
(->> (rx/timer 100)
(rx/mapcat #(rp/mutation :login params))
(rx/tap on-success)
(rx/catch on-error)
(rx/map (fn [profile]
(with-meta profile
{::ev/source "login"})))
(rx/map logged-in))))))
;; NOTE: We can't take the profile value from login because
;; there are cases when login is successfull but the cookie is
;; not set properly (because of possible misconfiguration).
;; So, we proceed to make an additional call to fetch the
;; profile, and ensure that cookie is set correctly. If
;; profile fetch is successful, we mark the user logged in, if
;; the returned profile is an NOT authenticated profile, we
;; proceed to logout and show an error message.
(rx/merge
(->> (rp/mutation :login params)
(rx/map fetch-profile)
(rx/catch on-error))
(->> stream
(rx/filter profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter (complement is-authenticated?))
(rx/tap on-error)
(rx/map #(ex/raise :type :authentication))
(rx/observe-on :async))
(->> stream
(rx/filter profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter is-authenticated?)
(rx/map (fn [profile]
(with-meta profile
{::ev/source "login"})))
(rx/tap on-success)
(rx/map logged-in)
(rx/observe-on :async)))))))
(defn login-from-token
[{:keys [profile] :as tdata}]
@ -221,44 +252,46 @@
(rx/map (fn [profile]
(with-meta profile
{::ev/source "register"})))
(rx/map logged-in))))))
(rx/map logged-in)
(rx/observe-on :async))))))
;; --- EVENT: logout
(defn logged-out
[]
(ptk/reify ::logged-out
ptk/UpdateEvent
(update [_ state]
(select-keys state [:route :router :session-id :history]))
([] (logged-out {}))
([_params]
(ptk/reify ::logged-out
ptk/UpdateEvent
(update [_ state]
(select-keys state [:route :router :session-id :history]))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/nav :auth-login)))
ptk/WatchEvent
(watch [_ _ _]
;; NOTE: We need the `effect` of the current event to be
;; executed before the redirect.
(->> (rx/of (rt/nav :auth-login))
(rx/observe-on :async)))
ptk/EffectEvent
(effect [_ _ _]
(reset! storage {})
(i18n/reset-locale))))
ptk/EffectEvent
(effect [_ _ _]
(reset! storage {})
(i18n/reset-locale)))))
(defn logout
[]
(ptk/reify ::logout
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation :logout)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map logged-out)))))
([] (logout {}))
([params]
(ptk/reify ::logout
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation :logout)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params)))))))
;; --- EVENT: register
;; TODO: remove
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::register
(s/keys :req-un [::fullname ::password ::email]
:opt-un [::invitation-token]))
(s/keys :req-un [::fullname ::password ::email]))
(defn register
"Create a register event instance."
@ -347,20 +380,33 @@
(rx/empty)))
(rx/ignore))))))
(defn mark-onboarding-as-viewed
([] (mark-onboarding-as-viewed nil))
([{:keys [version]}]
(ptk/reify ::mark-oboarding-as-viewed
ptk/WatchEvent
(watch [_ state _]
(watch [_ _ _]
(let [version (or version (:main @cf/version))
props (-> (get-in state [:profile :props])
(assoc :onboarding-viewed true)
(assoc :release-notes-viewed version))]
props {:onboarding-viewed true
:release-notes-viewed version}]
(->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile)))))))))
(defn mark-questions-as-answered
[]
(ptk/reify ::mark-questions-as-answered
ptk/UpdateEvent
(update [_ state]
(update-in state [:profile :props] assoc :onboarding-questions-answered true))
ptk/WatchEvent
(watch [_ _ _]
(let [props {:onboarding-questions-answered true}]
(->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile))))))))
;; --- Update Photo
(defn update-photo

View file

@ -13,6 +13,7 @@
[app.main.data.users :as du]
[app.main.sentry :as sentry]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[cljs.pprint :refer [pprint]]
@ -48,7 +49,9 @@
;; here and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication
[_]
(ts/schedule (st/emitf (du/logout))))
(let [msg (tr "errors.auth.unable-to-login")]
(st/emit! (du/logout {:capture-redirect true}))
(ts/schedule 500 (st/emitf (dm/warn msg)))))
;; That are special case server-errors that should be treated

View file

@ -6,6 +6,7 @@
(ns app.main.ui
(:require
[app.config :as cf]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.auth :refer [auth]]
@ -17,6 +18,8 @@
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs]
[app.main.ui.onboarding]
[app.main.ui.onboarding.questions]
[app.main.ui.releases]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings]
[app.main.ui.static :as static]
@ -32,7 +35,7 @@
(mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route] :as props}]
[{:keys [route profile]}]
(let [{:keys [data params]} route]
[:& (mf/provider ctx/current-route) {:value route}
(case (:name data)
@ -70,13 +73,32 @@
:dashboard-font-providers
:dashboard-team-members
:dashboard-team-settings)
[:*
#_[:div.modal-wrapper
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding/onboarding-team-modal]
]
[:& dashboard {:route route}]]
(when-let [props (some-> profile (get :props {}))]
(cond
(and cf/onboarding-form-id
(not (:onboarding-questions-answered props false))
(not (:onboarding-viewed props false)))
[:& app.main.ui.onboarding.questions/questions
{:profile profile
:form-id cf/onboarding-form-id}]
(not (:onboarding-viewed props))
[:& app.main.ui.onboarding/onboarding-modal {}]
(and (:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main @cf/version))
(not= "0.0" (:main @cf/version)))
[:& app.main.ui.releases/release-notes-modal {}]))
[:& dashboard {:route route :profile profile}]]
:viewer
(let [{:keys [query-params path-params]} route
@ -124,12 +146,14 @@
(mf/defc app
[]
(let [route (mf/deref refs/route)
edata (mf/deref refs/exception)]
(let [route (mf/deref refs/route)
edata (mf/deref refs/exception)
profile (mf/deref refs/profile)]
[:& (mf/provider ctx/current-route) {:value route}
(if edata
[:& static/exception-page {:data edata}]
[:*
[:& msgs/notifications]
(when route
[:& main-page {:route route}])])]))
[:& (mf/provider ctx/current-profile) {:value profile}
(if edata
[:& static/exception-page {:data edata}]
[:*
[:& msgs/notifications]
(when route
[:& main-page {:route route :profile profile}])])]]))

View file

@ -30,8 +30,7 @@
(mf/use-callback
(fn [_ _]
(reset! submitted false)
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login))))
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent")))))
on-error
(mf/use-callback

View file

@ -15,8 +15,9 @@
;; for text shapes in the export process
(def text-plain-colors-ctx (mf/create-context false))
(def current-route (mf/create-context nil))
(def current-team-id (mf/create-context nil))
(def current-route (mf/create-context nil))
(def current-profile (mf/create-context nil))
(def current-team-id (mf/create-context nil))
(def current-project-id (mf/create-context nil))
(def current-page-id (mf/create-context nil))
(def current-file-id (mf/create-context nil))
(def current-page-id (mf/create-context nil))
(def current-file-id (mf/create-context nil))

View file

@ -7,9 +7,7 @@
(ns app.main.ui.dashboard
(:require
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
@ -22,7 +20,6 @@
[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.util.timers :as tm]
[rumext.alpha :as mf]))
(defn ^boolean uuid-str?
@ -77,9 +74,8 @@
nil)])
(mf/defc dashboard
[{:keys [route] :as props}]
(let [profile (mf/deref refs/profile)
section (get-in route [:data :name])
[{:keys [route profile] :as props}]
(let [section (get-in route [:data :name])
params (parse-params route)
project-id (:project-id params)
@ -94,18 +90,8 @@
(mf/use-effect
(mf/deps team-id)
(st/emitf (dd/initialize {:id team-id})))
(mf/use-effect
(mf/deps)
(fn []
(let [props (:props profile)
version (:release-notes-viewed props)]
(when (and (:onboarding-viewed props)
(not= version (:main @cf/version))
(not= "0.0" (:main @cf/version)))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
:version (:main @cf/version)})))))))
(st/emit! (dd/initialize {:id team-id}))))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}

View file

@ -115,7 +115,7 @@
(st/emit! (dm/success (tr "dashboard.success-move-file"))))
(if (or navigate? (not= team-id current-team-id))
(st/emit! (dd/go-to-files team-id project-id))
(st/emit! (dd/fetch-recent-files)
(st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files))))
on-move

View file

@ -327,8 +327,9 @@
on-finish-import
(mf/use-callback
(mf/deps (:id team))
(fn []
(st/emit! (dd/fetch-recent-files)
(st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))
import-files (use-import-file project-id on-finish-import)
@ -366,7 +367,7 @@
on-drop-success
(fn []
(st/emit! (dm/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files)
(dd/fetch-recent-files (:id team))
(dd/clear-selected-files)))
on-drop

View file

@ -97,9 +97,10 @@
on-import
(mf/use-callback
(mf/deps (:id project) (:id team))
(fn []
(st/emit! (dd/fetch-files {:project-id (:id project)})
(dd/fetch-recent-files)
(dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))]
[:div.dashboard-project-row {:class (when first? "first")}
@ -163,15 +164,15 @@
(mf/use-effect
(mf/deps team)
(fn []
(when team
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(dom/set-html-title (tr "title.dashboard.projects" tname))))))
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(dom/set-html-title (tr "title.dashboard.projects" tname)))))
(mf/use-effect
(mf/deps (:id team))
(fn []
(st/emit! (dd/fetch-recent-files)
(st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))
(when (seq projects)

View file

@ -28,6 +28,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[goog.functions :as f]
[rumext.alpha :as mf]))
@ -287,27 +288,39 @@
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
on-rename-clicked
(st/emitf (modal/show :team-form {:team team}))
on-leaved-success
(fn []
(st/emit! (modal/hide)
(du/fetch-teams)))
leave-fn
(st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
leave-and-reassign-fn
(fn [member-id]
(let [params {:reassign-to member-id}]
(st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/leave-team (with-meta params {:on-success on-leaved-success})))))
delete-fn
on-success
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/delete-team (with-meta team {:on-success on-leaved-success}))))
(modal/hide)
(du/fetch-teams)))
on-error
(fn [{:keys [code] :as error}]
(condp = code
:no-enough-members-for-leave
(rx/of (dm/error (tr "errors.team-leave.insufficient-members")))
:member-does-not-exist
(rx/of (dm/error (tr "errors.team-leave.member-does-not-exists")))
:owner-cant-leave-team
(rx/of (dm/error (tr "errors.team-leave.owner-cant-leave")))
(rx/throw error)))
leave-fn
(fn [member-id]
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
(st/emit! (dd/leave-team (with-meta params
{:on-success on-success
:on-error on-error})))))
delete-fn
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error}))))
on-rename-clicked
(fn []
(st/emit! (modal/show :team-form {:team team})))
on-leave-clicked
(st/emitf (modal/show
@ -324,7 +337,7 @@
{:type ::leave-and-reassign
:profile profile
:team team
:accept leave-and-reassign-fn})))
:accept leave-fn})))
on-delete-clicked
(st/emitf
@ -501,7 +514,7 @@
[:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock]
[:span.text (tr "labels.password")]]
[:li {:on-click (partial on-click (du/logout))}
[:li {:on-click #(on-click (du/logout) %)}
[:span.icon i/exit]
[:span.text (tr "labels.logout")]]

View file

@ -6,32 +6,16 @@
(ns app.main.ui.onboarding
(:require
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[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 :as fm]
[app.main.ui.icons :as i]
[app.main.ui.onboarding.questions]
[app.main.ui.onboarding.team-choice]
[app.main.ui.onboarding.templates]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
[app.main.ui.releases.v1-7]
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.timers :as tm]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
;; --- ONBOARDING LIGHTBOX
@ -189,297 +173,3 @@
:slide @slide
:navigate navigate
:skip skip)))]]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-choice-modal
{::mf/register modal/components
::mf/register-as :onboarding-choice}
[]
(let [;; When user choices the option of `fly solo`, we proceed to show
;; the onboarding templates modal.
on-fly-solo
(fn []
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
on-team-up
(fn []
(st/emit! (modal/show {:type :onboarding-team})))
]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
[:h1 (tr "onboarding.choice.title")]
[:p (tr "onboarding.choice.desc")]]
[:div.modal-columns
[:div.modal-left
[:div.content-button {:on-click on-fly-solo}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
[:div.content-button {:on-click on-team-up}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [form (fm/use-form :spec ::team-form
:initial {})
on-submit
(mf/use-callback
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.team-row
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team-input-placeholder")}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.next")}]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-form
(s/keys :req-un [::role ::email]))
;; This is the final step of team creation, consists in provide a
;; shortcut for invite users.
(mf/defc onboarding-team-invitations-modal
{::mf/register modal/components
::mf/register-as :onboarding-team-invitations}
[{:keys [name] :as props}]
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name}))
form (fm/use-form :spec ::invite-form
:initial initial)
roles (mf/use-memo #(get-available-roles))
on-success
(mf/use-callback
(fn [_form response]
(let [project-id (:default-project-id response)
team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/show {:type :onboarding-templates
:project-id project-id}))))))
on-error
(mf/use-callback
(fn [_form _response]
(st/emit! (dm/error "Error on creating team."))))
;; The SKIP branch only creates the team, without invitations
on-skip
(mf/use-callback
(fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))))))
;; The SUBMIT branch creates the team with the invitations
on-submit
(mf/use-callback
(fn [form _]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params (:clean-data @form)]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.invite-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.create")}]]
[:div.skip-action
{:on-click on-skip}
[:div.action "Skip and invite later"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc cf/public-uri :path path))
on-finish-import
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-recent-files)
(dd/clear-selected-files)))
open-import-modal
(fn [file]
(st/emit! (modal/show
{:type :import
:project-id project-id
:files [file]
:on-finish-import on-finish-import})))
on-click
(fn []
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []
(reset! downloading? false)))))
]
[:div.template-item
[:div.template-item-content
[:img {:src image}]]
[:div.template-item-title
[:div.label name]
(if @downloading?
[:div.action "Fetching..."]
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
(mf/defc onboarding-templates-modal
{::mf/register modal/components
::mf/register-as :onboarding-templates}
;; NOTE: the project usually comes empty, it only comes fullfilled
;; when a user creates a new team just after signup.
[{:keys [project-id] :as props}]
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
profile (mf/deref refs/profile)
project-id (or project-id (:default-project-id profile))]
[:div.modal-overlay
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
{:on-click close-fn} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
[:p (tr "onboarding.templates.subtitle")]
[:div.templates
[:& template-item
{:path "/github/penpot-files/Penpot-Design-system.penpot"
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:name "Penpot Design System"
:project-id project-id}]
[:& template-item
{:path "/github/penpot-files/Material-Design-Kit.penpot"
:image "https://penpot.app/images/libraries/cover-material.jpg"
:name "Material Design Kit"
:project-id project-id}]]]]]))
;;; --- RELEASE NOTES MODAL
(mf/defc release-notes
[{:keys [version] :as props}]
(let [slide (mf/use-state :start)
klass (mf/use-state "fadeInDown")
navigate
(mf/use-callback #(reset! slide %))
next
(mf/use-callback
(mf/deps slide)
(fn []
(if (= @slide :start)
(navigate 0)
(navigate (inc @slide)))))
finish
(mf/use-callback
(st/emitf (modal/hide)
(du/mark-onboarding-as-viewed {:version version})))
]
(mf/use-effect
(mf/deps)
(fn []
(st/emitf (du/mark-onboarding-as-viewed {:version version}))))
(mf/use-layout-effect
(mf/deps @slide)
(fn []
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem)))))
(rc/render-release-notes
{:next next
:navigate navigate
:finish finish
:klass klass
:slide slide
:version version})))
(mf/defc release-notes-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :release-notes}
[props]
(let [versions (methods rc/render-release-notes)
version (obj/get props "version")]
(when (contains? versions version)
[:div.relnotes
[:> release-notes props]])))
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "1.10")))

View file

@ -0,0 +1,48 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.onboarding.questions
"External form for onboarding questions."
(:require
[app.main.data.users :as du]
[app.main.store :as st]
[app.util.dom :as dom]
[goog.events :as ev]
[promesa.core :as p]
[rumext.alpha :as mf]))
(defn load-arengu-sdk
[container-ref email form-id]
(letfn [(on-init []
(when-let [container (mf/ref-val container-ref)]
(-> (.embed js/ArenguForms form-id container)
(p/then (fn [form]
(.setHiddenField ^js form "email" email))))))
(on-submit-success [_event]
(st/emit! (du/mark-questions-as-answered)))
]
(let [script (dom/create-element "script")
head (unchecked-get js/document "head")
lkey1 (ev/listen js/document "af-submitForm-success" on-submit-success)]
(unchecked-set script "src" "https://sdk.arengu.com/forms.js")
(unchecked-set script "onload" on-init)
(dom/append-child! head script)
(fn []
(ev/unlistenByKey lkey1)))))
(mf/defc questions
[{:keys [profile form-id]}]
(let [container (mf/use-ref)]
(mf/use-effect (partial load-arengu-sdk container (:email profile) form-id))
[:div.modal-wrapper.questions-form
[:div.modal-overlay
[:div.modal-container {:ref container}]]]))

View file

@ -0,0 +1,181 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.onboarding.team-choice
(:require
[app.common.spec :as us]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as tm]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-choice-modal
{::mf/register modal/components
::mf/register-as :onboarding-choice}
[]
(let [;; When user choices the option of `fly solo`, we proceed to show
;; the onboarding templates modal.
on-fly-solo
(fn []
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
on-team-up
(fn []
(st/emit! (modal/show {:type :onboarding-team})))
]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
[:h1 (tr "onboarding.welcome.title")]
[:p (tr "onboarding.welcome.desc3")]]
[:div.modal-columns
[:div.modal-left
[:div.content-button {:on-click on-fly-solo}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
[:div.content-button {:on-click on-team-up}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [form (fm/use-form :spec ::team-form
:initial {})
on-submit
(mf/use-callback
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.team-row
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team-input-placeholder")}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.next")}]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-form
(s/keys :req-un [::role ::email]))
;; This is the final step of team creation, consists in provide a
;; shortcut for invite users.
(mf/defc onboarding-team-invitations-modal
{::mf/register modal/components
::mf/register-as :onboarding-team-invitations}
[{:keys [name] :as props}]
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name}))
form (fm/use-form :spec ::invite-form
:initial initial)
roles (mf/use-memo #(get-available-roles))
on-success
(mf/use-callback
(fn [_form response]
(let [project-id (:default-project-id response)
team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/show {:type :onboarding-templates
:project-id project-id}))))))
on-error
(mf/use-callback
(fn [_form _response]
(st/emit! (dm/error "Error on creating team."))))
;; The SKIP branch only creates the team, without invitations
on-skip
(mf/use-callback
(fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))))))
;; The SUBMIT branch creates the team with the invitations
on-submit
(mf/use-callback
(fn [form _]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params (:clean-data @form)]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.invite-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.create")}]]
[:div.skip-action
{:on-click on-skip}
[:div.action "Skip and invite later"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))

View file

@ -0,0 +1,88 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.onboarding.templates
(:require
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[beicon.core :as rx]
[rumext.alpha :as mf]))
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc cf/public-uri :path path))
on-finish-import
(fn []
(st/emit! (dd/fetch-recent-files)))
open-import-modal
(fn [file]
(st/emit! (modal/show
{:type :import
:project-id project-id
:files [file]
:on-finish-import on-finish-import})))
on-click
(fn []
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []
(reset! downloading? false)))))
]
[:div.template-item
[:div.template-item-content
[:img {:src image}]]
[:div.template-item-title
[:div.label name]
(if @downloading?
[:div.action "Fetching..."]
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
(mf/defc onboarding-templates-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :onboarding-templates}
;; NOTE: the project usually comes empty, it only comes fullfilled
;; when a user creates a new team just after signup.
[{:keys [project-id] :as props}]
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
profile (mf/deref refs/profile)
project-id (or project-id (:default-project-id profile))]
[:div.modal-overlay
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
{:on-click close-fn} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
[:p (tr "onboarding.templates.subtitle")]
[:div.templates
[:& template-item
{:path "/github/penpot-files/Penpot-Design-system.penpot"
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:name "Penpot Design System"
:project-id project-id}]
[:& template-item
{:path "/github/penpot-files/Material-Design-Kit.penpot"
:image "https://penpot.app/images/libraries/cover-material.jpg"
:name "Material Design Kit"
:project-id project-id}]]]]]))

View file

@ -0,0 +1,83 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.releases
(:require
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
[app.main.ui.releases.v1-7]
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.alpha :as mf]))
;;; --- RELEASE NOTES MODAL
(mf/defc release-notes
[{:keys [version] :as props}]
(let [slide (mf/use-state :start)
klass (mf/use-state "fadeInDown")
navigate
(mf/use-callback #(reset! slide %))
next
(mf/use-callback
(mf/deps slide)
(fn []
(if (= @slide :start)
(navigate 0)
(navigate (inc @slide)))))
finish
(mf/use-callback
(st/emitf (modal/hide)
(du/mark-onboarding-as-viewed {:version version})))
]
(mf/use-effect
(mf/deps)
(fn []
(st/emitf (du/mark-onboarding-as-viewed {:version version}))))
(mf/use-layout-effect
(mf/deps @slide)
(fn []
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem)))))
(rc/render-release-notes
{:next next
:navigate navigate
:finish finish
:klass klass
:slide slide
:version version})))
(mf/defc release-notes-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :release-notes}
[props]
(let [versions (methods rc/render-release-notes)
version (obj/get props "version")]
(when (contains? versions version)
[:div.relnotes
[:> release-notes props]])))
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "1.10")))

View file

@ -6,10 +6,9 @@
(ns app.main.ui.static
(:require
[app.main.data.users :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.globals :as globals]
[app.util.i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
@ -19,14 +18,7 @@
{::mf/wrap-props false}
[props]
(let [children (obj/get props "children")
on-click (mf/use-callback
(fn []
(let [profile (deref refs/profile)]
(if (du/is-authenticated? profile)
(let [team-id (du/get-current-team-id profile)]
(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))
(st/emit! (rt/nav :auth-login {}))))))]
on-click (mf/use-callback #(set! (.-href globals/location) ""))]
[:section.exception-layout
[:div.exception-header
{:on-click on-click}

View file

@ -166,7 +166,7 @@
(defn append-child!
[el child]
(.appendChild el child))
(.appendChild ^js el child))
(defn get-first-child
[el]

View file

@ -37,10 +37,16 @@
[& {:keys [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))]
get-state (mf/use-callback
(mf/deps initial)
(fn []
{:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}}))
state-ref (mf/use-ref (get-state))
form (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]
(mf/use-effect
(mf/deps initial)
@ -72,7 +78,7 @@
(not= cleaned ::s/invalid))))))
(defn- create-form-mutator
[state-ref render opts]
[state-ref render get-state opts]
(reify
IDeref
(-deref [_]
@ -80,7 +86,9 @@
IReset
(-reset! [it new-value]
(mf/set-ref-val! state-ref new-value)
(if (nil? new-value)
(mf/set-ref-val! state-ref (get-state))
(mf/set-ref-val! state-ref new-value))
(render inc))
ISwap

View file

@ -88,6 +88,7 @@
:credentials credentials
:referrerPolicy "no-referrer"
:signal signal}]
(-> (js/fetch (str uri) params)
(p/then (fn [response]
(vreset! abortable? false)

View file

@ -19,17 +19,16 @@
;; --- Router API
(defn map->Match
[data]
(r/map->Match data))
(defn resolve
([router id] (resolve router id {} {}))
([router id path-params] (resolve router id path-params {}))
([router id path-params query-params]
(when-let [match (r/match-by-name router id path-params)]
(if (empty? query-params)
(r/match->path match)
(let [query (u/map->query-string query-params)]
(-> (u/uri (r/match->path match))
(assoc :query query)
(str)))))))
(r/match->path match query-params))))
(defn create
[routes]
@ -161,7 +160,3 @@
(e/unlistenByKey key)))))
(rx/take-until stoper)
(rx/subs #(on-change router %)))))))

View file

@ -3245,4 +3245,16 @@ msgid "workspace.updates.update"
msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"
msgstr "Click to close the path"
msgid "errors.team-leave.member-does-not-exists"
msgstr "The member you try to assign does not exist."
msgid "errors.team-leave.owner-cant-leave"
msgstr "Owner can't leave team, you must reassign the owner role."
msgid "errors.team-leave.insufficient-members"
msgstr "Insufficient members to leave team, you probably want to delete it."
msgid "errors.auth.unable-to-login"
msgstr "Looks like you are not authenticated or session expired."