🎉 New oops page with login and request access

This commit is contained in:
Pablo Alba 2024-06-27 11:00:31 +02:00
parent d2311f066a
commit 6169f5c2e8
46 changed files with 4117 additions and 134 deletions

View file

@ -155,3 +155,18 @@
:files files
:binary? binary?}))))))))
;;;;;;;;;;;;;;;;;;;;;;
;; Team Request
;;;;;;;;;;;;;;;;;;;;;;
(defn create-team-access-request
[params]
(ptk/reify ::create-team-access-request
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :create-team-access-request params)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -153,13 +153,8 @@
accepting invitation, or third party auth signup or singin."
[profile]
(letfn [(get-redirect-event []
(let [team-id (get-current-team-id profile)
redirect-url (:redirect-url @storage)]
(if (some? redirect-url)
(do
(swap! storage dissoc :redirect-url)
(.replace js/location redirect-url))
(rt/nav' :dashboard-projects {:team-id team-id}))))]
(let [team-id (get-current-team-id profile)]
(rt/nav' :dashboard-projects {:team-id team-id})))]
(ptk/reify ::logged-in
ev/Event
@ -316,7 +311,6 @@
ptk/EffectEvent
(effect [_ _ _]
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
(swap! storage dissoc :redirect-url)
(set-current-team! nil)))))
(defn logout

View file

@ -17,7 +17,6 @@
[app.util.globals :as glob]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :refer [storage]]
[app.util.timers :as ts]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
@ -96,16 +95,23 @@
(print-trace! error)
(print-data! error))))
;; We receive a explicit authentication error; this explicitly clears
;; We receive a explicit authentication error;
;; If the uri is for workspace, dashboard or view assign the
;; exception for the 'Oops' page. Otherwise this explicitly clears
;; all profile data and redirect the user to the login page. This is
;; here and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication
[_]
(let [msg (tr "errors.auth.unable-to-login")
uri (. (. js/document -location) -href)]
(st/emit! (du/logout {:capture-redirect true}))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))
(ts/schedule 1000 #(swap! storage assoc :redirect-url uri))))
[e]
(let [msg (tr "errors.auth.unable-to-login")
uri (.-href glob/location)
show-oops? (or (str/includes? uri "workspace")
(str/includes? uri "dashboard")
(str/includes? uri "view"))]
(if show-oops?
(st/async-emit! (rt/assign-exception e))
(do
(st/emit! (du/logout {:capture-redirect true}))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
;; Error that happens on an active business model validation does not
;; passes an validation (example: profile can't leave a team). From

View file

@ -133,7 +133,7 @@
[:& dashboard-page {:route route :profile profile}]]
:viewer
(let [{:keys [query-params path-params]} route
{:keys [index share-id section page-id interactions-mode frame-id]
{:keys [index share-id section page-id interactions-mode frame-id share]
:or {section :interactions interactions-mode :show-on-click}} query-params
{:keys [file-id]} path-params]
[:? {}
@ -154,7 +154,8 @@
:hide false
:show true
:show-on-click false)
:frame-id frame-id}])])
:frame-id frame-id
:share share}])])
:workspace
(let [project-id (some-> params :path :project-id uuid)

View file

@ -14,31 +14,12 @@
[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 register-success-page register-validate-page]]
[app.main.ui.auth.register :refer [register-page register-success-page register-validate-page terms-register]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc terms-login
[]
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
show-terms? (some? cf/terms-of-service-uri)
show-privacy? (some? cf/privacy-policy-uri)]
(when show-all?
[:div {:class (stl/css :terms-login)}
(when show-terms?
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
(tr "auth.terms-of-service")])
(when show-all?
[:span {:class (stl/css :and-text)}
(dm/str " " (tr "labels.and") " ")])
(when show-privacy?
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
(tr "auth.privacy-policy")])])))
(mf/defc auth
{::mf/props :obj}
@ -90,4 +71,4 @@
[:& recovery-page {:params params}])
(when (= section :auth-register)
[:& terms-login])]]))
[:& terms-register])]]))

View file

@ -103,23 +103,3 @@
fill: var(--main-icon-foreground);
}
}
.terms-login {
@include bodySmallTypography;
display: flex;
gap: $s-4;
justify-content: center;
width: 100%;
}
.and-text {
border-bottom: $s-1 solid transparent;
color: var(--title-foreground-color);
}
.auth-link {
color: var(--link-foreground-color);
&:hover {
text-decoration: underline;
}
}

View file

@ -71,7 +71,7 @@
[:string {:min 1}]]])
(mf/defc login-form
[{:keys [params on-success-callback origin] :as props}]
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
(let [initial (mf/with-memo [params] params)
error (mf/use-state false)
form (fm/use-form :schema schema:login-form
@ -139,9 +139,12 @@
:on-success on-success})]
(st/emit! (du/login-with-ldap params)))))
on-recovery-request
default-recovery-req
(mf/use-fn
#(st/emit! (rt/nav :auth-recovery-request)))]
#(st/emit! (rt/nav :auth-recovery-request)))
on-recovery-request (or on-recovery-request
default-recovery-req)]
[:*
(when-let [message @error]
@ -243,7 +246,7 @@
(tr "auth.login-with-oidc-submit")])))
(mf/defc login-methods
[{:keys [params on-success-callback origin] :as props}]
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
[:*
(when show-alt-login-buttons?
[:*
@ -257,7 +260,7 @@
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password)
(contains? cf/flags :login-with-ldap))
[:& login-form {:params params :on-success-callback on-success-callback :origin origin}])])
[:& login-form {:params params :on-success-callback on-success-callback :on-recovery-request on-recovery-request :origin origin}])])
(mf/defc login-page
[{:keys [params] :as props}]

View file

@ -102,3 +102,16 @@
:class (stl/css :go-back-link)
:data-testid "go-back-link"}
(tr "labels.go-back")]]]))
(mf/defc recovery-sent-page
{::mf/props :obj}
[{:keys [email]}]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)}
(tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]]
[:div {:class (stl/css :notification-text-email)} email]
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]])

View file

@ -10,3 +10,10 @@
.fields-row {
margin-bottom: $s-8;
}
.notification-text-email {
@include medTitleTipography;
font-size: $fs-20;
color: var(--register-confirmation-color);
margin-inline: $s-36;
}

View file

@ -7,6 +7,7 @@
(ns app.main.ui.auth.register
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.config :as cf]
[app.main.data.notifications :as ntf]
@ -103,11 +104,12 @@
(mf/defc register-methods
{::mf/props :obj}
[{:keys [params on-success-callback]}]
[{:keys [params hide-separator on-success-callback]}]
[:*
(when login/show-alt-login-buttons?
[:& login/login-buttons {:params params}])
[:hr {:class (stl/css :separator)}]
(when (or login/show-alt-login-buttons? (false? hide-separator))
[:hr {:class (stl/css :separator)}])
[:& register-form {:params params :on-success-callback on-success-callback}]])
(mf/defc register-page
@ -251,14 +253,37 @@
(mf/defc register-success-page
{::mf/props :obj}
[]
(let [email (::email @sto/storage)]
[{:keys [params]}]
(let [email (or (:email params) (::email @sto/storage))]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
(when-not (:hide-logo params)
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)}
(tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
[:div {:class (stl/css :notification-text-email)} email]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]))
(mf/defc terms-register
[]
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
show-terms? (some? cf/terms-of-service-uri)
show-privacy? (some? cf/privacy-policy-uri)]
(when show-all?
[:div {:class (stl/css :terms-register)}
(when show-terms?
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
(tr "auth.terms-of-service")])
(when show-all?
[:span {:class (stl/css :and-text)}
(dm/str " " (tr "labels.and") " ")])
(when show-privacy?
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
(tr "auth.privacy-policy")])])))

View file

@ -66,3 +66,23 @@
width: $s-120;
margin-block-end: $s-24;
}
.terms-register {
@include bodySmallTypography;
display: flex;
gap: $s-4;
justify-content: center;
width: 100%;
}
.and-text {
border-bottom: $s-1 solid transparent;
color: var(--title-foreground-color);
}
.auth-link {
color: var(--link-foreground-color);
&:hover {
text-decoration: underline;
}
}

View file

@ -420,7 +420,7 @@
(into [] (distinct) (conj coll item)))
(mf/defc multi-input
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}]
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit invite-email] :as props}]
(let [form (or form (mf/use-ctx form-ctx))
input-name (get props :name)
touched? (get-in @form [:touched input-name])
@ -528,6 +528,12 @@
values (filterv #(:valid %) values)]
(update-form! values)))
(mf/with-effect []
(when invite-email
(swap! items conj-dedup {:text (str/trim invite-email)
:valid (valid-item-fn invite-email)
:caution (caution-item-fn invite-email)})))
[:div {:class klass}
[:input {:id (name input-name)
:class in-klass

View file

@ -52,7 +52,7 @@
(assoc :project-id (uuid project-id)))))
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
[{:keys [team projects project section search-term profile invite-email] :as props}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)
project-id (:id project)
@ -129,7 +129,7 @@
[:& libraries-page {:team team}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile}]
[:& team-members-page {:team team :profile profile :invite-email invite-email}]
:dashboard-team-invitations
[:& team-invitations-page {:team team}]
@ -153,6 +153,7 @@
project-id (:project-id params)
team-id (:team-id params)
search-term (:search-term params)
invite-email (-> route :query-params :invite-email)
teams (mf/deref refs/teams)
team (get teams team-id)
@ -204,5 +205,6 @@
:project project
:section section
:search-term search-term
:team team}])])]]))
:team team
:invite-email invite-email}])])]]))

View file

@ -61,7 +61,7 @@
(mf/defc header
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [section team]}]
[{:keys [section team invite-email]}]
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
@ -79,7 +79,12 @@
(fn []
(st/emit! (modal/show {:type :invite-members
:team team
:origin :team}))))]
:origin :team
:invite-email invite-email}))))]
(mf/with-effect []
(when invite-email
(on-invite-member)))
[:header {:class (stl/css :dashboard-header :team) :data-testid "dashboard-header"}
[:div {:class (stl/css :dashboard-title)}
@ -141,7 +146,7 @@
{::mf/register modal/components
::mf/register-as :invite-members
::mf/wrap-props false}
[{:keys [team origin]}]
[{:keys [team origin invite-email]}]
(let [members-map (mf/deref refs/dashboard-team-members)
perms (:permissions team)
@ -192,7 +197,8 @@
:on-error (partial on-error form)}]
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
(with-meta {::ev/origin origin}))
(dd/fetch-team-invitations))))]
(dd/fetch-team-invitations)
(dd/fetch-team-members (:id team)))))]
[:div {:class (stl/css-case :modal-team-container true
@ -223,7 +229,8 @@
:valid-item-fn us/parse-email
:caution-item-fn current-members-emails
:label (tr "modals.invite-member.emails")
:on-submit on-submit}]]
:on-submit on-submit
:invite-email invite-email}]]
[:div {:class (stl/css :action-buttons)}
[:> fm/submit-button*
@ -497,7 +504,7 @@
(mf/defc team-members-page
{::mf/wrap-props false}
[{:keys [team profile]}]
[{:keys [team profile invite-email]}]
(let [members-map (mf/deref refs/dashboard-team-members)]
(mf/with-effect [team]
@ -511,7 +518,7 @@
(st/emit! (dd/fetch-team-members (:id team))))
[:*
[:& header {:section :dashboard-team-members :team team}]
[:& header {:section :dashboard-team-members :team team :invite-email invite-email}]
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
[:& team-members
{:profile profile

View file

@ -10,32 +10,50 @@
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.main.data.common :as dc]
[app.main.data.events :as ev]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-methods]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.header :as header]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(mf/defc error-container
{::mf/wrap-props false}
[{:keys [children]}]
(let [on-click (mf/use-callback #(set! (.-href globals/location) "/"))]
(let [profile-id (:profile-id @st/state)]
[:section {:class (stl/css :exception-layout)}
[:button
{:class (stl/css :exception-header)
:on-click on-click}
i/logo-icon]
:on-click rt/nav-root}
i/logo-icon
(when profile-id
(str "< "
(tr "not-found.no-permission.go-dashboard")))]
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
(when-not profile-id
[:button {:class (stl/css :login-header)
:on-click rt/nav-root}
(tr "labels.login")])
[:div {:class (stl/css :exception-content)}
[:div {:class (stl/css :container)} children]]
[:div {:class (stl/css :deco-after)} i/logo-error-screen]]))
[:div {:class (stl/css :deco-after2)}
[:span (tr "labels.copyright")]
i/logo-error-screen
[:span (tr "not-found.made-with-love")]]]))
(mf/defc invalid-token
[]
@ -43,16 +61,221 @@
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
(mf/defc login-dialog
{::mf/props :obj}
[{:keys [show-dialog]}]
(let [current-section (mf/use-state :login)
user-email (mf/use-state "")
register-token (mf/use-state "")
set-section
(mf/use-fn
(fn [event]
(let [section (-> (dom/get-current-target event)
(dom/get-data "section")
(keyword))]
(reset! current-section section))))
set-section-recovery
(mf/use-fn
#(reset! current-section :recovery-request))
set-section-login
(mf/use-fn
#(reset! current-section :login))
success-login
(fn []
(reset! show-dialog false)
(.reload js/window.location true))
success-register
(fn [data]
(reset! register-token (:token data))
(reset! current-section :register-validate))
register-email-sent
(fn [email]
(reset! user-email email)
(reset! current-section :register-email-sent))
recovery-email-sent
(fn [email]
(reset! user-email email)
(reset! current-section :recovery-email-sent))]
[:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog-login)}
[:div {:class (stl/css :modal-close)}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
i/close]]
[:div {:class (stl/css :login)}
[:div {:class (stl/css :logo)} i/logo]
(case @current-section
:login
[:*
[:div {:class (stl/css :logo-title)} (tr "labels.login")]
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
[:& login-methods {:on-recovery-request set-section-recovery
:on-success-callback success-login}]
[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :change-section)}
(tr "auth.register")
" "
[:a {:data-section "register"
:on-click set-section} (tr "auth.register-submit")]]]
:register
[:*
[:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
[:& register-methods {:on-success-callback success-register :hide-separator true}]
#_[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :separator)}]
[:div {:class (stl/css :change-section)}
(tr "auth.already-have-account")
" "
[:a {:data-section "login"
:on-click set-section} (tr "auth.login-here")]]
[:div {:class (stl/css :links)}
[:hr {:class (stl/css :separator)}]
[:& terms-register]]]
:register-validate
[:div {:class (stl/css :form-container)}
[:& register-validate-form {:params {:token @register-token}
:on-success-callback register-email-sent}]
[:div {:class (stl/css :links)}
[:div {:class (stl/css :register)}
[:a {:data-section "register"
:on-click set-section}
(tr "labels.go-back")]]]]
:register-email-sent
[:div {:class (stl/css :form-container)}
[:& register-success-page {:params {:email @user-email :hide-logo true}}]]
:recovery-request
[:& recovery-request-page {:go-back-callback set-section-login
:on-success-callback recovery-email-sent}]
:recovery-email-sent
[:div {:class (stl/css :form-container)}
[:& recovery-sent-page {:email @user-email}]])]]]))
(mf/defc request-dialog
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text]}]
(let [on-click (or on-button-click rt/nav-root)]
[:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog)}
[:div {:class (stl/css :modal-close)}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
i/close]]
[:div {:class (stl/css :dialog-title)} title]
(for [txt content]
[:div txt])
[:div {:class (stl/css :sign-info)}
(when cancel-text
[:button {:class (stl/css :cancel-button) :on-click rt/nav-root} cancel-text])
[:button {:on-click on-click} button-text]]]]))
(mf/defc request-access
{::mf/props :obj}
[{:keys [file-id team-id is-default workspace?]}]
(let [profile (:profile @st/state)
requested* (mf/use-state {:sent false :already-requested false})
requested (deref requested*)
show-dialog (mf/use-state true)
on-success
(mf/use-fn
#(reset! requested* {:sent true :already-requested false}))
on-error
(mf/use-fn
#(reset! requested* {:sent true :already-requested true}))
on-request-access
(mf/use-fn
(mf/deps file-id team-id workspace?)
(fn []
(let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id})
mdata {:on-success on-success :on-error on-error}]
(st/emit! (dc/create-team-access-request (with-meta params mdata))))))]
[:*
(if (some? file-id)
(if workspace?
[:div {:class (stl/css :workspace)}
[:div {:class (stl/css :workspace-left)}
i/logo-icon
[:div
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
[:div {:class (stl/css :workspace-right)}]]
[:div {:class (stl/css :viewer)}
[:& header/header {:project {:name (tr "not-found.no-permission.project-name")}
:index 0
:file {:name (tr "not-found.no-permission.penpot-file")}
:page nil
:frame nil
:permissions {:is-logged true}
:zoom 1
:section :interactions
:shown-thumbnails false
:interactions-mode nil}]])
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
[:& sidebar
{:team nil
:projects []
:project (:default-project-id profile)
:profile profile
:section :dashboard-projects
:search-term ""}]]])
(when @show-dialog
(cond
(nil? profile)
[:& login-dialog {:show-dialog show-dialog}]
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") :content [(tr "not-found.no-permission.already-requested.or-others.file")] :button-text (tr "not-found.no-permission.go-dashboard")}]
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") :content [(tr "not-found.no-permission.already-requested.or-others.project")] :button-text (tr "not-found.no-permission.go-dashboard")}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success") :content [(tr "not-found.no-permission.done.remember")] :button-text (tr "not-found.no-permission.go-dashboard")}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file") :content [(tr "not-found.no-permission.you-can-ask.file") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project") :content [(tr "not-found.no-permission.you-can-ask.project") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
(mf/defc not-found
[]
[:> error-container {}
[:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.not-found.desc-message")]])
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")]
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]])
(mf/defc bad-gateway
[]
(let [handle-retry
(mf/use-callback
(mf/use-fn
(fn [] (st/emit! (rt/assign-exception nil))))]
[:> error-container {}
[:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")]
@ -150,13 +373,49 @@
(mf/defc exception-page
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [type (:type data)
path (:path route)
query-params (u/map->query-string (:query-params route))]
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
(let [file-info (mf/use-state {:pending true})
team-info (mf/use-state {:pending true})
type (:type data)
path (:path route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
request-access? (and
(or workspace? dashboard? view?)
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info)))))
query-params (u/map->query-string (:query-params route))
pparams (:path-params route)
on-file-info (mf/use-fn
(fn [info]
(reset! file-info {:file-id (:id info)})))
on-team-info (mf/use-fn
(fn [info]
(reset! team-info {:team-id (:id info) :is-default (:is-default info)})))]
(mf/with-effect [type path query-params pparams @file-info @team-info]
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
(when (and (:file-id pparams) (:pending @file-info))
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)})
(rx/subs! on-file-info)))
(when (and (:team-id pparams) (:pending @team-info))
(->> (rp/cmd! :get-team-info {:id (:team-id pparams)})
(rx/subs! on-team-info))))
(case (:type data)
:not-found
[:& not-found]
(if request-access?
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}]
[:& not-found])
:authentication
(if request-access?
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}]
[:& not-found])
:bad-gateway
[:& bad-gateway]

View file

@ -28,6 +28,7 @@
.deco-before {
height: 34vh;
top: 0;
svg {
bottom: 0;
}
@ -36,17 +37,52 @@
.deco-after {
height: 34vh;
bottom: 0;
svg {
top: 0;
}
}
.deco-after2 {
display: flex;
justify-content: center;
gap: $s-8;
width: 100%;
height: 34vh;
position: absolute;
left: 0;
bottom: 0;
color: var(--color-foreground-primary);
svg {
fill: var(--color-foreground-secondary);
height: 1537px;
width: $s-80;
}
span {
display: flex;
flex-direction: column;
justify-content: flex-end;
height: 100%;
width: 25%;
&:first-child {
text-align: right;
}
}
}
.exception-header {
color: var(--color-foreground-secondary);
padding: $s-24 $s-32;
position: fixed;
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
svg {
fill: var(--color-foreground-primary);
width: $s-48;
@ -54,6 +90,15 @@
}
}
.login-header {
@extend .button-primary;
padding: $s-8 $s-16;
font-size: $fs-11;
position: fixed;
right: 0;
margin: $s-40 $s-32;
}
.exception-content {
display: flex;
height: 100%;
@ -85,6 +130,7 @@
.sign-info {
text-align: center;
button {
@extend .button-primary;
text-transform: uppercase;
@ -98,3 +144,180 @@
fill: var(--color-foreground-primary);
}
}
.workspace {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-color: var(--color-canvas);
position: relative;
.workspace-left,
.workspace-right {
padding: $s-12;
width: $s-276;
height: 100%;
background-color: var(--color-background-primary);
display: flex;
gap: $s-4;
svg {
width: 2rem;
height: 2rem;
fill: var(--icon-foreground-hover);
}
.project-name {
@include uppercaseTitleTipography;
color: var(--title-foreground-color);
}
.file-name {
@include smallTitleTipography;
text-transform: none;
color: var(--title-foreground-color-hover);
}
}
}
.dashboard {
width: 100%;
height: 100%;
.dashboard-sidebar {
width: $s-300;
height: 100%;
}
}
.viewer {
width: 100%;
height: 100%;
}
.overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 100;
background-color: rgba(0, 0, 0, 0.65);
display: flex;
justify-content: center;
align-items: center;
.dialog,
.dialog-login {
width: 556px;
background-color: var(--color-background-primary);
border-radius: $s-8;
display: flex;
flex-direction: column;
align-content: stretch;
padding: $s-36;
color: var(--modal-text-foreground-color);
.modal-close {
text-align: right;
.modal-close-button {
background: none;
border: none;
cursor: pointer;
svg {
cursor: pointer;
width: $s-24;
height: $s-24;
fill: var(--modal-text-foreground-color);
stroke: var(--modal-text-foreground-color);
}
}
}
.dialog-title {
font-size: $fs-20;
}
.sign-info {
display: flex;
justify-content: flex-end;
margin-top: $s-32;
button {
@extend .button-primary;
text-transform: uppercase;
padding: $s-8 $s-16;
font-size: $fs-11;
}
.cancel-button {
@extend .button-secondary;
text-transform: uppercase;
padding: $s-8 $s-16;
font-size: $fs-11;
margin-right: $s-16;
}
}
}
.dialog {
gap: $s-12;
}
.login {
gap: 0;
padding: 0 $s-36 $s-72 $s-36;
.logo {
margin-bottom: $s-40;
svg {
fill: var(--color-foreground-primary);
width: $s-120;
height: $s-40;
}
}
.logo-title {
font-size: $fs-20;
color: var(--title-foreground-color-hover);
margin-bottom: $s-4;
}
.logo-subtitle {
font-size: $fs-14;
color: var(--title-foreground-color-hover);
margin-bottom: $s-24;
}
.change-section {
width: 100%;
text-align: center;
a {
color: var(--link-foreground-color);
}
}
hr {
margin: $s-20 0;
border-top: solid 1px var(--modal-separator-backogrund-color);
}
.separator {
margin: $s-20 0;
}
form div {
margin-bottom: $s-8;
}
}
}
.login-container {
width: 100%;
background-color: red;
}

View file

@ -276,7 +276,7 @@
(mf/defc viewer-content
{::mf/wrap-props false}
[{:keys [data page-id share-id section index interactions-mode] :as props}]
[{:keys [data page-id share-id section index interactions-mode share] :as props}]
(let [{:keys [file users project permissions]} data
allowed (or
(= section :interactions)
@ -615,7 +615,8 @@
:zoom zoom
:section section
:shown-thumbnails (:show-thumbnails local)
:interactions-mode interactions-mode}]]))
:interactions-mode interactions-mode
:share share}]]))
;; --- Component: Viewer

View file

@ -120,7 +120,7 @@
:key (dm/str "zoom-fullscreen-" sc)} sc])]]]]]))
(mf/defc header-options
[{:keys [section zoom page file index permissions interactions-mode]}]
[{:keys [section zoom page file index permissions interactions-mode share]}]
(let [fullscreen? (mf/deref fullscreen-ref)
toggle-fullscreen
@ -159,6 +159,12 @@
handle-zoom-fit
(mf/use-fn
#(st/emit! dv/zoom-to-fit))]
(mf/with-effect [permissions share]
(when (and
(:in-team permissions)
(:is-admin permissions)
share)
(open-share-dialog)))
[:div {:class (stl/css :options-zone)}
[:& export-progress-widget]
@ -261,7 +267,7 @@
(mf/defc header
[{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails]}]
[{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails share]}]
(let [go-to-dashboard
(mf/use-fn
#(st/emit! (dv/go-to-dashboard)))
@ -351,4 +357,5 @@
:file file
:index index
:zoom zoom
:interactions-mode interactions-mode}]]))
:interactions-mode interactions-mode
:share share}]]))

View file

@ -10,14 +10,12 @@
[app.common.logging :as log]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.auth :refer [terms-login]]
[app.main.ui.auth.login :refer [login-methods]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page]]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :refer [storage]]
[rumext.v2 :as mf]))
(log/set-level! :warn)
@ -26,8 +24,7 @@
{::mf/register modal/components
::mf/register-as :login-register}
[_]
(let [uri (. (. js/document -location) -href)
user-email (mf/use-state "")
(let [user-email (mf/use-state "")
register-token (mf/use-state "")
current-section* (mf/use-state :login)
@ -66,9 +63,6 @@
(reset! register-token (:token data))
(set-current-section :register-validate))]
(mf/with-effect []
(swap! storage assoc :redirect-url uri))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
@ -125,4 +119,4 @@
(when main-section
[:div {:class (stl/css :links)}
[:& terms-login]])]]]))
[:& terms-register]])]]]))

View file

@ -12,6 +12,7 @@
.modal-container {
@extend .modal-container-base;
width: $s-368;
}
.modal-header {
@ -32,8 +33,8 @@
@include bodySmallTypography;
gap: $s-24;
max-height: $s-400;
width: $s-368;
overflow: hidden auto;
form {
display: flex;
flex-direction: column;
@ -46,7 +47,6 @@
display: flex;
justify-content: center;
flex-direction: column;
max-width: $s-368;
}
.links {
@ -64,6 +64,7 @@
color: var(--modal-text-foreground-color);
margin-top: $s-12;
}
a {
@extend .button-secondary;
height: $s-40;

View file

@ -13,6 +13,7 @@
[app.main.data.events :as ev]
[app.util.browser-history :as bhistory]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[goog.events :as e]
@ -143,6 +144,11 @@
(= (.-hostname location) (:host referrer)))
(nav-back))))
(defn nav-root
"Navigate to the root page."
[]
(set! (.-href globals/location) "/"))
;; --- History API
(defn initialize-history

View file

@ -44,3 +44,4 @@
(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage")))))
(add-watch storage :persistence #(persist js/localStorage %3 %4))