0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Merge pull request #5411 from penpot/niwinz-routing-refactor

♻️ Refactor application routing
This commit is contained in:
Belén Albeza 2024-12-04 15:31:07 +01:00 committed by GitHub
commit 0828d2e092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
145 changed files with 3338 additions and 3061 deletions

View file

@ -575,7 +575,7 @@
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))

View file

@ -9,7 +9,6 @@
[app.common.schema :as sm]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :refer [resolve-public-uri]]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
@ -61,7 +60,7 @@
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))))
(def ^:private schema:search-files

View file

@ -77,7 +77,7 @@
:share-links links
:libraries libs
:file file
:team team
:team (assoc team :permissions perms)
:permissions perms}))
(def schema:get-view-only-bundle

View file

@ -1010,6 +1010,9 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-text?
(validator ::text))
(def check-safe-int!
(check-fn ::safe-int))

View file

@ -412,7 +412,6 @@
(recur (when continue? (rest styles)) taking? to result))
result))))
(defn content->text
"Given a root node of a text content extracts the texts with its associated styles"
[content]

View file

@ -18,11 +18,18 @@
java.nio.ByteBuffer)))
(defn uuid
"Parse string uuid representation into proper UUID instance."
"Creates an UUID instance from string, expectes valid uuid strings,
the existense of validation is implementation detail"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/uuid s)))
(defn parse
"Parse string uuid representation into proper UUID instance, validates input"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/parse-uuid s)))
(defn next
[]
#?(:clj (UUIDv8/create)

View file

@ -1,6 +1,6 @@
{
"~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1715266551088",
"~:modified-at": "~m1715266551088",
"~:is-default": false,

View file

@ -6,6 +6,7 @@
"~:modified-at": "~m1714045654874",
"~:name": "New File 2",
"~:revn": 1,
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false
},
{
@ -15,6 +16,7 @@
"~:modified-at": "~m1713519762931",
"~:name": "New File 1",
"~:revn": 1,
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,23 @@
[{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": false,
"~:is-admin": false,
"~:can-edit": false
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:created-at": "~m1713533116375",
"~:is-default": true
}]

View file

@ -0,0 +1,23 @@
[{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:created-at": "~m1713533116375",
"~:is-default": true
}]

View file

@ -110,6 +110,10 @@ export class DashboardPage extends BaseWebSocketPage {
"get-project-files?project-id=*",
"dashboard/get-project-files.json",
);
await this.mockRPC(/assets\/by-id/gi, "dashboard/thumbnail.png", {
contentType: "image/png",
});
}
async setupNewProject() {
@ -207,60 +211,64 @@ export class DashboardPage extends BaseWebSocketPage {
async goToDashboard() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.anyTeamId}/projects`,
`#/dashboard/recent?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toBeVisible();
}
async goToSecondTeamDashboard() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/projects`,
`#/dashboard/recent?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamMembersSection() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/members`,
`#/dashboard/members?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamInvitationsSection() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/invitations`,
`#/dashboard/invitations?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamWebhooksSection() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`,
`#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamWebhooksSection() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`,
`#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSecondTeamSettingsSection() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/settings`,
`#/dashboard/settings?team-id=${DashboardPage.secondTeamId}`,
);
}
async goToSearch() {
await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/search`);
await this.page.goto(
`#/dashboard/search?team-id=${DashboardPage.anyTeamId}`,
);
}
async goToDrafts() {
await this.page.goto(
`#/dashboard/team/${DashboardPage.anyTeamId}/projects/${DashboardPage.draftProjectId}`,
`#/dashboard/files?team-id=${DashboardPage.anyTeamId}&project-id=${DashboardPage.draftProjectId}`,
);
await expect(this.mainHeading).toHaveText("Drafts");
}
async goToFonts() {
await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/fonts`);
await this.page.goto(
`#/dashboard/fonts?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Fonts");
}

View file

@ -85,7 +85,7 @@ export class ViewerPage extends BaseWebSocketPage {
pageId = ViewerPage.anyPageId,
} = {}) {
await this.page.goto(
`/#/view/${fileId}?page-id=${pageId}&section=interactions&index=0`,
`/#/view?file-id=${fileId}&page-id=${pageId}&section=interactions&index=0`,
);
this.#ws = await this.waitForNotificationsWebSocket();

View file

@ -36,6 +36,14 @@ export class WorkspacePage extends BaseWebSocketPage {
"get-team?id=*",
"workspace/get-team-default.json",
);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
await BaseWebSocketPage.mockRPC(
page,
"get-team-members?team-id=*",
"logged-in-user/get-team-members-your-penpot.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-profiles-for-file-comments?file-id=*",
@ -43,6 +51,7 @@ export class WorkspacePage extends BaseWebSocketPage {
);
}
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b";
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
@ -83,7 +92,7 @@ export class WorkspacePage extends BaseWebSocketPage {
pageId = WorkspacePage.anyPageId,
} = {}) {
await this.page.goto(
`/#/workspace/${WorkspacePage.anyProjectId}/${fileId}?page-id=${pageId}`,
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
);
this.#ws = await this.waitForNotificationsWebSocket();

View file

@ -7,11 +7,7 @@ test.beforeEach(async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await WorkspacePage.mockRPC(
page,
"get-team?id=*",
"workspace/get-team-role-viewer.json",
);
await WorkspacePage.mockRPC(page, "get-teams", "get-teams-role-viewer.json");
await workspacePage.goToWorkspace();
});

View file

@ -10,8 +10,9 @@
[app.common.logging :as log]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.users :as du]
[app.main.data.auth :as da]
[app.main.data.event :as ev]
[app.main.data.profile :as dp]
[app.main.data.websocket :as ws]
[app.main.errors]
[app.main.features :as feat]
@ -66,14 +67,14 @@
authenticated user; proceed to fetch teams."
[stream]
(rx/merge
(rx/of (du/fetch-profile))
(rx/of (dp/fetch-profile))
(->> stream
(rx/filter (ptk/type? ::profile-fetched))
(rx/filter dp/profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/mapcat (fn [profile]
(if (du/is-authenticated? profile)
(rx/of (du/fetch-teams))
(if (dp/is-authenticated? profile)
(rx/of (dp/initialize-profile profile))
(rx/empty))))
(rx/observe-on :async))))
@ -92,19 +93,24 @@
(initialize-profile stream)
;; Watch for profile deletion events
(->> stream
(rx/filter dp/profile-deleted?)
(rx/map da/logged-out))
;; Once profile is fetched, initialize all penpot application
;; routes
(->> stream
(rx/filter du/profile-fetched?)
(rx/filter dp/profile-fetched?)
(rx/take 1)
(rx/map #(rt/init-routes)))
;; Once profile fetched and the current user is authenticated,
;; proceed to initialize the websockets connection.
(->> stream
(rx/filter du/profile-fetched?)
(rx/filter dp/profile-fetched?)
(rx/map deref)
(rx/filter du/is-authenticated?)
(rx/filter dp/is-authenticated?)
(rx/take 1)
(rx/map #(ws/initialize)))))))

View file

@ -0,0 +1,319 @@
;; 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) KALEIDOS INC
(ns app.main.data.auth
"Auth related data events"
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.profile :as dp]
[app.main.data.team :as dtm]
[app.main.data.websocket :as ws]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
;; --- HELPERS
(defn is-authenticated?
[{:keys [id]}]
(and (uuid? id) (not= id uuid/zero)))
;; --- 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."
[{:keys [props] :as profile}]
(letfn [(get-redirect-events [teams]
(if-let [redirect-href (:login-redirect storage/session)]
(binding [storage/*sync* true]
(swap! storage/session dissoc :login-redirect)
(if (= redirect-href (rt/get-current-href))
(rx/of (rt/reload true))
(rx/of (rt/nav-raw :href redirect-href))))
(if-let [file-id (get props :welcome-file-id)]
(rx/of (dcm/go-to-workspace
:file-id file-id
:team-id (:default-team-id profile))
(dp/update-profile-props {:welcome-file-id nil}))
(let [teams (into #{} (map :id) teams)
team-id (dtm/get-last-team-id)
team-id (if (and team-id (contains? teams team-id))
team-id
(:default-team-id profile))]
(rx/of (dcm/go-to-dashboard-recent {:team-id team-id}))))))]
(ptk/reify ::logged-in
ev/Event
(-data [_]
{::ev/name "signin"
::ev/type "identify"
:email (:email profile)
:auth-backend (:auth-backend profile)
:fullname (:fullname profile)
:is-muted (:is-muted profile)
:default-team-id (:default-team-id profile)
:default-project-id (:default-project-id profile)})
ptk/WatchEvent
(watch [_ _ stream]
(->> (rx/merge
(rx/of (dp/initialize-profile profile)
(ws/initialize)
(dtm/fetch-teams))
(->> stream
(rx/filter (ptk/type? ::dtm/teams-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat get-redirect-events)))
(rx/observe-on :async))))))
(declare login-from-register)
(defn login
[{:keys [email password invitation-token] :as data}]
(ptk/reify ::login
ptk/WatchEvent
(watch [_ _ stream]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)
params {:email email
:password password
:invitation-token invitation-token}]
;; NOTE: We can't take the profile value from login because
;; there are cases when login is successful 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.
(->> (rp/cmd! :login-with-password (d/without-nils params))
(rx/merge-map (fn [data]
(rx/merge
(rx/of (dp/fetch-profile))
(->> stream
(rx/filter dp/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 dp/profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter is-authenticated?)
(rx/map (fn [profile]
(with-meta (merge data profile)
{::ev/source "login"})))
(rx/tap on-success)
(rx/map logged-in)
(rx/observe-on :async)))))
(rx/catch on-error))))))
(def ^:private schema:login-with-ldap
[:map {:title "login-with-ldap"}
[:email ::sm/email]
[:password :string]])
(defn login-with-ldap
[params]
(dm/assert!
"expected valid params"
(sm/check schema:login-with-ldap params))
(ptk/reify ::login-with-ldap
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta params)]
(->> (rp/cmd! :login-with-ldap params)
(rx/tap on-success)
(rx/map (fn [profile]
(-> profile
(with-meta {::ev/source "login-with-ldap"})
(logged-in))))
(rx/catch on-error))))))
(defn login-from-token
"Used mainly as flow continuation after token validation."
[{:keys [profile] :as tdata}]
(ptk/reify ::login-from-token
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
;; NOTE: we need this to be asynchronous because the effect
;; should be called before proceed with the login process
(rx/observe-on :async)))))
(defn login-from-register
"Event used mainly for mark current session as logged-in in after the
user successfully registered using third party auth provider (in this
case we dont need to verify the email)."
[]
(ptk/reify ::login-from-register
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (dp/fetch-profile))
(->> stream
(rx/filter dp/profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter is-authenticated?)
(rx/map (fn [profile]
(with-meta profile
{::ev/source "register"})))
(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]))
ptk/WatchEvent
(watch [_ _ _]
(rx/merge
;; 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))
(rx/of (ws/finalize))))
ptk/EffectEvent
(effect [_ _ _]
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
(swap! storage/user (constantly {})))))
(defn logout
[]
(ptk/reify ::logout
ev/Event
(-data [_] {})
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (:profile-id state)]
(->> (rx/interval 500)
(rx/take 1)
(rx/mapcat (fn [_]
(->> (rp/cmd! :logout {:profile-id profile-id})
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1))))))
(rx/map logged-out))))))
;; --- Update Profile
(def ^:private
schema:request-profile-recovery
[:map {:title "request-profile-recovery" :closed true}
[:email ::sm/email]])
(defn request-profile-recovery
[data]
(dm/assert!
"expected valid parameters"
(sm/check schema:request-profile-recovery data))
(ptk/reify ::request-profile-recovery
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)]
(->> (rp/cmd! :request-profile-recovery data)
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: recover-profile (Password)
(def ^:private
schema:recover-profile
[:map {:title "recover-profile" :closed true}
[:password :string]
[:token :string]])
(defn recover-profile
[data]
(dm/assert!
"expected valid arguments"
(sm/check schema:recover-profile data))
(ptk/reify ::recover-profile
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)]
(->> (rp/cmd! :recover-profile data)
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: crete-demo-profile
(defn create-demo-profile
[]
(ptk/reify ::create-demo-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :create-demo-profile {})
(rx/map login)))))
(defn show-redirect-error
"A helper event that interprets the OIDC redirect errors on the URI
and shows an appropriate error message using the notification
banners."
[error]
(ptk/reify ::show-redirect-error
ptk/WatchEvent
(watch [_ _ _]
(when-let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
;; We explicitly do not show any error here, it a explicit user operation.
"unable-to-auth"
nil
(tr "errors.generic"))]
(rx/of (ntf/warn hint))))))

View file

@ -12,7 +12,7 @@
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
@ -603,3 +603,18 @@
(filter (fn [comment] (some #(= % (:frame-id comment)) frame-ids?)))
(map update-comment-thread-frame)
(rx/from))))))
(defn fetch-profiles
"Fetch or refresh all profile data for comments of the current file"
[]
(ptk/reify ::fetch-comments-profiles
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
share-id (or (-> state :viewer-local :share-id)
(:current-share-id state))]
(->> (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})
(rx/map (fn [profiles]
#(update % :profiles merge (d/index-by :id profiles)))))))))

View file

@ -14,14 +14,18 @@
[app.common.types.team :as ctt]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.util.dom :as-alias dom]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(declare go-to-dashboard-recent)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -233,12 +237,165 @@
(watch [_ state _]
(when (= :removed change)
(let [message (tr "dashboard.removed-from-team" team-name)
profile (:profile state)]
team-id (-> state :profile :default-team-id)]
(rx/concat
(rx/of (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))
(rx/of (go-to-dashboard-recent :team-id team-id))
(->> (rx/of (ntf/info message))
;; Delay so the navigation can finish
(rx/delay 250))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NAVEGATION EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn go-to-feedback
[]
(ptk/reify ::go-to-feedback
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/nav :settings-feedback {}
::rt/new-window true
::rt/window-name "penpot-feedback")))))
(defn go-to-dashboard-files
[& {:keys [project-id team-id] :as options}]
(ptk/reify ::go-to-dashboard-files
ptk/WatchEvent
(watch [_ state _]
(let [profile (get state :profile)
team-id (or team-id (:current-team-id state))
project-id (if (= project-id :default)
(:default-project-id profile)
project-id)
params {:team-id team-id
:project-id project-id}]
(rx/of (rt/nav :dashboard-files params options))))))
(defn go-to-dashboard-search
[& {:keys [term] :as options}]
(ptk/reify ::go-to-dashboard-search
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(rx/merge
(->> (rx/of (rt/nav :dashboard-search
{:team-id team-id
:search-term term})
(modal/hide))
(rx/observe-on :async))
(->> stream
(rx/filter (ptk/type? ::rt/navigated))
(rx/take 1)
(rx/map (fn [_]
(ptk/event ::dom/focus-element
{:name "search-input"})))))))))
(defn go-to-dashboard-libraries
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-libraries
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))]
(rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
(defn go-to-dashboard-fonts
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-fonts
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))]
(rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
(defn go-to-dashboard-recent
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-recent
ptk/WatchEvent
(watch [_ state _]
(let [profile (get state :profile)
team-id (cond
(= :default team-id)
(:default-team-id profile)
(uuid? team-id)
team-id
:else
(:current-team-id state))
params {:team-id team-id}]
(rx/of (modal/hide)
(rt/nav :dashboard-recent params options))))))
(defn go-to-dashboard-members
[& {:as options}]
(ptk/reify ::go-to-dashboard-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-members {:team-id team-id}))))))
(defn go-to-dashboard-invitations
[& {:as options}]
(ptk/reify ::go-to-dashboard-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-invitations {:team-id team-id}))))))
(defn go-to-dashboard-webhooks
[& {:as options}]
(ptk/reify ::go-to-dashboard-webhooks
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-webhooks {:team-id team-id}))))))
(defn go-to-dashboard-settings
[& {:as options}]
(ptk/reify ::go-to-dashboard-settings
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-settings {:team-id team-id}))))))
(defn go-to-workspace
[& {:keys [team-id file-id page-id layout] :as options}]
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))
file-id (or file-id (:current-file-id state))
;: FIXME: why not :current-page-id
page-id (or page-id
(dm/get-in state [:workspace-data :pages 0]))
params (-> (rt/get-params state)
(assoc :team-id team-id)
(assoc :file-id file-id)
(assoc :page-id page-id)
(assoc :layout layout)
(d/without-nils))]
(rx/of (rt/nav :workspace params options))))))
(defn go-to-viewer
[& {:keys [file-id page-id section frame-id index] :as options}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
(watch [_ state _]
(let [page-id (or page-id (:current-page-id state))
file-id (or file-id (:current-file-id state))
section (or section :interactions)
params {:file-id file-id
:page-id page-id
:section section
:frame-id frame-id
:index index}
params (d/without-nils params)
name (dm/str "viewer-" file-id)
options (merge {::rt/new-window true
::rt/window-name name}
options)]
(rx/of ::dps/force-persist
(rt/nav :viewer params options))))))

View file

@ -12,26 +12,17 @@
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.common :as dc]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.websocket :as dws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.sse :as sse]
[app.util.time :as dt]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[clojure.set :as set]
[potok.v2.core :as ptk]))
@ -43,143 +34,38 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects)
(declare fetch-team-members)
(declare process-message)
(defn initialize
[{:keys [id]}]
(dm/assert! (uuid? id))
[]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [prev-team-id (:current-team-id state)]
(cond-> state
(not= prev-team-id id)
(-> (dissoc :current-team-initialized)
(dissoc :dashboard-files)
(dissoc :dashboard-projects)
(dissoc :dashboard-shared-files)
(dissoc :dashboard-recent-files)
(dissoc :dashboard-team-members)
(dissoc :dashboard-team-stats)
(assoc :current-team-id id)
(update :workspace-global dissoc :default-font)))))
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (rx/filter (ptk/type? ::finalize) stream)
(let [stopper (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(->> (rx/merge
;; fetch teams must be first in case the team doesn't exist
(ptk/watch (du/fetch-teams) state stream)
(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-users) state stream)
(rx/of (fetch-projects)
(df/fetch-fonts))
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [topic] :as msg}]
(or (= topic uuid/zero)
(= topic profile-id))))
(rx/map process-message))
;; Once the teams are fecthed, initialize features related
;; to currently active team
(->> stream
(rx/filter (ptk/type? ::du/teams-fetched))
(rx/observe-on :async)
(rx/mapcat deref)
(rx/filter #(= id (:id %)))
(rx/mapcat (fn [team]
(rx/of (du/set-current-team team)
#(assoc % :current-team-initialized true))))))
(rx/map process-message)
(rx/ignore)))
(rx/take-until stopper))))))
(defn finalize
[params]
(ptk/data-event ::finalize params))
[]
(ptk/data-event ::finalize {}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- EVENT: fetch-team-members
(defn team-members-fetched
[members]
(ptk/reify ::team-members-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-members (d/index-by :id members)))))
(defn fetch-team-members
([] (fetch-team-members nil))
([team-id]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))]
(assert (uuid? team-id) "expected team-id to be resolved")
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map team-members-fetched)))))))
;; --- EVENT: fetch-team-stats
(defn team-stats-fetched
[stats]
(ptk/reify ::team-stats-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-stats stats))))
(defn fetch-team-stats
[team-id]
(ptk/reify ::fetch-team-stats
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map team-stats-fetched)))))
;; --- EVENT: fetch-team-invitations
(defn team-invitations-fetched
[invitations]
(ptk/reify ::team-invitations-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-invitations invitations))))
(defn fetch-team-invitations
[]
(ptk/reify ::fetch-team-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-invitations {:team-id team-id})
(rx/map team-invitations-fetched))))))
;; --- EVENT: fetch-team-webhooks
(defn team-webhooks-fetched
[webhooks]
(ptk/reify ::team-webhooks-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-webhooks webhooks))))
(defn fetch-team-webhooks
[]
(ptk/reify ::fetch-team-webhooks
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-webhooks {:team-id team-id})
(rx/map team-webhooks-fetched))))))
;; --- EVENT: fetch-projects
(defn projects-fetched
@ -187,8 +73,10 @@
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(let [projects (d/index-by :id projects)]
(assoc state :dashboard-projects projects)))))
(reduce (fn [state {:keys [id] :as project}]
(update-in state [:projects id] merge project))
state
projects))))
(defn fetch-projects
[]
@ -201,82 +89,28 @@
;; --- EVENT: search
(defn search-result-fetched
[result]
(ptk/reify ::search-result-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-search-result result))))
(def schema:search-params
(def ^:private schema:search-params
[:map {:closed true}
[:search-term [:maybe :string]]])
(def ^:private check-search-params
(sm/check-fn schema:search-params))
(defn search
[params]
(dm/assert! schema:search-params params)
(ptk/reify ::search
ptk/UpdateEvent
(update [_ state]
(dissoc state :dashboard-search-result))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :search-files params)
(rx/map search-result-fetched))))))
;; --- EVENT: files
(defn files-fetched
[project-id files]
(letfn [(remove-project-files [files]
(reduce-kv (fn [result id file]
(cond-> result
(= (:project-id file) project-id) (dissoc id)))
files
files))]
(ptk/reify ::files-fetched
(let [params (check-search-params params)]
(ptk/reify ::search
ptk/UpdateEvent
(update [_ state]
(-> state
(update :dashboard-files
(fn [state]
(let [state (remove-project-files state)]
(reduce #(assoc %1 (:id %2) %2) state files))))
(assoc-in [:dashboard-projects project-id :count] (count files)))))))
(dissoc state :search-result))
(defn fetch-files
[{:keys [project-id] :as params}]
(dm/assert! (uuid? project-id))
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-project-files {:project-id project-id})
(rx/map #(files-fetched project-id %))))))
;; --- EVENT: shared-files
(defn shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [files (d/index-by :id files)]
(-> state
(assoc :dashboard-shared-files files)
(update :dashboard-files d/merge files))))))
(defn fetch-shared-files
([] (fetch-shared-files nil))
([team-id]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :search-files params)
(rx/map (fn [result]
#(assoc % :search-result result)))))))))
;; --- EVENT: recent-files
@ -287,8 +121,8 @@
(update [_ state]
(let [files (d/index-by :id files)]
(-> state
(assoc :dashboard-recent-files files)
(update :dashboard-files d/merge files))))))
(assoc :recent-files files)
(update :files d/merge files))))))
(defn fetch-recent-files
[]
@ -325,27 +159,22 @@
(ptk/reify ::clear-file-select
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :selected-files #{}
:selected-project nil
:menu-open false
:menu-pos nil))))
(-> state
(dissoc :selected-files)
(dissoc :selected-project)
(update :dashboard-local dissoc :menu-open :menu-pos)))))
(defn toggle-file-select
[{:keys [id project-id] :as file}]
(ptk/reify ::toggle-file-select
ptk/UpdateEvent
(update [_ state]
(let [selected-project-id (get-in state [:dashboard-local :selected-project])]
(let [selected-project-id (get state :selected-project)]
(if (or (nil? selected-project-id)
(= selected-project-id project-id))
(update state :dashboard-local
(fn [local]
(-> local
(update :selected-files #(if (contains? % id)
(disj % id)
(conj % id)))
(assoc :selected-project project-id))))
(-> state
(update :selected-files #(if (contains? % id) (disj % id) (conj % id)))
(assoc :selected-project project-id))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -399,320 +228,6 @@
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- EVENT: create-team
(defn team-created
[team]
(ptk/reify ::team-created
IDeref
(-deref [_] team)))
(defn create-team
[{:keys [name] :as params}]
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
params {:name name :features features}]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: create-team-with-invitations
(defn create-team-with-invitations
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
params {:name name
:emails emails
:role role
:features features}]
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: update-team
(defn update-team
[{:keys [id name] :as params}]
(ptk/reify ::update-team
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:teams id :name] name)
(assoc-in [:team :name] name)))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :update-team params)
(rx/ignore)))))
(defn update-team-photo
[file]
(dm/assert!
"expected a valid blob for `file` param"
(di/blob? file))
(ptk/reify ::update-team-photo
ptk/WatchEvent
(watch [_ state _]
(let [on-success di/notify-finished-loading
on-error #(do (di/notify-finished-loading)
(di/process-error %))
team-id (:current-team-id state)
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/cmd! :update-team-photo %))
(rx/tap on-success)
(rx/mapcat (fn [_]
(rx/of (du/fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "update-team-photo"
:team-id team-id}))))
(rx/catch on-error))))))
(defn update-team-member-role
[{:keys [role member-id] :as params}]
(dm/assert! (uuid? member-id))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-team-member-role
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :update-team-member-role params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(du/fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "update-team-member-role"
:team-id team-id
:role role
:member-id member-id})))))))))
(defn delete-team-member
[{:keys [member-id] :as params}]
(dm/assert! (uuid? member-id))
(ptk/reify ::delete-team-member
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :delete-team-member params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(du/fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "delete-team-member"
:team-id team-id
:member-id member-id})))))))))
(defn leave-team
[{:keys [reassign-to] :as params}]
(dm/assert! (or (nil? reassign-to)
(uuid? reassign-to)))
(ptk/reify ::leave-team
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
team-id (:current-team-id state)
params (cond-> {:id team-id}
(uuid? reassign-to)
(assoc :reassign-to reassign-to))]
(->> (rp/cmd! :leave-team params)
(rx/tap #(tm/schedule on-success))
(rx/map (fn [_]
(ptk/data-event ::ev/event
{::ev/name "leave-team"
:reassign-to reassign-to
:team-id team-id})))
(rx/catch on-error))))))
(defn invite-team-members
[{:keys [emails role team-id resend?] :as params}]
(dm/assert! (keyword? role))
(dm/assert! (uuid? team-id))
(dm/assert!
"expected a valid set of emails"
(sm/check-set-of-emails! emails))
(ptk/reify ::invite-team-members
ev/Event
(-data [_]
{:role role
:team-id team-id
:resend resend?})
ptk/WatchEvent
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params (dissoc params :resend?)]
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/catch on-error))))))
(defn copy-invitation-link
[{:keys [email team-id] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(ptk/reify ::copy-invitation-link
IDeref
(-deref [_] {:email email :team-id team-id})
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
router (:router state)]
(->> (rp/cmd! :get-team-invitation-token params)
(rx/map (fn [params]
(rt/resolve router :auth-verify-token {} params)))
(rx/map (fn [fragment]
(assoc cf/public-uri :fragment fragment)))
(rx/tap (fn [uri]
(wapi/write-to-clipboard (str uri))))
(rx/tap on-success)
(rx/ignore)
(rx/catch on-error))))))
(defn update-team-invitation-role
[{:keys [email team-id role] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-team-invitation-role
IDeref
(-deref [_] {:role role})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :update-team-invitation-role params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-team-invitation
[{:keys [email team-id] :as params}]
(dm/assert! (sm/check-email! email))
(dm/assert! (uuid? team-id))
(ptk/reify ::delete-team-invitation
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :delete-team-invitation params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-team-webhook
[{:keys [id] :as params}]
(dm/assert! (uuid? id))
(ptk/reify ::delete-team-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :delete-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
(def valid-mtypes
#{"application/json"
"application/x-www-form-urlencoded"
"application/transit+json"})
(defn update-team-webhook
[{:keys [id uri mtype is-active] :as params}]
(dm/assert! (uuid? id))
(dm/assert! (contains? valid-mtypes mtype))
(dm/assert! (boolean? is-active))
(dm/assert! (u/uri? uri))
(ptk/reify ::update-team-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :update-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn create-team-webhook
[{:keys [uri mtype is-active] :as params}]
(dm/assert! (contains? valid-mtypes mtype))
(dm/assert! (boolean? is-active))
(dm/assert! (u/uri? uri))
(ptk/reify ::create-team-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (-> params
(assoc :team-id team-id)
(update :uri str))
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :create-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: delete-team
(defn delete-team
[{:keys [id] :as params}]
(ptk/reify ::delete-team
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :delete-team {:id id})
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: create-project
(defn- project-created
@ -724,7 +239,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:dashboard-projects id] project)
(assoc-in [:projects id] project)
(assoc-in [:dashboard-local :project-for-edit] id)))))
(defn create-project
@ -732,7 +247,7 @@
(ptk/reify ::create-project
ptk/WatchEvent
(watch [_ state _]
(let [projects (get state :dashboard-projects)
(let [projects (get state :projects)
unames (cfh/get-used-names projects)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
team-id (:current-team-id state)
@ -753,7 +268,7 @@
(ptk/reify ::project-duplicated
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-projects id] project))))
(assoc-in state [:projects id] project))))
(defn duplicate-project
[{:keys [id name] :as params}]
@ -803,11 +318,11 @@
(ptk/reify ::toggle-project-pin
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned)))
(assoc-in state [:projects id :is-pinned] (not is-pinned)))
ptk/WatchEvent
(watch [_ state _]
(let [project (get-in state [:dashboard-projects id])
(let [project (get-in state [:projects id])
params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/cmd! :update-project-pin params)
(rx/ignore))))))
@ -820,7 +335,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:dashboard-projects id :name] (constantly name))
(update-in [:projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent
@ -836,7 +351,7 @@
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-projects dissoc id))
(update state :projects dissoc id))
ptk/WatchEvent
(watch [_ _ _]
@ -850,7 +365,7 @@
(ptk/reify ::file-deleted
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-projects project-id :count] dec))))
(update-in state [:projects project-id :count] dec))))
(defn delete-file
[{:keys [id project-id] :as params}]
@ -858,9 +373,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-when :dashboard-files dissoc id)
(d/update-when :dashboard-shared-files dissoc id)
(d/update-when :dashboard-recent-files dissoc id)))
(d/update-when :files dissoc id)
(d/update-when :shared-files dissoc id)
(d/update-when :recent-files dissoc id)))
ptk/WatchEvent
(watch [_ state _]
@ -882,9 +397,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-in-when [:dashboard-files id :name] (constantly name))
(d/update-in-when [:dashboard-shared-files id :name] (constantly name))
(d/update-in-when [:dashboard-recent-files id :name] (constantly name))))
(d/update-in-when [:files id :name] (constantly name))
(d/update-in-when [:shared-files id :name] (constantly name))
(d/update-in-when [:recent-files id :name] (constantly name))))
ptk/WatchEvent
(watch [_ _ _]
@ -906,10 +421,10 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
(d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))
(d/update-in-when [:files id :is-shared] (constantly is-shared))
(d/update-in-when [:recent-files id :is-shared] (constantly is-shared))
(cond-> (not is-shared)
(d/update-when :dashboard-shared-files dissoc id))))
(d/update-when :shared-files dissoc id))))
ptk/WatchEvent
(watch [_ _ _]
@ -928,8 +443,8 @@
(= file-id (:id %))
(assoc :thumbnail-id thumbnail-id)))))]
(-> state
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file
@ -946,9 +461,9 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:dashboard-files id] file)
(assoc-in [:dashboard-recent-files id] file)
(update-in [:dashboard-projects project-id :count] inc)))))
(assoc-in [:files id] file)
(assoc-in [:recent-files id] file)
(update-in [:projects project-id :count] inc)))))
(defn create-file
[{:keys [project-id name] :as params}]
@ -963,7 +478,7 @@
:or {on-success identity
on-error rx/throw}} (meta params)
files (get state :dashboard-files)
files (get state :files)
unames (cfh/get-used-names files)
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
features (-> (features/get-team-enabled-features state)
@ -1015,14 +530,14 @@
ptk/UpdateEvent
(update [_ state]
(let [origin-project (get-in state [:dashboard-files (first ids) :project-id])
(let [origin-project (get-in state [:files (first ids) :project-id])
update-project (fn [project delta op]
(-> project
(update :count #(op % (count ids)))
(assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))]
(-> state
(d/update-in-when [:dashboard-projects origin-project] update-project 0 -)
(d/update-in-when [:dashboard-projects project-id] update-project 10 +))))
(d/update-in-when [:projects origin-project] update-project 0 -)
(d/update-in-when [:projects project-id] update-project 10 +))))
ptk/WatchEvent
(watch [_ _ _]
@ -1063,123 +578,6 @@
(rx/tap on-success)
(rx/catch on-error))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn go-to-workspace
[{:keys [id project-id] :as file}]
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ _ _]
(let [pparams {:project-id project-id :file-id id}]
(rx/of (rt/nav :workspace pparams))))))
(defn go-to-files
([project-id]
(ptk/reify ::go-to-files-1
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id project-id}))))))
([team-id project-id]
(ptk/reify ::go-to-files-2
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id project-id}))))))
(defn go-to-search
([] (go-to-search nil))
([term]
(ptk/reify ::go-to-search
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(if (empty? term)
(do
(dom/focus! (dom/get-element "search-input"))
(rx/of (rt/nav :dashboard-search
{:team-id team-id})))
(rx/of (rt/nav :dashboard-search
{:team-id team-id}
{:search-term term})))))
ptk/EffectEvent
(effect [_ _ _]
(dom/focus! (dom/get-element "search-input"))))))
(defn go-to-projects
([]
(ptk/reify ::go-to-projects-0
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
([team-id]
(ptk/reify ::go-to-projects-1
ptk/UpdateEvent
(update [_ state]
;; FIXME: Remove on 2.5
(assoc state :current-team-id team-id))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
(defn go-to-team-members
[]
(ptk/reify ::go-to-team-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-members {:team-id team-id}))))))
(defn go-to-team-invitations
[]
(ptk/reify ::go-to-team-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-invitations {:team-id team-id}))))))
(defn go-to-team-webhooks
[]
(ptk/reify ::go-to-team-webhooks
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-webhooks {:team-id team-id}))))))
(defn go-to-team-settings
[]
(ptk/reify ::go-to-team-settings
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-settings {:team-id team-id}))))))
(defn go-to-drafts
[]
(ptk/reify ::go-to-drafts
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
projects (:dashboard-projects state)
default-project (d/seek :is-default (vals projects))]
(when default-project
(rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id (:id default-project)})))))))
(defn go-to-libs
[]
(ptk/reify ::go-to-libs
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))
(defn create-element
[]
(ptk/reify ::create-element
@ -1190,10 +588,10 @@
pparams (:path-params route)
in-project? (contains? pparams :project-id)
name (if in-project?
(let [files (get state :dashboard-files)
(let [files (get state :files)
unames (cfh/get-used-names files)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
(let [projects (get state :dashboard-projects)
(let [projects (get state :projects)
unames (cfh/get-used-names projects)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
params (if in-project?
@ -1203,7 +601,7 @@
:team-id team-id})
action-name (if in-project? :create-file :create-project)
action (if in-project? file-created project-created)
can-edit? (dm/get-in state [:permissions :can-edit])]
can-edit? (dm/get-in state [:teams team-id :permissions :can-edit])]
(when can-edit?
(->> (rp/cmd! action-name params)
@ -1214,10 +612,9 @@
(ptk/reify ::open-selected-file
ptk/WatchEvent
(watch [_ state _]
(let [files (get-in state [:dashboard-local :selected-files])]
(let [[file-id :as files] (get state :selected-files)]
(if (= 1 (count files))
(let [file (get-in state [:dashboard-files (first files)])]
(rx/of (go-to-workspace file)))
(rx/of (dcm/go-to-workspace :file-id file-id))
(rx/empty))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1229,13 +626,13 @@
(ptk/reify ::handle-change-team-role
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dc/change-team-role params)
(rx/of (dcm/change-team-role params)
(modal/hide)))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dc/handle-notification msg)
:notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dc/team-membership-change msg)
:team-membership-change (dcm/team-membership-change msg)
nil))

View file

@ -6,27 +6,28 @@
(ns app.main.data.dashboard.shortcuts
(:require
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
[app.main.data.users :as du]
[app.main.store :as st]))
(def shortcuts
{:go-to-search {:tooltip (ds/meta "F")
:command (ds/c-mod "f")
:subsections [:navigation-dashboard]
:fn #(st/emit! (dd/go-to-search))}
:fn #(st/emit! (dcm/go-to-dashboard-search))}
:go-to-drafts {:tooltip "G D"
:command "g d"
:subsections [:navigation-dashboard]
:fn #(st/emit! (dd/go-to-drafts))}
:fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))}
:go-to-libs {:tooltip "G L"
:command "g l"
:subsections [:navigation-dashboard]
:fn #(st/emit! (dd/go-to-libs))}
:fn #(st/emit! (dcm/go-to-dashboard-libraries))}
:create-new-project {:tooltip "+"
:command "+"

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.events
(ns app.main.data.event
(:require
["ua-parser-js" :as ua]
[app.common.data :as d]
@ -182,7 +182,7 @@
(rx/filter #(pos? (count %)))
(rx/debounce 2000))
(->> stream
(rx/filter (ptk/type? :app.main.data.users/logout))
(rx/filter (ptk/type? :app.main.data.profile/logout))
(rx/observe-on :async)))
(rx/map (fn [_]
(into [] (take max-buffer-size) @buffer)))

View file

@ -7,7 +7,7 @@
(ns app.main.data.exports.assets
(:require
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.persistence :as dwp]
[app.main.data.workspace.state-helpers :as wsh]

View file

@ -10,7 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.repo :as rp]

View file

@ -12,7 +12,7 @@
[app.common.logging :as log]
[app.common.media :as cm]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.fonts :as fonts]
[app.main.repo :as rp]
@ -59,10 +59,10 @@
(adapt-font-id [variant]
(update variant :font-id #(str "custom-" %)))]
(ptk/reify ::team-fonts-loaded
(ptk/reify ::fonts-loaded
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-fonts (d/index-by :id fonts)))
(assoc state :fonts (d/index-by :id fonts)))
ptk/EffectEvent
(effect [_ _ _]
@ -72,13 +72,14 @@
(mapv prepare-font))]
(fonts/register! :custom fonts))))))
(defn load-team-fonts
[team-id]
(defn fetch-fonts
[]
(ptk/reify ::load-team-fonts
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-font-variants {:team-id team-id})
(rx/map fonts-fetched)))))
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-font-variants {:team-id team-id})
(rx/map fonts-fetched))))))
(defn process-upload
"Given a seq of blobs and the team id, creates a ready-to-use fonts
@ -90,12 +91,15 @@
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))
;; Vertical metrics determine the baseline in a text and the space between lines of text.
;; For historical reasons, there are three pairs of ascender/descender values, known as hhea, OS/2 and uSWin metrics.
;; Depending on the font, operating system and application a different set will be used to render text on the screen.
;; On Mac, Safari and Chrome use the hhea values to render text. Firefox will respect the useTypoMetrics setting and will use the OS/2 if it is set.
;; If the useTypoMetrics is not set, Firefox will also use metrics from the hhea table.
;; On Windows, all browsers use the usWin metrics, but respect the useTypoMetrics setting and if set will use the OS/2 values.
;; Vertical metrics determine the baseline in a text and the space between lines of
;; text. For historical reasons, there are three pairs of ascender/descender
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
;; system and application a different set will be used to render text on the
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
;; table. On Windows, all browsers use the usWin metrics, but respect the
;; useTypoMetrics setting and if set will use the OS/2 values.
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
@ -239,7 +243,7 @@
(ptk/reify ::add-font
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts assoc (:id font) font))
(update state :fonts assoc (:id font) font))
ptk/WatchEvent
(watch [_ state _]
@ -260,13 +264,10 @@
(update [_ state]
;; Update all variants that has the same font-id with the new
;; name in the local state.
(update state :dashboard-fonts
(fn [fonts]
(d/mapm (fn [_ font]
(cond-> font
(= id (:font-id font))
(assoc :font-family name)))
fonts))))
(update state :fonts update-vals (fn [font]
(cond-> font
(= id (:font-id font))
(assoc :font-family name)))))
ptk/WatchEvent
(watch [_ state _]
@ -285,10 +286,11 @@
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts
(update state :fonts
(fn [variants]
(d/removem (fn [[_id variant]]
(= (:font-id variant) font-id)) variants))))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
@ -305,7 +307,7 @@
(ptk/reify ::delete-font-variants
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts
(update state :fonts
(fn [variants]
(d/removem (fn [[_ variant]]
(= (:id variant) id))

View file

@ -8,7 +8,7 @@
(:refer-clojure :exclude [update])
(:require
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.store :as st]
[cljs.core :as c]
[potok.v2.core :as ptk]))

View file

@ -64,7 +64,7 @@
(rx/merge
(let [stopper (rx/filter (ptk/type? ::hide) stream)]
(->> stream
(rx/filter (ptk/type? :app.util.router/navigate))
(rx/filter (ptk/type? :app.main.router/navigate))
(rx/map (fn [_] (hide)))
(rx/take-until stopper)))
(when (:timeout data)

View file

@ -4,24 +4,21 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.users
(ns app.main.data.profile
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.media :as di]
[app.main.data.notifications :as ntf]
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.data.team :as-alias dtm]
[app.main.repo :as rp]
[app.plugins.register :as register]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.main.router :as rt]
[app.plugins.register :as plugins.register]
[app.util.i18n :as i18n]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -40,7 +37,7 @@
[:lang {:optional true} :string]
[:theme {:optional true} :string]])
(def check-profile!
(def check-profile
(sm/check-fn schema:profile))
;; --- HELPERS
@ -49,98 +46,31 @@
[{:keys [id]}]
(and (uuid? id) (not= id uuid/zero)))
(defn get-current-team-id
[profile]
(let [team-id (::current-team-id storage/user)]
(or team-id (:default-team-id profile))))
(defn set-current-team!
[team-id]
(if (nil? team-id)
(swap! storage/user dissoc ::current-team-id)
(swap! storage/user assoc ::current-team-id team-id)))
;; --- EVENT: fetch-teams
(defn teams-fetched
[teams]
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
ptk/UpdateEvent
(update [_ state]
(assoc state :teams (d/index-by :id teams)))
ptk/EffectEvent
(effect [_ _ _]
;; Check if current team-id is part of available teams
;; if not, dissoc it from storage.
(let [ids (into #{} (map :id) teams)]
(when-let [ctid (::current-team-id storage/user)]
(when-not (contains? ids ctid)
(swap! storage/user dissoc ::current-team-id)))))))
(defn fetch-teams
[]
(ptk/reify ::fetch-teams
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-teams)
(rx/map teams-fetched)))))
(defn set-current-team
[team]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :team team)
(assoc :permissions (:permissions team))
(assoc :current-team-id (:id team))))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (:features team #{}))))
ptk/EffectEvent
(effect [_ _ _]
(set-current-team! (:id team)))))
;; --- EVENT: fetch-profile
(declare logout)
(def profile-fetched?
(ptk/type? ::profile-fetched))
(defn profile-fetched
(defn initialize-profile
"Initialize profile state, only logged-in profile data should be
passed to this event"
[{:keys [id] :as profile}]
(ptk/reify ::profile-fetched
(ptk/reify ::initialize-profile
IDeref
(-deref [_] profile)
ptk/UpdateEvent
(update [_ state]
(cond-> state
(is-authenticated? profile)
(-> (assoc :profile-id id)
(assoc :profile profile))))
(-> state
(assoc :profile-id id)
(assoc :profile profile)))
ptk/EffectEvent
(effect [_ state _]
(let [profile (:profile state)
email (:email profile)
previous-profile (:profile storage/user)
previous-email (:email previous-profile)]
(when profile
(swap! storage/user assoc :profile profile)
(i18n/set-locale! (:lang profile))
(when (not= previous-email email)
(set-current-team! nil))
(let [profile (:profile state)]
(swap! storage/user assoc :profile profile)
(i18n/set-locale! (:lang profile))
(plugins.register/init)))))
(register/init))))))
(def profile-fetched?
(ptk/type? ::profile-fetched))
(defn- on-fetch-profile-exception
[cause]
@ -160,213 +90,9 @@
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-profile)
(rx/map profile-fetched)
(rx/map (partial ptk/data-event ::profile-fetched))
(rx/catch on-fetch-profile-exception)))))
;; --- 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]
(letfn [(get-redirect-events []
(let [team-id (get-current-team-id profile)
welcome-file-id (dm/get-in profile [:props :welcome-file-id])
redirect-href (:login-redirect @storage/session)
current-href (rt/get-current-href)]
(cond
(some? redirect-href)
(binding [storage/*sync* true]
(swap! storage/session dissoc :login-redirect)
(if (= current-href redirect-href)
(rx/of (rt/reload true))
(rx/of (rt/nav-raw :href redirect-href))))
(some? welcome-file-id)
(rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
:file-id welcome-file-id})
(update-profile-props {:welcome-file-id nil}))
:else
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
(ptk/reify ::logged-in
ev/Event
(-data [_]
{::ev/name "signin"
::ev/type "identify"
:email (:email profile)
:auth-backend (:auth-backend profile)
:fullname (:fullname profile)
:is-muted (:is-muted profile)
:default-team-id (:default-team-id profile)
:default-project-id (:default-project-id profile)})
ptk/WatchEvent
(watch [_ _ _]
(when (is-authenticated? profile)
(->> (rx/concat
(rx/of (profile-fetched profile)
(fetch-teams)
(ws/initialize))
(get-redirect-events))
(rx/observe-on :async)))))))
(declare login-from-register)
(defn login
[{:keys [email password invitation-token] :as data}]
(ptk/reify ::login
ptk/WatchEvent
(watch [_ _ stream]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)
params {:email email
:password password
:invitation-token invitation-token}]
;; NOTE: We can't take the profile value from login because
;; there are cases when login is successful 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.
(->> (rp/cmd! :login-with-password (d/without-nils params))
(rx/merge-map (fn [data]
(rx/merge
(rx/of (fetch-profile))
(->> 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 (merge data profile)
{::ev/source "login"})))
(rx/tap on-success)
(rx/map logged-in)
(rx/observe-on :async)))))
(rx/catch on-error))))))
(def ^:private schema:login-with-ldap
[:map {:title "login-with-ldap"}
[:email ::sm/email]
[:password :string]])
(defn login-with-ldap
[params]
(dm/assert!
"expected valid params"
(sm/check schema:login-with-ldap params))
(ptk/reify ::login-with-ldap
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta params)]
(->> (rp/cmd! :login-with-ldap params)
(rx/tap on-success)
(rx/map (fn [profile]
(-> profile
(with-meta {::ev/source "login-with-ldap"})
(logged-in))))
(rx/catch on-error))))))
(defn login-from-token
"Used mainly as flow continuation after token validation."
[{:keys [profile] :as tdata}]
(ptk/reify ::login-from-token
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
;; NOTE: we need this to be asynchronous because the effect
;; should be called before proceed with the login process
(rx/observe-on :async)))
ptk/EffectEvent
(effect [_ _ _]
(set-current-team! nil))))
(defn login-from-register
"Event used mainly for mark current session as logged-in in after the
user successfully registered using third party auth provider (in this
case we dont need to verify the email)."
[]
(ptk/reify ::login-from-register
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (fetch-profile))
(->> stream
(rx/filter (ptk/type? ::profile-fetched))
(rx/take 1)
(rx/map deref)
(rx/map (fn [profile]
(with-meta profile
{::ev/source "register"})))
(rx/map logged-in)
(rx/observe-on :async))))))
;; --- EVENT: logout
(defn logged-out
([] (logged-out {}))
([_params]
(ptk/reify ::logged-out
ptk/UpdateEvent
(update [_ state]
(select-keys state [:route :router :session-id :history]))
ptk/WatchEvent
(watch [_ _ _]
(rx/merge
;; 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))
(rx/of (ws/finalize))))
ptk/EffectEvent
(effect [_ _ _]
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
(swap! storage/user (constantly {}))))))
(defn logout
([] (logout {}))
([params]
(ptk/reify ::logout
ev/Event
(-data [_] {})
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (:profile-id state)]
(->> (rx/interval 500)
(rx/take 1)
(rx/mapcat (fn [_]
(->> (rp/cmd! :logout {:profile-id profile-id})
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1))))))
(rx/map #(logged-out params))))))))
;; --- Update Profile
(defn persist-profile
@ -386,7 +112,7 @@
[data]
(dm/assert!
"expected valid profile data"
(check-profile! data))
(check-profile data))
(ptk/reify ::update-profile
ptk/WatchEvent
@ -589,6 +315,9 @@
;; --- EVENT: request-account-deletion
(def profile-deleted?
(ptk/type? ::profile-deleted))
(defn request-account-deletion
[params]
(ptk/reify ::request-account-deletion
@ -599,7 +328,8 @@
on-success identity}} (meta params)]
(->> (rp/cmd! :delete-profile {})
(rx/tap on-success)
(rx/map logged-out)
(rx/map (fn [_]
(ptk/data-event ::profile-deleted params)))
(rx/catch on-error)
(rx/delay-at-least 300))))))
@ -652,16 +382,6 @@
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: crete-demo-profile
(defn create-demo-profile
[]
(ptk/reify ::create-demo-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :create-demo-profile {})
(rx/map login)))))
;; --- EVENT: fetch-team-webhooks
(defn access-tokens-fetched
@ -716,28 +436,3 @@
(rx/tap on-success)
(rx/catch on-error))))))
(defn show-redirect-error
"A helper event that interprets the OIDC redirect errors on the URI
and shows an appropriate error message using the notification
banners."
[error]
(ptk/reify ::show-redirect-error
ptk/WatchEvent
(watch [_ _ _]
(when-let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
;; We explicitly do not show any error here, it a explicit user operation.
"unable-to-auth"
nil
(tr "errors.generic"))]
(rx/of (ntf/warn hint))))))

View file

@ -0,0 +1,80 @@
;; 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) KALEIDOS INC
(ns app.main.data.project
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(log/set-level! :warn)
(defn- project-fetched
[{:keys [id] :as project}]
(ptk/reify ::project-fetched
ptk/UpdateEvent
(update [_ state]
(update-in state [:projects id] merge project))))
(defn fetch-project
"Fetch or refresh a single project"
([] (fetch-project))
([project-id]
(assert (uuid? project-id) "expected a valid uuid for `project-id`")
(ptk/reify ::fetch-project
ptk/WatchEvent
(watch [_ state _]
(let [project-id (or project-id (:current-project-id state))]
(->> (rp/cmd! :get-project {:id project-id})
(rx/map project-fetched)))))))
(defn initialize-project
[project-id]
(ptk/reify ::initialize-project
ptk/UpdateEvent
(update [_ state]
(assoc state :current-project-id project-id))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (fetch-project project-id)))))
(defn finalize-project
[project-id]
(ptk/reify ::finalize-project
ptk/UpdateEvent
(update [_ state]
(let [project-id' (get state :current-project-id)]
(if (= project-id' project-id)
(dissoc state :current-project-id)
state)))))
(defn- files-fetched
[project-id files]
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(update :files merge (d/index-by :id files))
(d/update-in-when [:projects project-id] (fn [project]
(assoc project :count (count files))))))))
(defn fetch-files
[project-id]
(assert (uuid? project-id) "expected valid uuid for `project-id`")
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-project-files {:project-id project-id})
(rx/map (partial files-fetched project-id))))))

View file

@ -0,0 +1,559 @@
;; 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) KALEIDOS INC
(ns app.main.data.team
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.media :as di]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.util.storage :as storage]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(log/set-level! :warn)
(defn get-last-team-id
"Get last accessed team id"
[]
(::current-team-id storage/global))
(defn teams-fetched
[teams]
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as team}]
(update-in state [:teams id] merge team))
state
teams))))
(defn fetch-teams
[]
(ptk/reify ::fetch-teams
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-teams)
(rx/map teams-fetched)))))
;; --- EVENT: fetch-members
(defn- members-fetched
[team-id members]
(ptk/reify ::members-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:teams team-id] assoc :members members)
(update :profiles merge (d/index-by :id members))))))
(defn fetch-members
[]
(ptk/reify ::fetch-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map (partial members-fetched team-id)))))))
(defn- invitations-fetched
[team-id invitations]
(ptk/reify ::invitations-fetched
ptk/UpdateEvent
(update [_ state]
(update-in state [:teams team-id] assoc :invitations invitations))))
(defn fetch-invitations
[]
(ptk/reify ::fetch-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-invitations {:team-id team-id})
(rx/map (partial invitations-fetched team-id)))))))
(defn set-current-team
[{:keys [id permissions features] :as team}]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
;; FIXME: redundant operation, only necessary on workspace
;; until workspace initialization is refactored
(update-in [:teams id] merge team)
(assoc :permissions permissions)
;; FIXME: this is a redundant operation that only needed by
;; workspace; ti will not be needed after workspace
;; bootstrap & urls refactor
(assoc :current-team-id id)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (or features #{}))))
ptk/EffectEvent
(effect [_ _ _]
(swap! storage/global assoc ::current-team-id id))))
(defn- team-initialized
[]
(ptk/reify ::team-initialized
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
teams (get state :teams)
team (get teams team-id)]
(rx/of (set-current-team team)
(fetch-members))))))
(defn initialize-team
[team-id]
(ptk/reify ::initialize-team
ptk/UpdateEvent
(update [_ state]
(assoc state :current-team-id team-id))
ptk/WatchEvent
(watch [_ _ stream]
(let [stopper (rx/filter (ptk/type? ::finalize-team) stream)]
(->> (rx/merge
(rx/of (fetch-teams))
(->> stream
(rx/filter (ptk/type? ::teams-fetched))
(rx/observe-on :async)
(rx/map team-initialized)))
(rx/take-until stopper))))))
(defn finalize-team
[team-id]
(ptk/reify ::finalize-team
ptk/UpdateEvent
(update [_ state]
(let [team-id' (get state :current-team-id)]
(if (= team-id' team-id)
(-> state
(dissoc :current-team-id)
(dissoc :shared-files)
(dissoc :fonts))
state)))))
;; --- ROLES
(defn update-member-role
[{:keys [role member-id] :as params}]
(dm/assert! (uuid? member-id))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-member-role
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :update-team-member-role params)
(rx/mapcat (fn [_]
(rx/of (fetch-members)
(fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "update-team-member-role"
:team-id team-id
:role role
:member-id member-id})))))))))
(defn delete-member
[{:keys [member-id] :as params}]
(dm/assert! (uuid? member-id))
(ptk/reify ::delete-member
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :delete-team-member params)
(rx/mapcat (fn [_]
(rx/of (fetch-members)
(fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "delete-team-member"
:team-id team-id
:member-id member-id})))))))))
(defn- stats-fetched
[team-id stats]
(ptk/reify ::stats-fetched
ptk/UpdateEvent
(update [_ state]
(update-in state [:teams team-id] assoc :stats stats))))
(defn fetch-stats
[]
(ptk/reify ::fetch-stats
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map (partial stats-fetched team-id)))))))
(defn- webhooks-fetched
[team-id webhooks]
(ptk/reify ::webhooks-fetched
ptk/UpdateEvent
(update [_ state]
(update-in state [:team-id team-id] assoc :webhooks webhooks))))
(defn fetch-webhooks
[]
(ptk/reify ::fetch-webhooks
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-webhooks {:team-id team-id})
(rx/map (partial webhooks-fetched team-id)))))))
(defn- shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [files (d/index-by :id files)]
(assoc state :shared-files files)))))
(defn fetch-shared-files
"Event mainly used for fetch a list of shared libraries for a team,
this list does not includes the content of the library per se. It
is used mainly for show available libraries and a summary of it."
[]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
(rx/map shared-files-fetched))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn update-team-photo
[file]
(dm/assert!
"expected a valid blob for `file` param"
(di/blob? file))
(ptk/reify ::update-team-photo
ptk/WatchEvent
(watch [_ state _]
(let [on-success di/notify-finished-loading
on-error #(do (di/notify-finished-loading)
(di/process-error %))
team-id (:current-team-id state)
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/cmd! :update-team-photo %))
(rx/tap on-success)
(rx/mapcat (fn [_]
(rx/of (fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "update-team-photo"
:team-id team-id}))))
(rx/catch on-error))))))
;; --- EVENT: create-team
(defn- team-created
[team]
(ptk/reify ::team-created
IDeref
(-deref [_] team)))
(defn create-team
[{:keys [name] :as params}]
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
params {:name name :features features}]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: create-team-with-invitations
(defn create-team-with-invitations
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
params {:name name
:emails emails
:role role
:features features}]
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
(defn update-team
[{:keys [id name] :as params}]
(ptk/reify ::update-team
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:teams id :name] name))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :update-team params)
(rx/ignore)))))
(defn- team-leaved
[{:keys [id] :as params}]
(ptk/reify ::team-leaved
IDeref
(-deref [_] params)
ptk/UpdateEvent
(update [_ state]
(update state :teams dissoc id))
ptk/EffectEvent
(effect [_ state _]
(let [teams (get state :teams)]
(when-let [ctid (::current-team-id storage/user)]
(when-not (contains? teams ctid)
(swap! storage/user dissoc ::current-team-id)))))))
(defn leave-current-team
"High-level event for leave team, mainly executed from the
dashboard. It automatically redirects user to the default team, once
the team-leave operation succeed"
[{:keys [reassign-to] :as params}]
(when reassign-to
(assert (uuid? reassign-to) "expect a valid uuid for `reassign-to`"))
(ptk/reify ::leave-current-team
ptk/WatchEvent
(watch [_ state _]
(let [team-id (get state :current-team-id)
params (assoc params :id team-id)
{:keys [on-error on-success]
:or {on-success rx/empty
on-error rx/throw}}
(meta params)]
(->> (rp/cmd! :leave-team params)
(rx/mapcat
(fn [_]
(rx/merge
(rx/of (team-leaved params)
(fetch-teams)
(ptk/data-event ::ev/event
{::ev/name "leave-team"
:reassign-to reassign-to
:team-id team-id}))
(on-success))))
(rx/catch on-error))))))
(defn create-invitations
[{:keys [emails role team-id resend?] :as params}]
(dm/assert! (keyword? role))
(dm/assert! (uuid? team-id))
(dm/assert!
"expected a valid set of emails"
(sm/check-set-of-emails! emails))
(ptk/reify ::create-invitations
ev/Event
(-data [_]
{:role role
:team-id team-id
:resend resend?})
ptk/WatchEvent
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params (dissoc params :resend?)]
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/catch on-error))))))
(defn copy-invitation-link
[{:keys [email team-id] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(ptk/reify ::copy-invitation-link
IDeref
(-deref [_] {:email email :team-id team-id})
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
router (:router state)]
(->> (rp/cmd! :get-team-invitation-token params)
(rx/map (fn [params]
(rt/resolve router :auth-verify-token params)))
(rx/map (fn [fragment]
(assoc cf/public-uri :fragment fragment)))
(rx/tap (fn [uri]
(wapi/write-to-clipboard (str uri))))
(rx/tap on-success)
(rx/ignore)
(rx/catch on-error))))))
(defn update-invitation-role
[{:keys [email team-id role] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-invitation-role
IDeref
(-deref [_] {:role role})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :update-team-invitation-role params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-invitation
[{:keys [email team-id] :as params}]
(dm/assert! (sm/check-email! email))
(dm/assert! (uuid? team-id))
(ptk/reify ::delete-invitation
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :delete-team-invitation params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-team
[{:keys [id] :as params}]
(ptk/reify ::delete-team
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success rx/empty
on-error rx/throw}}
(meta params)]
(->> (rp/cmd! :delete-team {:id id})
(rx/mapcat on-success)
(rx/catch on-error))))))
(defn delete-webhook
[{:keys [id] :as params}]
(dm/assert! (uuid? id))
(ptk/reify ::delete-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :delete-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
(def valid-mtypes
#{"application/json"
"application/x-www-form-urlencoded"
"application/transit+json"})
(defn update-webhook
[{:keys [id uri mtype is-active] :as params}]
(dm/assert! (uuid? id))
(dm/assert! (contains? valid-mtypes mtype))
(dm/assert! (boolean? is-active))
(dm/assert! (u/uri? uri))
(ptk/reify ::update-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)
{:keys [on-success on-error]
:or {on-success rx/empty
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :update-webhook params)
(rx/mapcat (fn [_]
(rx/concat
(on-success)
(rx/of (fetch-webhooks)))))
(rx/catch on-error))))))
(defn create-webhook
[{:keys [uri mtype is-active] :as params}]
(dm/assert! (contains? valid-mtypes mtype))
(dm/assert! (boolean? is-active))
(dm/assert! (u/uri? uri))
(ptk/reify ::create-webhook
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (-> params
(assoc :team-id team-id)
(update :uri str))
{:keys [on-success on-error]
:or {on-success rx/empty
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :create-webhook params)
(rx/mapcat (fn [_]
(rx/concat
(on-success)
(rx/of (fetch-webhooks)))))
(rx/catch on-error))))))

View file

@ -15,13 +15,15 @@
[app.common.transit :as t]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.interactions :as ctsi]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.common.uuid :as uuid]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.util.globals :as ug]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -32,7 +34,7 @@
{:zoom 1
:fullscreen? false
:interactions-mode :show-on-click
:interactions-show? false
:show-interactions false
:comments-mode :all
:comments-show :unresolved
:selected #{}
@ -55,7 +57,7 @@
[:page-id {:optional true} ::sm/uuid]])
(defn initialize
[{:keys [file-id share-id interactions-show?] :as params}]
[{:keys [file-id share-id] :as params}]
(dm/assert!
"expected valid params"
(sm/check schema:initialize params))
@ -65,13 +67,13 @@
(update [_ state]
(-> state
(assoc :current-file-id file-id)
(assoc :current-share-id share-id)
(update :viewer-local
(fn [lstate]
(if (nil? lstate)
default-local-state
lstate)))
(assoc-in [:viewer-local :share-id] share-id)
(assoc-in [:viewer-local :interactions-show?] interactions-show?)))
(assoc-in [:viewer-local :share-id] share-id)))
ptk/WatchEvent
(watch [_ state _]
@ -256,12 +258,9 @@
ptk/WatchEvent
(watch [_ state _]
(let [zoom-type (get-in state [:viewer-local :zoom-type])
route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
params (rt/get-params state)]
(rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type)))))))
(rx/of (rt/nav :viewer (assoc params :zoom zoom-type)))))))
(def increase-zoom
(ptk/reify ::increase-zoom
@ -293,14 +292,17 @@
(ptk/reify ::zoom-to-fit
ptk/UpdateEvent
(update [_ state]
(let [srect (as-> (get-in state [:route :query-params :page-id]) %
(get-in state [:viewer :pages % :frames])
(nth % (get-in state [:route :query-params :index]))
(get % :selrect))
orig-size (get-in state [:viewer-local :viewport-size])
wdiff (/ (:width orig-size) (:width srect))
hdiff (/ (:height orig-size) (:height srect))
minzoom (min wdiff hdiff)]
(let [params (rt/get-params state)
page-id (some-> (:page-id params) uuid/parse)
index (some-> (:index params) parse-long)
frames (dm/get-in state [:viewer :pages page-id :frames])
srect (-> (nth frames index)
(get :selrect))
osize (dm/get-in state [:viewer-local :viewport-size])
wdiff (/ (:width osize) (:width srect))
hdiff (/ (:height osize) (:height srect))
minzoom (min wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] minzoom)
(assoc-in [:viewer-local :zoom-type] :fit))))
@ -312,17 +314,25 @@
(ptk/reify ::zoom-to-fill
ptk/UpdateEvent
(update [_ state]
(let [srect (as-> (get-in state [:route :query-params :page-id]) %
(get-in state [:viewer :pages % :frames])
(nth % (get-in state [:route :query-params :index]))
(get % :selrect))
orig-size (get-in state [:viewer-local :viewport-size])
wdiff (/ (:width orig-size) (:width srect))
hdiff (/ (:height orig-size) (:height srect))
maxzoom (max wdiff hdiff)]
(let [params (rt/get-params state)
page-id (some-> (:page-id params) uuid/parse)
index (some-> (:index params) parse-long)
frames (dm/get-in state [:viewer :pages page-id :frames])
srect (-> (nth frames index)
(get :selrect))
osize (dm/get-in state [:viewer-local :viewport-size])
wdiff (/ (:width osize) (:width srect))
hdiff (/ (:height osize) (:height srect))
maxzoom (max wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] maxzoom)
(assoc-in [:viewer-local :zoom-type] :fill))))
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
@ -376,16 +386,15 @@
(-> state
(dissoc :viewer-animations)
(assoc :viewer-overlays [])))
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
qparams (:query-params route)
pparams (:path-params route)
index (:index qparams)]
(let [params (rt/get-params state)
index (some-> params :index parse-long)]
(when (pos? index)
(rx/of
(dcm/close-thread)
(rt/nav :viewer pparams (assoc qparams :index (dec index)))))))))
(dcmt/close-thread)
(rt/nav :viewer (assoc params :index (dec index)))))))))
(def select-next-frame
(ptk/reify ::select-next-frame
@ -396,30 +405,25 @@
(assoc :viewer-overlays [])))
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
pparams (:path-params route)
qparams (:query-params route)
page-id (:page-id qparams)
index (:index qparams)
(let [params (rt/get-params state)
index (some-> params :index parse-long)
page-id (some-> params :page-id parse-uuid)
total (count (get-in state [:viewer :pages page-id :frames]))]
(when (< index (dec total))
(rx/of
(dcm/close-thread)
(rt/nav :viewer pparams (assoc qparams :index (inc index)))))))))
(dcmt/close-thread)
(rt/nav :viewer params (assoc params :index (inc index)))))))))
(def select-first-frame
(ptk/reify ::select-first-frame
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
qparams (:query-params route)
pparams (:path-params route)]
(let [params (rt/get-params state)]
(rx/of
(dcm/close-thread)
(rt/nav :viewer pparams (assoc qparams :index 0)))))))
(dcmt/close-thread)
(rt/nav :viewer (assoc params :index 0)))))))
(def valid-interaction-modes
#{:hide :show :show-on-click})
@ -434,17 +438,14 @@
(update [_ state]
(-> state
(assoc-in [:viewer-local :interactions-mode] mode)
(assoc-in [:viewer-local :interactions-show?] (case mode
:hide false
:show true
:show-on-click false))))
(assoc-in [:viewer-local :show-interactions] (case mode
:hide false
:show true
:show-on-click false))))
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :interactions-mode mode)))))))
(let [params (rt/get-params state)]
(rx/of (rt/nav :viewer (assoc params :interactions-mode mode)))))))
(declare flash-done)
@ -453,7 +454,7 @@
(ptk/reify ::flash-interactions
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :interactions-show?] true))
(assoc-in state [:viewer-local :show-interactions] true))
ptk/WatchEvent
(watch [_ _ stream]
@ -466,7 +467,7 @@
(ptk/reify ::flash-done
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :interactions-show?] false))))
(assoc-in state [:viewer-local :show-interactions] false))))
(defn set-nav-scroll
[scroll]
@ -500,11 +501,8 @@
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :index index)))))))
(let [params (rt/get-params state)]
(rx/of (rt/nav :viewer (assoc params :index index)))))))
(defn go-to-frame
([frame-id]
@ -573,10 +571,8 @@
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
pparams (:path-params route)
qparams (:query-params route)]
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
(let [params (rt/get-params state)]
(rx/of (rt/nav :viewer (assoc params :section section)))))))
;; --- Overlays
@ -771,9 +767,8 @@
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state _]
(let [team-id (get-in state [:viewer :project :team-id])
params {:team-id team-id}]
(rx/of (rt/nav :dashboard-projects params))))))
(let [team-id (get-in state [:viewer :project :team-id])]
(rx/of (dcm/go-to-dashboard-recent :team-id team-id))))))
(defn go-to-page
[page-id]
@ -784,13 +779,10 @@
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
pparams (:path-params route)
qparams (-> (:query-params route)
(assoc :index 0)
(assoc :page-id page-id))
rname (get-in route [:data :name])]
(rx/of (rt/nav rname pparams qparams))))))
(let [params (-> (rt/get-params state)
(assoc :index 0)
(assoc :page-id page-id))]
(rx/of (rt/nav :viewer params))))))
(defn go-to-workspace
([] (go-to-workspace nil))
@ -798,14 +790,16 @@
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
project-id (get-in state [:viewer :project :id])
file-id (get-in state [:viewer :file :id])
saved-page-id (get-in route [:query-params :page-id])
pparams {:project-id project-id :file-id file-id}
qparams {:page-id (or page-id saved-page-id)}]
(rx/of (rt/nav-new-window*
{:rname :workspace
:path-params pparams
:query-params qparams
:name (str "workspace-" file-id)})))))))
(let [params (rt/get-params state)
file-id (get-in state [:viewer :file :id])
team-id (get-in state [:viewer :project :team-id])
page-id (or page-id
(some-> (:page-id params) uuid/parse))
params {:page-id page-id
:file-id file-id
:team-id team-id}
name (dm/str "workspace-" file-id)]
(rx/of (rt/nav :workspace params
::rt/new-window true
::rt/window-name name)))))))

View file

@ -6,6 +6,7 @@
(ns app.main.data.viewer.shortcuts
(:require
[app.main.data.common :as dcm]
[app.main.data.shortcuts :as ds]
[app.main.data.viewer :as dv]
[app.main.store :as st]))
@ -69,7 +70,7 @@
:open-workspace {:tooltip "G W"
:command "g w"
:subsections [:navigation-viewer]
:fn #(st/emit! (dv/go-to-workspace))}})
:fn #(st/emit! (dcm/go-to-workspace))}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))

View file

@ -36,16 +36,18 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dps]
[app.main.data.plugins :as dp]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.data.project :as dpj]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.collapse :as dwco]
[app.main.data.workspace.colors :as dwcl]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.fix-broken-shapes :as fbs]
@ -74,6 +76,7 @@
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.render-wasm :as wasm]
@ -81,7 +84,6 @@
[app.util.globals :as ug]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as storage]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
@ -100,12 +102,15 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized)
(declare ^:private fetch-libraries)
(declare ^:private libraries-fetched)
(declare go-to-layout)
(declare ^:private preload-data-uris)
;; (declare go-to-layout)
;; --- Initialize Workspace
(defn initialize-layout
(defn initialize-workspace-layout
[lname]
(ptk/reify ::initialize-layout
ptk/UpdateEvent
@ -120,104 +125,45 @@
(rx/of (layout/ensure-layout lname))
(rx/of (layout/ensure-layout :layers))))))
(defn- workspace-initialized
[]
(ptk/reify ::workspace-initialized
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :workspace-undo {})
(assoc :workspace-ready? true)))
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get})
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
ptk/WatchEvent
(watch [_ state _]
(rx/of
(when (and (not (boolean (-> state :profile :props :v2-info-shown)))
(features/active-feature? state "components/v2"))
(modal/show :v2-info {}))
(dp/check-open-plugin)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes)))))
(defn- get-file-object-thumbnails
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (str/starts-with? v "data:")
(->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- workspace-data-loaded
[data]
(ptk/reify ::workspace-data-loaded
ptk/UpdateEvent
(update [_ state]
(let [data (d/removem (comp t/pointer? val) data)]
(assoc state :workspace-data data)))))
(defn- bundle-fetched
[{:keys [features file thumbnails project team team-users comments-users]}]
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :users (d/index-by :id team-users))
(assoc :workspace-thumbnails thumbnails)
(assoc :workspace-file (dissoc file :data))
(assoc :workspace-project project)
(assoc :current-file-comments-users (d/index-by :id comments-users))))
ptk/WatchEvent
(watch [_ _ stream]
(let [team-id (:id team)
file-id (:id file)
stopper (rx/filter (ptk/type? ::bundle-fetched) stream)]
(->> (rx/concat
;; Initialize notifications
(rx/of (dwn/initialize team-id file-id)
(dwsl/initialize))
;; Load team fonts. We must ensure custom fonts are
;; fully loadad before mark workspace as initialized
(rx/merge
(->> stream
(rx/filter (ptk/type? ::df/team-fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (df/load-team-fonts team-id))
;; FIXME: move to bundle fetch stages
;; Load main file
(->> (fpmap/resolve-file file)
(rx/map :data)
(rx/mapcat (fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index))
(rx/mapcat
(fn [[id page]]
(let [page (update page :objects ctst/start-page-index)]
(->> (uw/ask! {:cmd :initialize-page-index :page page})
(rx/map (fn [_] [id page]))))))
(rx/reduce conj {})
(rx/map (fn [pages-index]
(assoc data :pages-index pages-index))))))
(rx/map workspace-data-loaded))
;; Load libraries
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat (fn [libraries]
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at)))))
(rx/merge-map fpmap/resolve-file)
(rx/reduce conj [])
(rx/map libraries-fetched))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))))))
(rx/of (with-meta (workspace-initialized)
{:file-id file-id})))
(rx/take-until stopper))))))
(defn- resolve-file
[file]
(->> (fpmap/resolve-file file)
(rx/map :data)
(rx/mapcat
(fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index))
(rx/mapcat
(fn [[id page]]
(let [page (update page :objects ctst/start-page-index)]
(->> (uw/ask! {:cmd :initialize-page-index :page page})
(rx/map (fn [_] [id page]))))))
(rx/reduce conj {})
(rx/map (fn [pages-index]
(let [data (assoc data :pages-index pages-index)]
(assoc file :data (d/removem (comp t/pointer? val) data))))))))))
(defn- libraries-fetched
[libraries]
@ -238,180 +184,180 @@
(rx/concat (rx/timer 1000)
(rx/of (dwl/notify-sync-file file-id))))))))
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get})
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
(defn- fetch-file-object-thumbnails
(defn- fetch-libraries
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (str/starts-with? v "data:")
(->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- fetch-bundle-stage-1
[project-id file-id]
(ptk/reify ::fetch-bundle-stage-1
(ptk/reify ::fetch-libries
ptk/WatchEvent
(watch [_ _ stream]
(->> (rp/cmd! :get-project {:id project-id})
(rx/mapcat (fn [project]
(rx/concat
;; Wait the wasm module to be loaded or failed to
;; load. We need to wait the promise to be resolved
;; before continue with the next workspace loading
;; steps
(->> (rx/from wasm/module)
(rx/ignore))
(->> (rp/cmd! :get-team {:id (:team-id project)})
(rx/mapcat (fn [team]
(let [bundle {:team team
:project project
:file-id file-id
:project-id project-id}]
(rx/of (du/set-current-team team)
(ptk/data-event ::bundle-stage-1 bundle)))))))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(watch [_ state _]
(let [features (features/get-team-enabled-features state)]
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at)))))
(rx/merge-map resolve-file)
(rx/reduce conj [])
(rx/map libraries-fetched))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched))))))))))
(defn- workspace-initialized
[]
(ptk/reify ::workspace-initialized
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :workspace-undo {})
(assoc :workspace-ready true)))
(defn- fetch-bundle-stage-2
[{:keys [file-id project-id] :as bundle}]
(ptk/reify ::fetch-bundle-stage-2
ptk/WatchEvent
(watch [_ state stream]
(let [features (features/get-team-enabled-features state)
(watch [_ state _]
(rx/of
(when (and (not (boolean (-> state :profile :props :v2-info-shown)))
(features/active-feature? state "components/v2"))
(modal/show :v2-info {}))
(dp/check-open-plugin)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes)))))
;; WTF is this?
share-id (-> state :viewer-local :share-id)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id})
(fetch-file-object-thumbnails file-id)
(rp/cmd! :get-team-users {:file-id file-id})
(rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
(rx/take 1)
(rx/map (fn [[file thumbnails team-users comments-users]]
(let [bundle (-> bundle
(assoc :file file)
(assoc :features features)
(assoc :thumbnails thumbnails)
(assoc :team-users team-users)
(assoc :comments-users comments-users))]
(ptk/data-event ::bundle-stage-2 bundle))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream)))))))
(defn- bundle-fetched
[{:keys [features file thumbnails]}]
(ptk/reify ::bundle-fetched
IDeref
(-deref [_]
{:features features
:file file
:thumbnails thumbnails})
(declare go-to-component)
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :thumbnails thumbnails)
(assoc :workspace-file (dissoc file :data))
(assoc :workspace-data (:data file))))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
file-id (:id file)]
(rx/of (dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id))))))
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[project-id file-id]
[file-id]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state stream]
(->> (rx/merge
(rx/of (fetch-bundle-stage-1 project-id file-id))
(let [features (features/get-team-enabled-features state)
render-wasm? (contains? features "render-wasm/v1")
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> stream
(rx/filter (ptk/type? ::bundle-stage-1))
(rx/observe-on :async)
(rx/map deref)
(rx/map fetch-bundle-stage-2))
(->> (rx/concat
;; Firstly load wasm module if it is enabled and fonts
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::bundle-stage-2))
(rx/observe-on :async)
(rx/map deref)
(rx/map bundle-fetched))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (df/fetch-fonts)))
(when-let [component-id (get-in state [:route :query-params :component-id])]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
;; Then fetch file and thumbnails
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(get-file-object-thumbnails file-id))
(rx/take 1)
(rx/map #(go-to-component (uuid/uuid component-id))))))
(rx/mapcat
(fn [[file thumbnails]]
(->> (resolve-file file)
(rx/map (fn [file]
{:file file
:features features
:thumbnails thumbnails})))))
(rx/map bundle-fetched)))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(rx/take-until stopper-s))))))
(defn initialize-file
[project-id file-id]
(dm/assert! (uuid? project-id))
(dm/assert! (uuid? file-id))
(defn initialize-workspace
[file-id]
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-file
(ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
(assoc state
:recent-colors (:recent-colors storage/user)
:workspace-ready? false
:workspace-ready false
:current-file-id file-id
:current-project-id project-id
:workspace-presence {}))
ptk/WatchEvent
(watch [_ _ stream]
(log/debug :hint "initialize-file" :file-id file-id)
(let [stoper-s (rx/filter (ptk/type? ::finalize-file) stream)]
(rx/merge
(rx/of (ntf/hide)
;; We initialize the features without knowning the
;; team specific features in this step.
(features/initialize)
(dcm/retrieve-comment-threads file-id)
(fetch-bundle project-id file-id))
(watch [_ state stream]
(log/debug :hint "initialize-workspace" :file-id file-id)
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)]
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))
(->> (rx/merge
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(fetch-bundle file-id))
(rx/take-until stoper-s)))))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat (fn [{:keys [file]}]
(rx/of (dpj/initialize-project (:project-id file))
(-> (workspace-initialized)
(with-meta {:file-id file-id}))))))
(when-let [component-id (some-> rparams :component-id parse-uuid)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn reload-file
[]
(ptk/reify ::reload-file
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
project-id (:current-project-id state)]
(rx/of (initialize-file project-id file-id))))))
;; We need to inject this so there are no cycles
(set! app.main.data.workspace.notifications/reload-file reload-file)
(set! app.main.errors/reload-file reload-file)
(defn finalize-file
[_project-id file-id]
(defn finalize-workspace
[file-id]
(ptk/reify ::finalize-file
ptk/UpdateEvent
(update [_ state]
(-> state
(dissoc
:current-file-id
:current-project-id
:workspace-data
:workspace-editor-state
:workspace-file
@ -419,23 +365,37 @@
:workspace-media-objects
:workspace-persistence
:workspace-presence
:workspace-project
:workspace-ready?
:workspace-ready
:workspace-undo)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwn/finalize file-id)
(dwsl/finalize)))))
(watch [_ state _]
(let [project-id (:current-project-id state)]
(declare go-to-page)
(declare ^:private preload-data-uris)
(rx/of (dwn/finalize file-id)
(dpj/finalize-project project-id)
(dwsl/finalize-shape-layout)
(dwcl/stop-picker)
(modal/hide)
(ntf/hide))))))
(defn- reload-current-file
[]
(ptk/reify ::reload-current-file
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)]
(rx/of (initialize-workspace file-id))))))
;; Make this event callable through dynamic resolution
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
(defn initialize-page
[page-id]
(dm/assert! (uuid? page-id))
(assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
@ -450,30 +410,20 @@
;; FIXME: this should be done on `initialize-layout` (?)
(update :workspace-layout layout/load-layout-flags)
(update :workspace-global layout/load-layout-state)
(update :workspace-global assoc :background-color (-> page :options :background))
(update-in [:route :params :query] assoc :page-id (dm/str id))))
(update :workspace-global layout/load-layout-state)))
state))
ptk/WatchEvent
(watch [_ state _]
;; NOTE: there are cases between files navigation when this
;; event is emmited but the page-index is still not loaded, so
;; we only need to proceed when page-index is properly loaded
(when-let [pindex (-> state :workspace-data :pages-index)]
(if (contains? pindex page-id)
(let [file-id (:current-file-id state)]
(rx/of (preload-data-uris page-id)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes)))
(let [page-id (dm/get-in state [:workspace-data :pages 0])]
(rx/of (go-to-page page-id))))))))
(let [file-id (:current-file-id state)]
(rx/of (preload-data-uris page-id)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes))))))
(defn finalize-page
[page-id]
(dm/assert! (uuid? page-id))
(assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::finalize-page
ptk/UpdateEvent
(update [_ state]
@ -669,7 +619,7 @@
(rx/of (dch/commit-changes changes)
(when (= id (:current-page-id state))
go-to-file))))))
(go-to-file)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WORKSPACE File Actions
@ -833,7 +783,7 @@
(let [selected (wsh/lookup-selected state)
id (first selected)]
(when (= (count selected) 1)
(rx/of (go-to-layout :layers)
(rx/of (dcm/go-to-workspace :layout :layers)
(start-rename-shape id)))))))
;; --- Shape Vertical Ordering
@ -1097,76 +1047,17 @@
(rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true)))
(rx/of (dwsh/update-shapes selected #(update % :proportion-lock not))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn workspace-focus-lost
[]
(ptk/reify ::workspace-focus-lost
ptk/UpdateEvent
(update [_ state]
;; FIXME: remove the `?` from show-distances?
(assoc-in state [:workspace-global :show-distances?] false))))
(defn navigate-to-project
[project-id]
(ptk/reify ::navigate-to-project
ptk/WatchEvent
(watch [_ state _]
(let [page-ids (get-in state [:projects project-id :pages])
params {:project project-id :page (first page-ids)}]
(rx/of (rt/nav :workspace/page params))))))
(defn go-to-page
([]
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state _]
(let [project-id (:current-project-id state)
file-id (:current-file-id state)
page-id (get-in state [:workspace-data :pages 0])
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav' :workspace pparams qparams))))))
([page-id]
(dm/assert! (uuid? page-id))
(ptk/reify ::go-to-page-2
ptk/WatchEvent
(watch [_ state _]
(let [project-id (:current-project-id state)
file-id (:current-file-id state)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams)))))))
(defn go-to-layout
[layout]
(ptk/reify ::go-to-layout
IDeref
(-deref [_] {:layout layout})
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-file :id])
page-id (get state :current-page-id)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id :layout (name layout)}]
(rx/of (rt/nav :workspace pparams qparams))))))
(defn navigate-to-library
"Open a new tab, and navigate to the workspace with the provided file"
[library-id]
(ptk/reify ::navigate-to-file
ptk/WatchEvent
(watch [_ state _]
(when-let [file (dm/get-in state [:workspace-libraries library-id])]
(let [params {:rname :workspace
:path-params {:project-id (:project-id file)
:file-id (:id file)}
:query-params {:page-id (dm/get-in file [:data :pages 0])}}]
(rx/of (rt/nav-new-window* params)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-assets-section-open
[file-id section open?]
@ -1228,110 +1119,18 @@
(update-in state [:workspace-assets :selected] dissoc file-id)
(update state :workspace-assets dissoc :selected))))))
(defn go-to-main-instance
[file-id component-id]
(dm/assert!
"expected uuid type for `file-id` parameter (nilable)"
(or (nil? file-id)
(uuid? file-id)))
(dm/assert!
"expected uuid type for `component-id` parameter"
(uuid? component-id))
(ptk/reify ::go-to-main-instance
ptk/WatchEvent
(watch [_ state stream]
(let [current-file-id (:current-file-id state)
current-page-id (:current-page-id state)
current-project-id (:current-project-id state)
file-id (or file-id current-file-id)
select-and-zoom
(fn [shape-id]
(rx/of (dws/select-shapes (d/ordered-set shape-id))
dwz/zoom-to-selected-shape))
redirect-to-page
(fn [page-id shape-id]
(rx/concat
(rx/of (go-to-page page-id))
(->> stream
(rx/filter (ptk/type? ::initialize-page))
(rx/take 1)
(rx/observe-on :async))
(select-and-zoom shape-id)))
redirect-to-file
(fn [file-id page-id]
(let [pparams {:file-id file-id :project-id current-project-id}
qparams {:page-id page-id}]
(rx/merge
(rx/of (rt/nav :workspace pparams qparams))
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/map meta)
(rx/filter #(= file-id (:file-id %)))
(rx/take 1)
(rx/observe-on :async)
(rx/map #(go-to-main-instance file-id component-id))))))]
(if (= file-id current-file-id)
(let [component (dm/get-in state [:workspace-data :components component-id])
page-id (:main-instance-page component)
shape-id (:main-instance-id component)]
(when (some? page-id)
(if (= page-id current-page-id)
(select-and-zoom shape-id)
(redirect-to-page page-id shape-id))))
(let [component (dm/get-in state [:workspace-libraries file-id :data :components component-id])]
(some->> (:main-instance-page component)
(redirect-to-file file-id))))))))
(defn go-to-component
[component-id]
(ptk/reify ::go-to-component
IDeref
(-deref [_] {:layout :assets})
ptk/WatchEvent
(watch [_ state _]
(let [components-v2 (features/active-feature? state "components/v2")]
(if components-v2
(rx/of (go-to-main-instance nil component-id))
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-file :id])
page-id (get state :current-page-id)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id :layout :assets}]
(rx/of (rt/nav :workspace pparams qparams)
(set-assets-section-open file-id :library true)
(set-assets-section-open file-id :components true)
(select-single-asset file-id component-id :components))))))
ptk/EffectEvent
(effect [_ state _]
(let [components-v2 (features/active-feature? state "components/v2")
wrapper-id (str "component-shape-id-" component-id)]
(when-not components-v2
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id))))))))
(defn show-component-in-assets
[component-id]
(ptk/reify ::show-component-in-assets
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-file :id])
page-id (get state :current-page-id)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id :layout :assets}
component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path]))
paths (map (fn [i] (cfh/join-path (take (inc i) component-path))) (range (count component-path)))]
(let [component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path]))
paths (map (fn [i] (cfh/join-path (take (inc i) component-path))) (range (count component-path)))
file-id (:current-file-id state)]
(rx/concat
(rx/from (map #(set-assets-group-open file-id :components % true) paths))
(rx/of (rt/nav :workspace pparams qparams)
(rx/of (dcm/go-to-workspace :layout :assets)
(set-assets-section-open file-id :library true)
(set-assets-section-open file-id :components true)
(select-single-asset file-id component-id :components)))))
@ -1341,55 +1140,6 @@
(let [wrapper-id (str "component-shape-id-" component-id)]
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))
(def go-to-file
(ptk/reify ::go-to-file
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [id project-id data] :as file} (:workspace-file state)
page-id (get-in data [:pages 0])
pparams {:project-id project-id :file-id id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
(defn go-to-viewer
([] (go-to-viewer {}))
([{:keys [file-id page-id section frame-id]}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [current-file-id current-page-id]} state
pparams {:file-id (or file-id current-file-id)}
qparams (cond-> {:page-id (or page-id current-page-id)}
(some? section)
(assoc :section section)
(some? frame-id)
(assoc :frame-id frame-id))]
(rx/of ::dps/force-persist
(rt/nav-new-window* {:rname :viewer
:path-params pparams
:query-params qparams
:name (str "viewer-" (:file-id pparams))})))))))
(defn go-to-dashboard
([] (go-to-dashboard nil))
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(rx/of ::dps/force-persist
(rt/nav :dashboard-projects {:team-id team-id})))))))
(defn go-to-dashboard-fonts
[]
(ptk/reify ::go-to-dashboard-fonts
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of ::dps/force-persist
(rt/nav :dashboard-fonts {:team-id team-id}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1625,8 +1375,8 @@
(rx/catch on-copy-error)
(rx/ignore)))
;; FIXME: this is to support Firefox versions below 116 that don't support `ClipboardItem`
;; after the version 116 is less common we could remove this.
;; FIXME: this is to support Firefox versions below 116 that don't support
;; `ClipboardItem` after the version 116 is less common we could remove this.
;; https://caniuse.com/?search=ClipboardItem
(->> (rx/from shapes)
(rx/merge-map (partial prepare-object objects frame-id))
@ -1909,7 +1659,8 @@
;; the pasted object doesn't fit we try to:
;;
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right and bottom in the original frame
;; - Respect the distance of the object to the right
;; and bottom in the original frame
(gpt/point paste-x paste-y))]
[frame-id delta (dec (count (:shapes selected-frame-obj)))]))

View file

@ -16,7 +16,7 @@
[app.common.types.shape :refer [check-stroke!]]
[app.common.types.shape.shadow :refer [check-shadow!]]
[app.main.broadcast :as mbc]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]

View file

@ -13,8 +13,9 @@
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwco]
[app.main.data.workspace.drawing :as dwd]
@ -23,7 +24,6 @@
[app.main.repo :as rp]
[app.main.streams :as ms]
[app.util.mouse :as mse]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -38,7 +38,7 @@
(watch [_ _ stream]
(let [stopper (rx/filter #(= ::finalize %) stream)]
(rx/merge
(rx/of (dcm/retrieve-comment-threads file-id))
(rx/of (dcmt/retrieve-comment-threads file-id))
(->> stream
(rx/filter mse/mouse-event?)
(rx/filter mse/mouse-click-event?)
@ -60,8 +60,8 @@
(watch [_ state _]
(let [local (:comments-local state)]
(cond
(:draft local) (rx/of (dcm/close-thread))
(:open local) (rx/of (dcm/close-thread))
(:draft local) (rx/of (dcmt/close-thread))
(:open local) (rx/of (dcmt/close-thread))
:else
(rx/of (dw/clear-edition-mode)
@ -78,19 +78,19 @@
(watch [_ state _]
(let [local (:comments-local state)]
(if (some? (:open local))
(rx/of (dcm/close-thread))
(rx/of (dcmt/close-thread))
(let [page-id (:current-page-id state)
file-id (:current-file-id state)
params {:position position
:page-id page-id
:file-id file-id}]
(rx/of (dcm/create-draft params))))))))
(rx/of (dcmt/create-draft params))))))))
(defn center-to-comment-thread
[{:keys [position] :as thread}]
(dm/assert!
"expected valid comment thread"
(dcm/check-comment-thread! thread))
(dcmt/check-comment-thread! thread))
(ptk/reify ::center-to-comment-thread
ptk/UpdateEvent
@ -109,22 +109,21 @@
[thread]
(dm/assert!
"expected valid comment thread"
(dcm/check-comment-thread! thread))
(dcmt/check-comment-thread! thread))
(ptk/reify ::open-comment-thread
ptk/WatchEvent
(watch [_ _ stream]
(let [pparams {:project-id (:project-id thread)
:file-id (:file-id thread)}
qparams {:page-id (:page-id thread)}]
(rx/merge
(rx/of (rt/nav :workspace pparams qparams))
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/mapcat #(rx/of (center-to-comment-thread thread)
(dwd/select-for-drawing :comments)
(with-meta (dcm/open-thread thread)
{::ev/origin "workspace"})))))))))
(rx/merge
(rx/of (dcm/go-to-workspace :file-id (:file-id thread)
:page-id (:page-id thread)))
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/mapcat #(rx/of (center-to-comment-thread thread)
(dwd/select-for-drawing :comments)
(with-meta (dcmt/open-thread thread)
{::ev/origin "workspace"}))))))))
(defn update-comment-thread-position
([thread [new-x new-y]]
@ -133,7 +132,7 @@
([thread [new-x new-y] frame-id]
(dm/assert!
"expected valid comment thread"
(dcm/check-comment-thread! thread))
(dcmt/check-comment-thread! thread))
(ptk/reify ::update-comment-thread-position
ptk/WatchEvent
(watch [it state _]

View file

@ -12,7 +12,7 @@
[app.common.geom.shapes :as gsh]
[app.common.types.page :as ctp]
[app.main.data.changes :as dwc]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))

View file

@ -17,7 +17,7 @@
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]

View file

@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.util.storage :as storage]
[clojure.set :as set]
[potok.v2.core :as ptk]))

View file

@ -27,7 +27,8 @@
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.workspace :as-alias dw]
@ -40,14 +41,15 @@
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.zoom :as dwz]
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.util.color :as uc]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as storage]
[app.util.time :as dt]
[beicon.v2.core :as rx]
@ -684,21 +686,49 @@
(rx/of (when can-detach?
(dch/commit-changes changes)))))))
(defn nav-to-component-file
(defn go-to-component-file
[file-id component]
(dm/assert! (uuid? file-id))
(dm/assert! (some? component))
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:workspace-libraries file-id :project-id])
path-params {:project-id project-id
:file-id file-id}
query-params {:page-id (:main-instance-page component)
:component-id (:id component)}]
(rx/of (rt/nav-new-window* {:rname :workspace
:path-params path-params
:query-params query-params}))))))
(let [params (-> (rt/get-params state)
(assoc :file-id file-id)
(assoc :page-id (:main-instance-page component))
(assoc :component-id (:id component)))]
(rx/of (rt/nav :workspace params :new-window? true))))))
(defn go-to-local-component
[& {:keys [id] :as options}]
(ptk/reify ::go-to-local-component
ptk/WatchEvent
(watch [_ state stream]
(let [current-page-id (:current-page-id state)
select-and-zoom
(fn [shape-id]
(rx/of (dws/select-shapes (d/ordered-set shape-id))
dwz/zoom-to-selected-shape))
redirect-to-page
(fn [page-id shape-id]
(rx/merge
(rx/of (dcm/go-to-workspace :page-id page-id))
(->> stream
(rx/filter (ptk/type? ::initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (select-and-zoom shape-id))))))]
(when-let [component (dm/get-in state [:workspace-data :components id])]
(let [page-id (:main-instance-page component)
shape-id (:main-instance-id component)]
(when (some? page-id)
(if (= page-id current-page-id)
(select-and-zoom shape-id)
(redirect-to-page page-id shape-id)))))))))
(defn library-thumbnails-fetched
[thumbnails]
@ -1117,9 +1147,11 @@
ptk/WatchEvent
(watch [_ state _]
(rp/cmd! :ignore-file-library-sync-status
{:file-id (get-in state [:workspace-file :id])
:date (dt/now)}))))
(let [file-id (:current-file-id state)]
(->> (rp/cmd! :ignore-file-library-sync-status
{:file-id file-id
:date (dt/now)})
(rx/ignore))))))
(defn assets-need-sync
"Get a lazy sequence of all the assets of each type in the library that have
@ -1309,23 +1341,6 @@
(->> (rp/cmd! :set-file-shared params)
(rx/ignore))))))
(defn- shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [state (dissoc state :files)]
(assoc state :workspace-shared-files files)))))
(defn fetch-shared-files
[{:keys [team-id] :as params}]
(dm/assert! (uuid? team-id))
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))
;; --- Link and unlink Files
(defn link-file-to-library

View file

@ -16,6 +16,7 @@
[app.main.data.modal :as modal]
[app.main.data.plugins :as dpl]
[app.main.data.websocket :as dws]
[app.main.data.workspace :as-alias dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.layout :as dwly]
@ -30,9 +31,6 @@
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; From app.main.data.workspace we can use directly because it causes a circular dependency
(def reload-file nil)
;; FIXME: this ns should be renamed to something different
(declare process-message)
@ -292,7 +290,7 @@
curr-vern (dm/get-in state [:workspace-file :vern])
reload? (and (= file-id curr-file-id) (not= vern curr-vern))]
(when reload?
(rx/of (reload-file)))))))
(rx/of (ptk/event ::dw/reload-current-file)))))))
(def ^:private schema:handle-library-change
[:map {:title "handle-library-change"}

View file

@ -18,7 +18,7 @@
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.specialized-panel :as-alias dwsp]

View file

@ -21,7 +21,7 @@
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.colors :as cl]
[app.main.data.workspace.grid-layout.editor :as dwge]
[app.main.data.workspace.modifiers :as dwm]
@ -110,13 +110,15 @@
:undo-group undo-group})))
(rx/empty))))))
(defn initialize
(defn initialize-shape-layout
[]
(ptk/reify ::initialize
(ptk/reify ::initialize-shape-layout
ptk/WatchEvent
(watch [_ _ stream]
(let [stopper (rx/filter (ptk/type? ::finalize) stream)]
(let [stopper (rx/filter (ptk/type? ::finalize-shape-layout) stream)]
(->> stream
;; FIXME: we don't need use types for simple signaling,
;; we can just use a keyword for it
(rx/filter (ptk/type? :layout/update))
(rx/map deref)
;; We buffer the updates to the layout so if there are many changes at the same time
@ -129,9 +131,9 @@
(update-layout-positions {:ids ids}))))
(rx/take-until stopper))))))
(defn finalize
(defn finalize-shape-layout
[]
(ptk/reify ::finalize))
(ptk/data-event ::finalize-shape-layout))
(defn create-layout-from-id
[id type & {:keys [from-frame? calculate-params?] :or {from-frame? false calculate-params? true}}]

View file

@ -18,7 +18,7 @@
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]

View file

@ -7,13 +7,14 @@
(ns app.main.data.workspace.shortcuts
(:require
[app.common.data.macros :as dm]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.exports.assets :as de]
[app.main.data.modal :as modal]
[app.main.data.plugins :as dpl]
[app.main.data.preview :as dp]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
[app.main.data.users :as du]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
[app.main.data.workspace.drawing :as dwd]
@ -440,17 +441,18 @@
:toggle-layers {:tooltip (ds/alt "L")
:command (ds/a-mod "l")
:subsections [:panels]
:fn #(st/emit! (dw/go-to-layout :layers))}
:fn #(st/emit! (dcm/go-to-workspace :layout :layers))}
:toggle-assets {:tooltip (ds/alt "I")
:command (ds/a-mod "i")
:subsections [:panels]
:fn #(st/emit! (dw/go-to-layout :assets))}
:fn #(st/emit! (dcm/go-to-workspace :layout :assets))}
:toggle-history {:tooltip (ds/alt "H")
:command (ds/a-mod "h")
:subsections [:panels]
:fn #(emit-when-no-readonly (dw/go-to-layout :document-history))}
:fn #(emit-when-no-readonly
(dcm/go-to-workspace :layout :document-history))}
:toggle-colorpalette {:tooltip (ds/alt "P")
:command (ds/a-mod "p")
@ -516,22 +518,22 @@
:open-viewer {:tooltip "G V"
:command "g v"
:subsections [:navigation-workspace]
:fn #(st/emit! (dw/go-to-viewer))}
:fn #(st/emit! (dcm/go-to-viewer))}
:open-inspect {:tooltip "G I"
:command "g i"
:subsections [:navigation-workspace]
:fn #(st/emit! (dw/go-to-viewer {:section :inspect}))}
:fn #(st/emit! (dcm/go-to-viewer :section :inspect))}
:open-comments {:tooltip "G C"
:command "g c"
:subsections [:navigation-workspace]
:fn #(st/emit! (dw/go-to-viewer {:section :comments}))}
:fn #(st/emit! (dcm/go-to-viewer :section :comments))}
:open-dashboard {:tooltip "G D"
:command "g d"
:subsections [:navigation-workspace]
:fn #(st/emit! (dw/go-to-dashboard))}
:fn #(st/emit! (dcm/go-to-dashboard-recent))}
:select-prev {:tooltip (ds/shift "tab")
:command "shift+tab"

View file

@ -27,10 +27,6 @@
(-> (lookup-page state page-id)
(get :objects))))
(defn lookup-viewer-objects
([state page-id]
(dm/get-in state [:viewer :pages page-id :objects])))
(defn lookup-library-objects
[state file-id page-id]
(dm/get-in state [:workspace-libraries file-id :data :pages-index page-id :objects]))

View file

@ -17,7 +17,7 @@
[app.common.text :as txt]
[app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.modifiers :as dwm]
@ -27,7 +27,7 @@
[app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.util.router :as rt]
[app.main.router :as rt]
[app.util.text-editor :as ted]
[app.util.text.content.styles :as styles]
[app.util.timers :as ts]

View file

@ -13,8 +13,8 @@
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.changes :as dch]
[app.main.data.common :as dcm]
[app.main.data.workspace.state-helpers :as wsh]
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -290,14 +290,8 @@
(ptk/reify ::assure-valid-current-page
ptk/WatchEvent
(watch [_ state _]
(let [current_page (:current-page-id state)
pages (get-in state [:workspace-data :pages])
exists? (some #(= current_page %) pages)
project-id (:current-project-id state)
file-id (:current-file-id state)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id (first pages)}]
(if exists?
(let [page-id (:current-page-id state)
pages (dm/get-in state [:workspace-data :pages])]
(if (contains? pages page-id)
(rx/empty)
(rx/of (rt/nav :workspace pparams qparams)))))))
(rx/of (dcm/go-to-workspace :page-id (first pages))))))))

View file

@ -8,7 +8,8 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.events :as ev]
[app.common.schema :as sm]
[app.main.data.event :as ev]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
@ -25,7 +26,7 @@
(declare fetch-versions)
(defn init-version-state
[file-id]
[]
(ptk/reify ::init-version-state
ptk/UpdateEvent
(update [_ state]
@ -33,7 +34,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (fetch-versions file-id)))))
(rx/of (fetch-versions)))))
(defn update-version-state
[version-state]
@ -43,123 +44,90 @@
(update state :workspace-versions merge version-state))))
(defn fetch-versions
[file-id]
(dm/assert! (uuid? file-id))
[]
(ptk/reify ::fetch-versions
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/map #(update-version-state {:status :loaded :data %}))))))
(watch [_ state _]
(let [file-id (:current-file-id state)]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/map #(update-version-state {:status :loaded :data %})))))))
(defn create-version
[file-id]
(dm/assert! (uuid? file-id))
[]
(ptk/reify ::create-version
ptk/WatchEvent
(watch [_ _ _]
(let [label (dt/format (dt/now) :date-full)]
(watch [_ state _]
(let [label (dt/format (dt/now) :date-full)
file-id (:current-file-id state)]
;; Force persist before creating snapshot, otherwise we could loss changes
(rx/concat
(rx/of ::dwp/force-persist)
(rx/of ::dwp/force-persist
(ptk/event ::ev/event {::ev/name "create-version"}))
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
(rx/mapcat
(fn [{:keys [id]}]
(rx/of
(update-version-state {:editing id})
(fetch-versions file-id)))))
(rx/of (ptk/event ::ev/event {::ev/name "create-version"})))))))
(defn create-version-from-plugins
[file-id label resolve reject]
(dm/assert! (uuid? file-id))
(ptk/reify ::create-version-plugins
ptk/WatchEvent
(watch [_ _ _]
;; Force persist before creating snapshot, otherwise we could loss changes
(->> (rx/concat
(rx/of ::dwp/force-persist)
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
(rx/mapcat
(fn [{:keys [id]}]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/take 1)
(rx/map (fn [versions] (d/seek #(= id (:id %)) versions))))))
(rx/tap resolve)
(rx/ignore))
(rx/of (ptk/event ::ev/event {::ev/origin "plugins"
::ev/name "create-version"})))
;; On error reject the promise and empty the stream
(rx/catch (fn [error]
(reject error)
(rx/empty)))))))
(rx/of (update-version-state {:editing id})
(fetch-versions))))))))))
(defn rename-version
[file-id id label]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(dm/assert! (and (string? label) (d/not-empty? label)))
[id label]
(assert (uuid? id) "expected valid uuid for `id`")
(assert (sm/valid-text? label) "expected not empty string for `label`")
(ptk/reify ::rename-version
ptk/WatchEvent
(watch [_ _ _]
(rx/merge
(rx/of (update-version-state {:editing false}))
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
(rx/map #(fetch-versions file-id)))
(rx/of (ptk/event ::ev/event {::ev/name "rename-version"}))))))
(watch [_ state _]
(let [file-id (:current-file-id state)]
(rx/merge
(rx/of (update-version-state {:editing false})
(ptk/event ::ev/event {::ev/name "rename-version"
:file-id file-id}))
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
(rx/map fetch-versions)))))))
(defn restore-version
[project-id file-id id origin]
(dm/assert! (uuid? project-id))
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
[id origin]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::restore-version
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-file project-id file-id)))
(case origin
:version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
(watch [_ state _]
(let [file-id (:current-file-id state)]
(rx/concat
(rx/of ::dwp/force-persist)
:snapshot
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace file-id)))
:plugin
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
(rx/empty))))))
(when-let [name (case origin
:version "restore-pin-version"
:snapshot "restore-autosave"
:plugin "restore-version-plugin"
nil)]
(rx/of (ptk/event ::ev/event {::ev/name name}))))))))
(defn delete-version
[file-id id]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
[id]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::delete-version
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :delete-file-snapshot {:id id})
(rx/map #(fetch-versions file-id))))))
(rx/map fetch-versions)))))
(defn pin-version
[file-id id]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
[id]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::pin-version
ptk/WatchEvent
(watch [_ state _]
@ -168,8 +136,82 @@
params {:id id
:label (dt/format (:created-at version) :date-full)}]
(rx/concat
(->> (rp/cmd! :update-file-snapshot params)
(rx/mapcat #(rx/of (update-version-state {:editing id})
(fetch-versions file-id))))
(rx/of (ptk/event ::ev/event {::ev/name "pin-version"})))))))
(->> (rp/cmd! :update-file-snapshot params)
(rx/mapcat (fn [_]
(rx/of (update-version-state {:editing id})
(fetch-versions)
(ptk/event ::ev/event {::ev/name "pin-version"})))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PLUGINS SPECIFIC EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- wait-persisted-status
[]
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)))
(defn create-version-from-plugins
[file-id label resolve reject]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (sm/valid-text? label) "expected not empty string for `label`")
(ptk/reify ::create-version-from-plugins
ptk/WatchEvent
(watch [_ state _]
(let [current-file-id (:current-file-id state)]
;; Force persist before creating snapshot, otherwise we could loss changes
(->> (rx/concat
(rx/of (ptk/event ::ev/event {::ev/origin "plugins"
::ev/name "create-version"}))
(when (= file-id current-file-id)
(rx/of ::dwp/force-persist))
(->> (if (= file-id current-file-id)
(wait-persisted-status)
(rx/of :nothing))
(rx/mapcat
(fn [_]
(rp/cmd! :create-file-snapshot {:file-id file-id :label label})))
(rx/mapcat
(fn [{:keys [id]}]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/take 1)
(rx/map (fn [versions] (d/seek #(= id (:id %)) versions))))))
(rx/tap resolve)
(rx/ignore)))
;; On error reject the promise and empty the stream
(rx/catch (fn [error]
(reject error)
(rx/empty))))))))
(defn restore-version-from-plugin
[file-id id resolve _reject]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::restore-version-from-plugins
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
::dwp/force-persist)
;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace file-id)))
(->> (rx/of 1)
(rx/tap resolve)
(rx/ignore))))))

View file

@ -10,13 +10,14 @@
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.main.data.auth :as da]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.workspace :as-alias dw]
[app.main.router :as rt]
[app.main.store :as st]
[app.util.globals :as glob]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
@ -116,7 +117,7 @@
(if show-oops?
(st/async-emit! (rt/assign-exception e))
(do
(st/emit! (du/logout {:capture-redirect true}))
(st/emit! (da/logout))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
;; Error that happens on an active business model validation does not
@ -141,7 +142,7 @@
:timeout 3000})))
(= code :vern-conflict)
(st/emit! (reload-file))
(st/emit! (ptk/event ::dw/reload-current-file))
:else
(st/async-emit! (rt/assign-exception error))))
@ -212,7 +213,6 @@
(ts/schedule
#(st/emit! (rt/assign-exception error))))
(defn- redirect-to-dashboard
[]
(let [team-id (:current-team-id @st/state)

View file

@ -22,22 +22,33 @@
;; ---- Global refs
(def route
(l/derived :route st/state))
(l/derived (l/key :route) st/state))
(def router
(l/derived :router st/state))
(l/derived (l/key :router) st/state))
(def profile
(l/derived :profile st/state))
(l/derived (l/key :profile) st/state))
(def team
(l/derived :team st/state))
(l/derived (fn [state]
(let [team-id (:current-team-id state)
teams (:teams state)]
(get teams team-id)))
st/state))
(def project
(l/derived (fn [state]
(let [project-id (:current-project-id state)
projects (:projects state)]
(get projects project-id)))
st/state))
(def permissions
(l/derived :permissions st/state))
(l/derived (l/key :permissions) team))
(def teams
(l/derived :teams st/state))
(l/derived (l/key :teams) st/state))
(def exception
(l/derived :exception st/state))
@ -54,68 +65,41 @@
(def persistence
(l/derived :persistence st/state))
;; ---- Dashboard refs
(def projects
(l/derived :projects st/state))
(def dashboard-local
(l/derived :dashboard-local st/state))
(def files
(l/derived :files st/state))
(def dashboard-fonts
(l/derived :dashboard-fonts st/state))
(def shared-files
"A derived state that points to the current list of shared
files (without the content, only summary)"
(l/derived :shared-files st/state))
(def dashboard-projects
(l/derived :dashboard-projects st/state))
(def libraries
(l/derived :libraries st/state))
(def dashboard-files
(l/derived :dashboard-files st/state))
(def dashboard-shared-files
(l/derived :dashboard-shared-files st/state))
(def dashboard-search-result
(l/derived :dashboard-search-result st/state))
(def dashboard-team-stats
(l/derived :dashboard-team-stats st/state))
(def dashboard-team-members
(l/derived :dashboard-team-members st/state))
(def dashboard-team-invitations
(l/derived :dashboard-team-invitations st/state))
(def dashboard-team-webhooks
(l/derived :dashboard-team-webhooks st/state))
(def dashboard-selected-project
(l/derived (fn [state]
(dm/get-in state [:dashboard-local :selected-project]))
st/state))
(defn- dashboard-extract-selected
(defn extract-selected-files
[files selected]
(let [get-file #(get files %)
sim-file #(select-keys % [:id :name :project-id :is-shared])
xform (comp (keep get-file)
(map sim-file))]
(->> (into #{} xform selected)
(->> (sequence xform selected)
(d/index-by :id))))
(def dashboard-selected-search
(def selected-files
(l/derived (fn [state]
;; we need to this because :dashboard-search-result is a list
;; of maps and we need a map of maps (using :id as key).
(let [files (d/index-by :id (:dashboard-search-result state))]
(->> (dm/get-in state [:dashboard-local :selected-files])
(dashboard-extract-selected files))))
(let [selected (get state :selected-files)
files (get state :files)]
(extract-selected-files files selected)))
st/state))
(def dashboard-selected-files
(l/derived (fn [state]
(->> (dm/get-in state [:dashboard-local :selected-files])
(dashboard-extract-selected (:dashboard-files state))))
st/state))
(def selected-project
(l/derived :selected-project st/state))
;; ---- Workspace refs
(def dashboard-local
(l/derived :dashboard-local st/state))
(def render-state
(l/derived :render-state st/state))
@ -270,12 +254,6 @@
(def workspace-file-typography
(l/derived :typographies workspace-data))
(def workspace-project
(l/derived :workspace-project st/state))
(def workspace-shared-files
(l/derived :workspace-shared-files st/state))
(def workspace-local-library
(l/derived (fn [state]
(select-keys (:workspace-data state)
@ -532,12 +510,16 @@
;; ---- Viewer refs
(defn get-viewer-objects
[state page-id]
(dm/get-in state [:viewer :pages page-id :objects]))
(defn lookup-viewer-objects-by-id
[page-id]
(l/derived #(wsh/lookup-viewer-objects % page-id) st/state =))
(l/derived #(get-viewer-objects % page-id) st/state =))
(def viewer-data
(l/derived :viewer st/state))
(l/derived (l/key :viewer) st/state))
(def viewer-file
(l/derived :file viewer-data))
@ -563,14 +545,8 @@
(def comments-local
(l/derived :comments-local st/state))
(def users
(l/derived :users st/state))
(def current-file-comments-users
(l/derived :current-file-comments-users st/state))
(def current-team-comments-users
(l/derived :current-team-comments-users st/state))
(def profiles
(l/derived :profiles st/state))
(def viewer-fullscreen?
(l/derived (fn [state]
@ -582,14 +558,11 @@
(dm/get-in state [:viewer-local :zoom-type]))
st/state))
(def workspace-thumbnails
(l/derived :workspace-thumbnails st/state))
(defn workspace-thumbnail-by-id
[object-id]
(l/derived
(fn [state]
(some-> (dm/get-in state [:workspace-thumbnails object-id])
(some-> (dm/get-in state [:thumbnails object-id])
(cf/resolve-media)))
st/state))
@ -635,35 +608,9 @@
(every? (partial ctl/grid-layout-immediate-child? objects))))
workspace-page-objects =))
;; FIXME: move to viewer.inspect.code
(defn get-flex-child-viewer
[ids page-id]
(l/derived
(fn [state]
(let [objects (wsh/lookup-viewer-objects state page-id)]
(into []
(comp (map (d/getf objects))
(filter (partial ctl/flex-layout-immediate-child? objects)))
ids)))
st/state =))
;; FIXME: move to viewer.inspect.code
(defn get-viewer-objects
([]
(let [route (deref route)
page-id (:page-id (:query-params route))]
(get-viewer-objects page-id)))
([page-id]
(l/derived
(fn [state]
(let [objects (wsh/lookup-viewer-objects state page-id)]
objects))
st/state =)))
(def colorpicker
(l/derived :colorpicker st/state))
(def workspace-grid-edition
(l/derived :workspace-grid-edition st/state))
@ -671,6 +618,7 @@
[id]
(l/derived #(get % id) workspace-grid-edition))
;; FIXME: remove
(def current-file-id
(l/derived :current-file-id st/state))

View file

@ -11,7 +11,7 @@
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.events :as-alias ev]
[app.main.data.event :as-alias ev]
[app.util.http :as http]
[app.util.sse :as sse]
[beicon.v2.core :as rx]

View file

@ -4,13 +4,13 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.router
(ns app.main.router
(:refer-clojure :exclude [resolve])
(:require
[app.common.data.macros :as dm]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.util.browser-history :as bhistory]
[app.util.dom :as dom]
[app.util.globals :as globals]
@ -28,11 +28,10 @@
(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)]
(r/match->path match query-params))))
([router id] (resolve router id {}))
([router id params]
(when-let [match (r/match-by-name router id)]
(r/match->path match params))))
(defn create
[routes]
@ -63,6 +62,9 @@
(defn navigated
[match]
(ptk/reify ::navigated
IDeref
(-deref [_] match)
ev/Event
(-data [_]
(let [route (dm/get-in match [:data :name])
@ -77,25 +79,29 @@
(assoc :route match)
(dissoc :exception)))))
(defn navigate*
[id path-params query-params replace]
(defn navigate
[id params & {:keys [::replace ::new-window] :as options}]
(ptk/reify ::navigate
IDeref
(-deref [_]
{:id id
:path-params path-params
:query-params query-params
:replace replace})
:params params
:options options})
ptk/EffectEvent
(effect [_ state _]
(let [router (:router state)
history (:history state)
path (resolve router id path-params query-params)]
(ts/asap
#(if ^boolean replace
(bhistory/replace-token! history path)
(bhistory/set-token! history path)))))))
path (resolve router id params)]
(if ^boolean new-window
(let [name (or (::window-name options) "_blank")
uri (assoc cf/public-uri :fragment path)]
(dom/open-new-window uri name nil))
(ts/asap
#(if ^boolean replace
(bhistory/replace-token! history path)
(bhistory/set-token! history path))))))))
(defn assign-exception
[error]
@ -107,27 +113,14 @@
(assoc state :exception error)))))
(defn nav
([id] (nav id nil nil))
([id path-params] (nav id path-params nil))
([id path-params query-params] (navigate* id path-params query-params false)))
([id] (navigate id nil))
([id params] (navigate id params))
([id params & {:as options}]
(navigate id params options)))
(defn nav'
([id] (nav id nil nil))
([id path-params] (nav id path-params nil))
([id path-params query-params] (navigate* id path-params query-params true)))
(def navigate nav)
(defn nav-new-window*
[{:keys [rname path-params query-params name]}]
(ptk/reify ::nav-new-window
ptk/EffectEvent
(effect [_ state _]
(let [router (:router state)
path (resolve router rname path-params query-params)
name (or name "_blank")
uri (assoc cf/public-uri :fragment path)]
(dom/open-new-window uri name nil)))))
(defn get-params
[state]
(dm/get-in state [:route :params :query]))
(defn nav-back
[]

View file

@ -60,7 +60,7 @@
:app.main.data.workspace.persistence/update-persistence-status
:app.main.data.websocket/send-message
:app.main.data.workspace.notifications/handle-pointer-send
:app.util.router/assign-exception}]
:app.main.router/assign-exception}]
(->> (rx/merge
(->> stream
(rx/filter (ptk/type? :app.main.data.changes/commit))

View file

@ -6,14 +6,20 @@
(ns app.main.ui
(:require
[app.common.data :as d]
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.debug.icons-preview :refer [icons-preview]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.error-boundary :refer [error-boundary*]]
[app.main.ui.exports.files]
[app.main.ui.frame-preview :as frame-preview]
[app.main.ui.icons :as i]
[app.main.ui.notifications :as notifications]
[app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]]
[app.main.ui.onboarding.questions :refer [questions-modal]]
@ -22,6 +28,8 @@
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(def auth-page
@ -31,23 +39,117 @@
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def viewer-page
(mf/lazy-component app.main.ui.viewer/viewer))
(mf/lazy-component app.main.ui.viewer/viewer*))
(def dashboard-page
(mf/lazy-component app.main.ui.dashboard/dashboard))
(mf/lazy-component app.main.ui.dashboard/dashboard*))
(def settings-page
(mf/lazy-component app.main.ui.settings/settings))
(def workspace-page
(mf/lazy-component app.main.ui.workspace/workspace))
(mf/lazy-component app.main.ui.workspace/workspace*))
(mf/defc main-page
(mf/defc workspace-legacy-redirect*
{::mf/props :obj
::mf/private true}
[{:keys [project-id file-id page-id layout]}]
(mf/with-effect []
(->> (rp/cmd! :get-project {:id project-id})
(rx/subs! (fn [{:keys [team-id]}]
(st/emit! (dcm/go-to-workspace :team-id team-id
:file-id file-id
:page-id page-id
:layout layout)))
ptk/handle-error)))
[:> loader*
{:title (tr "labels.loading")
:overlay true}])
(mf/defc dashboard-legacy-redirect*
{::mf/props :obj
::mf/private true}
[{:keys [section team-id project-id search-term plugin-url]}]
(let [section (case section
:dashboard-legacy-search
:dashboard-search
:dashboard-legacy-projects
:dashboard-recent
:dashboard-legacy-files
:dashboard-files
:dashboard-legacy-libraries
:dashboard-libraries
:dashboard-legacy-fonts
:dashboard-fonts
:dashboard-legacy-font-providers
:dashboard-font-providers
:dashboard-legacy-team-members
:dashboard-members
:dashboard-legacy-team-invitations
:dashboard-invitations
:dashboard-legacy-team-webhooks
:dashboard-webhooks
:dashboard-legacy-team-settings
:dashboard-settings)]
(mf/with-effect []
(let [params {:team-id team-id
:project-id project-id
:search-term search-term
:plugin plugin-url}]
(st/emit! (rt/nav section (d/without-nils params)))))
[:> loader*
{:title (tr "labels.loading")
:overlay true}]))
(mf/defc viewer-legacy-redirect*
{::mf/props :obj
::mf/private true}
[{:keys [page-id file-id section index share-id interactions-mode frame-id share]}]
(mf/with-effect []
(let [params {:page-id page-id
:file-id file-id
:section section
:index index
:share-id share-id
:interactions-mode interactions-mode
:frame-id frame-id
:share share}]
(st/emit! (rt/nav :viewer (d/without-nils params)))))
[:> loader*
{:title (tr "labels.loading")
:overlay true}])
(mf/defc team-container*
{::mf/props :obj
::mf/private true}
[{:keys [team-id children]}]
(mf/with-effect [team-id]
(st/emit! (dtm/initialize-team team-id))
(fn []
(st/emit! (dtm/finalize-team team-id))))
(let [team (mf/deref refs/team)]
(when (= team-id (:id team))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/permissions) {:value (:permissions team)}
;; The `:key` is mandatory here because we want to reinitialize
;; all dom tree instead of simple rerender.
[:* {:key (str team-id)} children]]])))
(mf/defc page*
{::mf/props :obj
::mf/private true}
[{:keys [route profile]}]
(let [{:keys [data params]} route
props (get profile :props)
props (get profile :props)
section (get data :name)
show-question-modal?
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
@ -72,7 +174,7 @@
(not= "0.0" (:main cf/version)))]
[:& (mf/provider ctx/current-route) {:value route}
(case (:name data)
(case section
(:auth-login
:auth-register
:auth-register-validate
@ -96,66 +198,53 @@
[:& icons-preview])
(:dashboard-search
:dashboard-projects
:dashboard-recent
:dashboard-files
:dashboard-libraries
:dashboard-fonts
:dashboard-font-providers
:dashboard-team-members
:dashboard-team-invitations
:dashboard-team-webhooks
:dashboard-team-settings)
[:?
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
#_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
:dashboard-members
:dashboard-invitations
:dashboard-webhooks
:dashboard-settings)
(let [params (get params :query)
team-id (some-> params :team-id uuid)
project-id (some-> params :project-id uuid)
search-term (some-> params :search-term)
plugin-url (some-> params :plugin)]
[:?
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
#_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
(cond
show-question-modal?
[:& questions-modal]
(cond
show-question-modal?
[:& questions-modal]
show-newsletter-modal?
[:& onboarding-newsletter]
show-newsletter-modal?
[:& onboarding-newsletter]
show-team-modal?
[:& onboarding-team-modal {:go-to-team? true}]
show-team-modal?
[:& onboarding-team-modal {:go-to-team? true}]
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])
[:& 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 share]
:or {section :interactions interactions-mode :show-on-click}} query-params
{:keys [file-id]} path-params]
[:? {}
(if (:token query-params)
[:> static/error-container* {}
[:div.image i/detach]
[:div.main-message (tr "viewer.breaking-change.message")]
[:div.desc-message (tr "viewer.breaking-change.description")]]
[:& viewer-page
{:page-id page-id
:file-id file-id
:section section
:index index
:share-id share-id
:interactions-mode (keyword interactions-mode)
:interactions-show? (case (keyword interactions-mode)
:hide false
:show true
:show-on-click false)
:frame-id frame-id
:share share}])])
[:> team-container* {:team-id team-id}
[:> dashboard-page {:profile profile
:section section
:team-id team-id
:search-term search-term
:plugin-url plugin-url
:project-id project-id}]]])
:workspace
(let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid)
page-id (some-> params :query :page-id uuid)
layout (some-> params :query :layout keyword)]
(let [params (get params :query)
team-id (some-> params :team-id uuid)
file-id (some-> params :file-id uuid)
page-id (some-> params :page-id uuid)
layout (some-> params :layout keyword)]
[:? {}
(when (cf/external-feature-flag "onboarding-03" "test")
(cond
@ -171,11 +260,87 @@
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}]))
[:& workspace-page {:project-id project-id
:file-id file-id
:page-id page-id
:layout-name layout
:key file-id}]])
[:> team-container* {:team-id team-id}
[:> workspace-page {:team-id team-id
:file-id file-id
:page-id page-id
:layout-name layout
:key file-id}]]])
:viewer
(let [params (get params :query)
index (some-> (:index params) parse-long)
share-id (some-> (:share-id params) parse-uuid)
section (or (some-> (:section params) keyword)
:interactions)
file-id (some-> (:file-id params) parse-uuid)
page-id (some-> (:page-id params) parse-uuid)
imode (or (some-> (:interactions-mode params) keyword)
:show-on-click)
frame-id (some-> (:frame-id params) parse-uuid)
share (:share params)]
[:? {}
[:> viewer-page
{:page-id page-id
:file-id file-id
:frame-id frame-id
:section section
:index index
:share-id share-id
:interactions-mode imode
:share share}]])
:workspace-legacy
(let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid)
page-id (some-> params :query :page-id uuid)
layout (some-> params :query :layout keyword)]
[:> workspace-legacy-redirect*
{:project-id project-id
:file-id file-id
:page-id page-id
:layout layout}])
(:dashboard-legacy-search
:dashboard-legacy-projects
:dashboard-legacy-files
:dashboard-legacy-libraries
:dashboard-legacy-fonts
:dashboard-legacy-font-providers
:dashboard-legacy-team-members
:dashboard-legacy-team-invitations
:dashboard-legacy-team-webhooks
:dashboard-legacy-team-settings)
(let [team-id (some-> params :path :team-id uuid)
project-id (some-> params :path :project-id uuid)
search-term (some-> params :query :search-term)
plugin-url (some-> params :query :plugin)]
[:> dashboard-legacy-redirect*
{:team-id team-id
:section section
:project-id project-id
:search-term search-term
:plugin-url plugin-url}])
:viewer-legacy
(let [{:keys [query-params path-params]} route
{: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]
[:> viewer-legacy-redirect*
{:page-id page-id
:file-id file-id
:section section
:index index
:share-id share-id
:interactions-mode (keyword interactions-mode)
:frame-id frame-id
:share share}])
:frame-preview
[:& frame-preview/frame-preview]
@ -199,4 +364,4 @@
[:> error-boundary* {:fallback static/internal-error*}
[:& notifications/current-notification]
(when route
[:& main-page {:route route :profile profile}])])]]))
[:> page* {:route route :profile profile}])])]]))

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.users :as du]
[app.main.data.auth :as da]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]]
@ -19,7 +19,6 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc auth
{::mf/props :obj}
[{:keys [route]}]
@ -35,7 +34,7 @@
(mf/with-effect [error]
(when error
(st/emit! (du/show-redirect-error error))))
(st/emit! (da/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
(when show-login-icon

View file

@ -10,9 +10,10 @@
[app.common.logging :as log]
[app.common.schema :as sm]
[app.config :as cf]
[app.main.data.auth :as da]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.button-link :as bl]
[app.main.ui.components.forms :as fm]
@ -22,7 +23,6 @@
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[app.util.router :as rt]
[app.util.storage :as s]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@ -43,7 +43,7 @@
(defn create-demo-profile
[]
(st/emit! (du/create-demo-profile)))
(st/emit! (da/create-demo-profile)))
(defn- store-login-redirect
[save-login-redirect]
@ -124,7 +124,7 @@
(mf/use-fn
(fn [data]
(when-let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))))
(st/emit! (rt/nav :auth-verify-token {:token token})))))
on-success
(fn [data]
@ -140,7 +140,7 @@
(let [params (with-meta (:clean-data @form)
{:on-error on-error
:on-success on-success})]
(st/emit! (du/login params)))))
(st/emit! (da/login params)))))
on-submit-ldap
(mf/use-callback
@ -154,7 +154,7 @@
params (with-meta params
{:on-error on-error
:on-success on-success})]
(st/emit! (du/login-with-ldap params)))))
(st/emit! (da/login-with-ldap params)))))
default-recovery-req
(mf/use-fn
@ -283,7 +283,7 @@
[{:keys [params] :as props}]
(let [go-register
(mf/use-fn
#(st/emit! (rt/nav :auth-register {} params)))]
#(st/emit! (rt/nav :auth-register params)))]
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :auth-title)

View file

@ -9,11 +9,11 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[rumext.v2 :as mf]))
(def ^:private schema:recovery-form

View file

@ -9,12 +9,12 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))

View file

@ -10,16 +10,16 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.config :as cf]
[app.main.data.auth :as da]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.auth.login :as login]
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@ -74,7 +74,7 @@
on-success (fn [data]
(if (fn? on-success-callback)
(on-success-callback data)
(st/emit! (rt/nav :auth-register-validate {} data))))]
(st/emit! (rt/nav :auth-register-validate data))))]
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
@ -131,7 +131,7 @@
[:div {:class (stl/css :links)}
[:div {:class (stl/css :account)}
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
[:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params))
[:& lk/link {:action #(st/emit! (rt/nav :auth-login params))
:class (stl/css :account-link)
:data-testid "login-here-link"}
(tr "auth.login-here")]]
@ -191,10 +191,10 @@
(cond
(some? (:invitation-token params))
(let [token (:invitation-token params)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
(st/emit! (rt/nav :auth-verify-token {:token token})))
(:is-active params)
(st/emit! (du/login-from-register))
(st/emit! (da/login-from-register))
:else
(do
@ -257,7 +257,7 @@
[:div {:class (stl/css :links)}
[:div {:class (stl/css :go-back)}
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {}))
:class (stl/css :go-back-link)}
(tr "labels.go-back")]]]])

View file

@ -6,15 +6,17 @@
(ns app.main.ui.auth.verify-token
(:require
[app.main.data.auth :as da]
[app.main.data.common :as dcm]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@ -25,7 +27,7 @@
[data]
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
(ts/schedule 1000 #(st/emit! (ntf/success msg)))
(st/emit! (du/login-from-token data))))
(st/emit! (da/login-from-token data))))
(defmethod handle-token :change-email
[_data]
@ -36,21 +38,22 @@
(defmethod handle-token :auth
[tdata]
(st/emit! (du/login-from-token tdata)))
(st/emit! (da/login-from-token tdata)))
(defmethod handle-token :team-invitation
[tdata]
(case (:state tdata)
:created
(st/emit!
(ntf/success (tr "auth.notifications.team-invitation-accepted"))
(du/fetch-profile)
(rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
(let [team-id (:team-id tdata)]
(st/emit!
(ntf/success (tr "auth.notifications.team-invitation-accepted"))
(du/fetch-profile)
(dcm/go-to-dashboard-recent :team-id team-id)))
:pending
(let [token (:invitation-token tdata)
route-id (:redirect-to tdata :auth-register)]
(st/emit! (rt/nav route-id {} {:invitation-token token})))))
(st/emit! (rt/nav route-id {:invitation-token token})))))
(defmethod handle-token :default
[_tdata]

View file

@ -253,8 +253,8 @@
:disabled disabled?}]]]))
(mf/defc comment-item
[{:keys [comment thread users origin] :as props}]
(let [owner (get users (:owner-id comment))
[{:keys [comment thread profiles origin] :as props}]
(let [owner (get profiles (:owner-id comment))
profile (mf/deref refs/profile)
options (mf/deref comments-local-options)
edition? (mf/use-state false)
@ -384,7 +384,7 @@
(mf/defc thread-comments
{::mf/wrap [mf/memo]}
[{:keys [thread zoom users origin position-modifier viewport]}]
[{:keys [thread zoom profiles origin position-modifier viewport]}]
(let [ref (mf/use-ref)
thread-id (:id thread)
thread-pos (:position thread)
@ -435,13 +435,13 @@
[:div {:class (stl/css :comments)}
[:& comment-item {:comment comment
:users users
:profiles profiles
:thread thread
:origin origin}]
(for [item (rest comments)]
[:* {:key (dm/str (:id item))}
[:& comment-item {:comment item
:users users
:profiles profiles
:origin origin}]])]
[:& reply-form {:thread thread}]
[:div {:ref ref}]])))
@ -573,8 +573,8 @@
[:span (:seqn thread)]]))
(mf/defc comment-thread
[{:keys [item users on-click]}]
(let [owner (get users (:owner-id item))
[{:keys [item profiles on-click]}]
(let [owner (get profiles (:owner-id item))
on-click*
(mf/use-fn
(mf/deps item)
@ -613,7 +613,7 @@
[:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]))
(mf/defc comment-thread-group
[{:keys [group users on-thread-click]}]
[{:keys [group profiles on-thread-click]}]
[:div {:class (stl/css :thread-group)}
(if (:file-name group)
[:div {:class (stl/css :section-title)
@ -631,5 +631,5 @@
[:& comment-thread
{:item item
:on-click on-thread-click
:users users
:profiles profiles
:key (:id item)}])]])

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.events :as-alias ev]
[app.main.data.event :as-alias ev]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.timers :as tm]

View file

@ -33,4 +33,4 @@
(def is-component? (mf/create-context false))
(def sidebar (mf/create-context nil))
(def team-permissions (mf/create-context nil))
(def permissions (mf/create-context nil))

View file

@ -7,62 +7,46 @@
(ns app.main.ui.dashboard
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.dashboard.shortcuts :as sc]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as notif]
[app.main.data.plugins :as dp]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard.files :refer [files-section]]
[app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
[app.main.ui.dashboard.files :refer [files-section*]]
[app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
[app.main.ui.dashboard.import]
[app.main.ui.dashboard.libraries :refer [libraries-page]]
[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 team-invitations-page team-webhooks-page]]
[app.main.ui.dashboard.templates :refer [templates-section]]
[app.main.ui.dashboard.libraries :refer [libraries-page*]]
[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* team-invitations-page* webhooks-page*]]
[app.main.ui.dashboard.templates :refer [templates-section*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.workspace.plugins]
[app.plugins.register :as preg]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[goog.events :as events]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(defn ^boolean uuid-str?
[s]
(and (string? s)
(boolean (re-seq us/uuid-rx s))))
(defn- parse-params
[route]
(let [search-term (get-in route [:params :query :search-term])
team-id (get-in route [:params :path :team-id])
project-id (get-in route [:params :path :project-id])]
(cond-> {:search-term search-term}
(uuid-str? team-id)
(assoc :team-id (uuid team-id))
(uuid-str? project-id)
(assoc :project-id (uuid project-id)))))
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
(mf/defc dashboard-content*
{::mf/props :obj
::mf/private true}
[{:keys [team projects project section search-term profile default-project]}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)
project-id (:id project)
team-id (:id team)
@ -72,10 +56,7 @@
file-menu-open? (:menu-open dashboard-local)
default-project-id
(mf/with-memo [projects]
(->> (vals projects)
(d/seek :is-default)
(:id)))
(get default-project :id)
on-resize
(mf/use-fn
@ -88,7 +69,7 @@
(mf/use-fn
#(st/emit! (dd/clear-selected-files)))
show-templates
show-templates?
(and (contains? cf/flags :dashboard-templates-section)
(:can-edit permissions))]
@ -103,61 +84,64 @@
:on-click clear-selected-fn
:ref container}
(case section
:dashboard-projects
:dashboard-recent
[:*
[:& projects-section
[:> projects-section*
{:team team
:projects projects
:profile profile
:default-project-id default-project-id}]
:profile profile}]
(when show-templates
[:& templates-section {:profile profile
:project-id project-id
:team-id team-id
:default-project-id default-project-id
:content-width @content-width}])]
(when ^boolean show-templates?
[:> templates-section*
{:profile profile
:project-id project-id
:team-id team-id
:default-project-id default-project-id
:content-width @content-width}])]
:dashboard-fonts
[:& fonts-page {:team team}]
[:> fonts-page* {:team team}]
:dashboard-font-providers
[:& font-providers-page {:team team}]
[:> font-providers-page* {:team team}]
:dashboard-files
(when project
[:*
[:& files-section {:team team :project project}]
(when show-templates
[:& templates-section {:profile profile
:team-id team-id
:project-id project-id
:default-project-id default-project-id
:content-width @content-width}])])
[:> files-section* {:team team
:project project}]
(when ^boolean show-templates?
[:> templates-section*
{:profile profile
:team-id team-id
:project-id project-id
:default-project-id default-project-id
:content-width @content-width}])])
:dashboard-search
[:& search-page {:team team
:search-term search-term}]
[:> search-page* {:team team
:search-term search-term}]
:dashboard-libraries
[:& libraries-page {:team team}]
[:> libraries-page* {:team team
:default-project default-project}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile}]
:dashboard-members
[:> team-members-page* {:team team :profile profile}]
:dashboard-team-invitations
[:& team-invitations-page {:team team}]
:dashboard-invitations
[:> team-invitations-page* {:team team}]
:dashboard-team-webhooks
[:& team-webhooks-page {:team team}]
:dashboard-webhooks
[:> webhooks-page* {:team team}]
:dashboard-team-settings
[:& team-settings-page {:team team :profile profile}]
:dashboard-settings
[:> team-settings-page* {:team team :profile profile}]
nil)]))
(def ref:dashboard-initialized
(l/derived :current-team-initialized st/state))
(l/derived :team-initialized st/state))
(defn use-plugin-register
[plugin-url team-id project-id]
@ -167,8 +151,9 @@
(st/emit!
(dp/delay-open-plugin plugin)
(rt/nav :workspace
{:project-id project-id :file-id id}
{:page-id (dm/get-in data [:pages 0])})))
{:page-id (dm/get-in data [:pages 0])
:project-id project-id
:file-id id})))
create-file!
(fn [plugin]
@ -198,11 +183,11 @@
:on-accept
#(do (preg/install-plugin! plugin)
(st/emit! (modal/hide)
(rt/nav :dashboard-projects {:team-id team-id})
(rt/nav :dashboard-recent {:team-id team-id})
(open-try-out-dialog plugin)))
:on-close
#(st/emit! (modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))}))]
(rt/nav :dashboard-recent {:team-id team-id}))}))]
(mf/with-layout-effect
[plugin-url team-id project-id]
@ -218,33 +203,29 @@
(fn [_]
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
(mf/defc dashboard
(mf/defc dashboard*
{::mf/props :obj}
[{:keys [route profile]}]
(let [section (get-in route [:data :name])
params (parse-params route)
[{:keys [profile project-id team-id search-term plugin-url section]}]
(let [team (mf/deref refs/team)
projects (mf/deref refs/projects)
project-id (:project-id params)
project (get projects project-id)
projects (mf/with-memo [projects team-id]
(->> (vals projects)
(filterv #(= team-id (:team-id %)))))
team-id (:team-id params)
search-term (:search-term params)
plugin-url (-> route :query-params :plugin)
team (mf/deref refs/team)
projects (mf/deref refs/dashboard-projects)
project (get projects project-id)
default-project (->> projects vals (d/seek :is-default))
initialized? (mf/deref ref:dashboard-initialized)]
default-project
(mf/with-memo [projects]
(->> projects
(filter :is-default)
(first)))]
(hooks/use-shortcuts ::dashboard sc/shortcuts)
(mf/with-effect [team-id]
(st/emit! (dd/initialize {:id team-id}))
(mf/with-effect []
(st/emit! (dd/initialize))
(fn []
(st/emit! (dd/finalize {:id team-id}))))
(st/emit! (dd/finalize))))
(mf/with-effect []
(let [key (events/listen goog/global "keydown"
@ -257,31 +238,30 @@
(use-plugin-register plugin-url team-id (:id default-project))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:& (mf/provider ctx/team-permissions) {:value (:permissions team)}
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
(when (and team initialized?)
[:main {:class (stl/css :dashboard)
:key (:id team)}
[:& sidebar
{:team team
:projects projects
:project project
:profile profile
:section section
:search-term search-term}]
(when (and team profile (seq projects))
[:& dashboard-content
{:projects projects
:profile profile
:project project
:section section
:search-term search-term
:team team}])])]]]))
[:& (mf/provider ctx/current-project-id) {:value project-id}
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
[:main {:class (stl/css :dashboard)
:key (dm/str (:id team))}
[:> sidebar*
{:team team
:projects projects
:project project
:default-project default-project
:profile profile
:section section
:search-term search-term}]
(when (seq projects)
[:> dashboard-content*
{:projects projects
:profile profile
:project project
:default-project default-project
:section section
:search-term search-term
:team team}])]]))

View file

@ -9,7 +9,6 @@
(:require
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
@ -25,8 +24,7 @@
::mf/register-as :leave-and-reassign}
[{:keys [profile team accept]}]
(let [form (fm/use-form :schema schema:leave-modal-form :initial {})
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
members (get team :members)
options
(into [{:value ""

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.workspace.comments :as dwcm]
[app.main.refs :as refs]
[app.main.store :as st]
@ -63,7 +63,7 @@
(mf/defc comments-section
[{:keys [profile team show? on-hide-comments]}]
(let [threads-map (mf/deref refs/comment-threads)
users (mf/deref refs/current-team-comments-users)
profiles (mf/deref refs/profiles)
team-id (:id team)
tgroups (->> (vals threads-map)
@ -114,13 +114,13 @@
{:group (first tgroups)
:on-thread-click on-navigate
:show-file-name true
:users users}]
:profiles profiles}]
(for [tgroup (rest tgroups)]
[:& cmt/comment-thread-group
{:group tgroup
:on-thread-click on-navigate
:show-file-name true
:users users
:profiles profiles
:key (:page-id tgroup)}])]
[:div {:class (stl/css :thread-groups-placeholder)}

View file

@ -9,18 +9,18 @@
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as-alias ev]
[app.main.data.event :as-alias ev]
[app.main.data.exports.files :as fexp]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.context :as ctx]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@ -85,10 +85,9 @@
on-new-tab
(fn [_]
(let [path-params {:project-id (:project-id file)
:file-id (:id file)}]
(st/emit! (rt/nav-new-window* {:rname :workspace
:path-params path-params}))))
(st/emit! (dcm/go-to-workspace
{:file-id (:id file)
::rt/new-window true})))
on-duplicate
(fn [_]
@ -134,7 +133,9 @@
(st/emit! (ntf/success (tr "dashboard.success-move-files")))
(st/emit! (ntf/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! (dcm/go-to-dashboard-files
{:project-id project-id
:team-id team-id}))
(st/emit! (dd/fetch-recent-files)
(dd/clear-selected-files))))

View file

@ -7,8 +7,10 @@
(ns app.main.ui.dashboard.files
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
@ -21,7 +23,6 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -32,9 +33,12 @@
{::mf/props :obj
::mf/private true}
[{:keys [project create-fn can-edit]}]
(let [local (mf/use-state
{:menu-open false
:edition false})
(let [project-id (:id project)
local
(mf/use-state
{:menu-open false
:edition false})
on-create-click
(mf/use-fn
@ -63,9 +67,9 @@
on-import
(mf/use-fn
(mf/deps (:id project))
(mf/deps project-id)
(fn []
(st/emit! (dd/fetch-files {:project-id (:id project)})
(st/emit! (dpj/fetch-files project-id)
(dd/clear-selected-files))))]
@ -126,32 +130,37 @@
:on-menu-close on-menu-close
:on-import on-import}])]]))
(mf/defc files-section
(mf/defc files-section*
{::mf/props :obj}
[{:keys [project team]}]
(let [files-map (mf/deref refs/dashboard-files)
can-edit? (-> team :permissions :can-edit)
project-id (:id project)
is-draft-proyect (:is-default project)
(let [files (mf/deref refs/files)
project-id (get project :id)
[rowref limit] (hooks/use-dynamic-grid-item-width)
files (mf/with-memo [project-id files]
(->> (vals files)
(filter #(= project-id (:project-id %)))
(sort-by :modified-at)
(reverse)))
files (mf/with-memo [project-id files-map]
(->> (vals files-map)
(filter #(= project-id (:project-id %)))
(sort-by :modified-at)
(reverse)))
file-count (or (count files) 0)
can-edit? (-> team :permissions :can-edit)
project-id (:id project)
is-draft-proyect (:is-default project)
[rowref limit] (hooks/use-dynamic-grid-item-width)
file-count (or (count files) 0)
empty-state-viewer (and (not can-edit?)
(= 0 file-count))
selected-files (mf/deref refs/selected-files)
on-file-created
(mf/use-fn
(fn [data]
(let [pparams {:project-id (:project-id data)
:file-id (:id data)}
qparams {:page-id (get-in data [:data :pages 0])}]
(st/emit! (rt/nav :workspace pparams qparams)))))
(fn [file-data]
(let [file-id (:id file-data)
page-id (get-in file-data [:pages 0])]
(st/emit! (dcm/go-to-workspace :file-id file-id :page-id page-id)))))
create-file
(mf/use-fn
@ -170,7 +179,7 @@
(dom/set-html-title (tr "title.dashboard.files" pname)))))
(mf/with-effect [project-id]
(st/emit! (dd/fetch-files {:project-id project-id})
(st/emit! (dpj/fetch-files project-id)
(dd/clear-selected-files)))
[:*
@ -191,6 +200,7 @@
(tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& grid {:project project
:files files
:selected-files selected-files
:can-edit can-edit?
:origin :files
:create-fn create-file

View file

@ -11,7 +11,6 @@
[app.common.media :as cm]
[app.main.data.fonts :as df]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
@ -24,6 +23,7 @@
[app.util.keyboard :as kbd]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(defn- use-page-title
@ -42,7 +42,7 @@
(and (contains? font :font-family-tmp)
(str/blank? (:font-family-tmp font))))
(mf/defc header
(mf/defc header*
{::mf/props :obj
::mf/memo true
::mf/private true}
@ -52,7 +52,7 @@
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.fonts")]]])
(mf/defc font-variant-display-name
(mf/defc font-variant-display-name*
{::mf/props :obj
::mf/private true}
[{:keys [variant]}]
@ -61,10 +61,10 @@
(when (not= "normal" (:font-style variant))
[:span " " (str/capital (:font-style variant))])])
(mf/defc uploaded-fonts
(mf/defc uploaded-fonts*
{::mf/props :obj
::mf/private true}
[{:keys [team installed-fonts] :as props}]
[{:keys [team installed-fonts]}]
(let [fonts* (mf/use-state {})
fonts (deref fonts*)
font-vals (mf/with-memo [fonts]
@ -219,7 +219,7 @@
:default-value (:font-family item)}]]
[:div {:class (stl/css :table-field :variants)}
[:span {:class (stl/css :label)}
[:& font-variant-display-name {:variant item}]]]
[:> font-variant-display-name* {:variant item}]]]
[:div {:class (stl/css :table-field :filenames)}
(for [item (:names item)]
@ -364,7 +364,7 @@
:inhert-variant (not can-edit))
:key (dm/str id)}
[:span {:class (stl/css :label)}
[:& font-variant-display-name {:variant item}]]
[:> font-variant-display-name* {:variant item}]]
(when can-edit
[:span
{:class (stl/css :icon :close)
@ -396,8 +396,9 @@
:on-delete on-delete-font
:on-edit on-edit}]]))]))
(mf/defc installed-fonts
[{:keys [fonts can-edit] :as props}]
(mf/defc installed-fonts*
{::mf/props :obj}
[{:keys [fonts can-edit]}]
(let [sterm (mf/use-state "")
matches?
@ -445,26 +446,27 @@
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
:type 2}]))]))
(def ^:private ref:fonts
(l/derived :fonts st/state))
(mf/defc fonts-page
(mf/defc fonts-page*
{::mf/props :obj}
[{:keys [team]}]
(let [fonts (mf/deref refs/dashboard-fonts)
(let [fonts (mf/deref ref:fonts)
permissions (:permissions team)
can-edit (:can-edit permissions)]
[:*
[:& header {:team team :section :fonts}]
[:> header* {:team team :section :fonts}]
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
(when ^boolean can-edit
[:& uploaded-fonts {:team team :installed-fonts fonts}])
[:& installed-fonts {:team team :fonts fonts :can-edit can-edit}]]]))
[:> uploaded-fonts* {:team team :installed-fonts fonts}])
[:> installed-fonts*
{:team team :fonts fonts :can-edit can-edit}]]]))
(mf/defc font-providers-page
(mf/defc font-providers-page*
{::mf/props :obj}
[{:keys [team]}]
[:*
[:& header {:team team :section :providers}]
[:> header* {:team team :section :providers}]
[:section {:class (stl/css :dashboard-container)}
[:span "font providers"]]])

View file

@ -12,8 +12,11 @@
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.notifications :as ntf]
[app.main.data.project :as dpj]
[app.main.data.team :as dtm]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.rasterizer :as thr]
@ -71,11 +74,18 @@
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn))))
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}
[{:keys [file-id revn thumbnail-id background-color can-edit]}]
(let [container (mf/use-ref)
visible? (h/use-visible container :once? true)]
(mf/defc grid-item-thumbnail*
{::mf/props :obj
::mf/private true}
[{:keys [can-edit file]}]
(let [file-id (get file :id)
revn (get file :revn)
thumbnail-id (get file :thumbnail-id)
bg-color (dm/get-in file [:data :background])
container (mf/use-ref)
visible? (h/use-visible container :once? true)]
(mf/with-effect [file-id revn visible? thumbnail-id]
(when (and visible? (not thumbnail-id))
@ -89,7 +99,7 @@
:message (ex-message cause)))))))
[:div {:class (stl/css :grid-item-th)
:style {:background-color background-color}
:style {:background-color bg-color}
:ref container}
(when visible?
(if thumbnail-id
@ -108,10 +118,9 @@
(def ^:private menu-icon
(i/icon-xref :menu (stl/css :menu-icon)))
(mf/defc grid-item-library
{::mf/wrap [mf/memo]}
[{:keys [file] :as props}]
(mf/defc grid-item-library*
{::mf/props :obj}
[{:keys [file]}]
(mf/with-effect [file]
(when file
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
@ -231,16 +240,12 @@
(dom/set-text! counter-el (str file-count))
counter-el))
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [file origin library-view? can-edit] :as props}]
(mf/defc grid-item*
{::mf/props :obj}
[{:keys [file origin can-edit selected-files]}]
(let [file-id (:id file)
;; FIXME: this breaks react hooks rule, hooks should never to
;; be in a conditional code
selected-files (if (= origin :search)
(mf/deref refs/dashboard-selected-search)
(mf/deref refs/dashboard-selected-files))
is-library-view (= origin :libraries)
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
@ -267,12 +272,12 @@
on-navigate
(mf/use-fn
(mf/deps file)
(mf/deps file-id)
(fn [event]
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
(st/emit! (dd/go-to-workspace file))))))
(st/emit! (dcm/go-to-workspace :file-id file-id))))))
on-drag-start
(mf/use-fn
@ -354,9 +359,12 @@
(on-select event)) ;; TODO Fix this
)))]
[:li {:class (stl/css-case :grid-item true :project-th true :library library-view?)}
[:li {:class (stl/css-case :grid-item true
:project-th true
:library is-library-view)}
[:div
{:class (stl/css-case :selected selected? :library library-view?)
{:class (stl/css-case :selected selected?
:library is-library-view)
:ref node-ref
:role "button"
:title (:name file)
@ -369,16 +377,11 @@
[:div {:class (stl/css :overlay)}]
(if library-view?
[:& grid-item-library {:file file}]
[:& grid-item-thumbnail
{:file-id (:id file)
:can-edit can-edit
:revn (:revn file)
:thumbnail-id (:thumbnail-id file)
:background-color (dm/get-in file [:data :options :background])}])
(if ^boolean is-library-view
[:> grid-item-library* {:file file}]
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
(when (and (:is-shared file) (not library-view?))
(when (and (:is-shared file) (not is-library-view))
[:div {:class (stl/css :item-badge)} i/library])
[:div {:class (stl/css :info-wrapper)}
@ -417,7 +420,8 @@
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
(mf/defc grid
[{:keys [files project origin limit library-view? create-fn can-edit] :as props}]
{::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files]}]
(let [dragging? (mf/use-state false)
project-id (:id project)
node-ref (mf/use-var nil)
@ -425,10 +429,12 @@
on-finish-import
(mf/use-fn
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-shared-files)
(st/emit! (dpj/fetch-files project-id)
(dtm/fetch-shared-files)
(dd/clear-selected-files))))
import-files (use-import-file project-id on-finish-import)
on-drag-enter
@ -484,13 +490,12 @@
(when @dragging?
[:li {:class (stl/css :grid-item)}])
(for [item slice]
[:& grid-item
[:> grid-item*
{:file item
:key (:id item)
:navigate? true
:key (dm/str (:id item))
:origin origin
:can-edit can-edit
:library-view? library-view?}])])
:selected-files selected-files
:can-edit can-edit}])])
:else
[:& empty-placeholder
@ -510,13 +515,12 @@
[:li {:class (stl/css :grid-item :dragged)}])
(for [item (take limit files)]
[:& grid-item
[:> grid-item*
{:id (:id item)
:file item
:selected-files selected-files
:can-edit can-edit
:key (:id item)
:navigate? false}])]))
:key (dm/str (:id item))}])]))
(mf/defc line-grid
[{:keys [project team files limit create-fn can-edit] :as props}]
@ -524,8 +528,8 @@
project-id (:id project)
team-id (:id team)
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
selected-files (mf/deref refs/selected-files)
selected-project (mf/deref refs/selected-project)
on-finish-import
(mf/use-fn

View file

@ -11,7 +11,7 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.errors :as errors]

View file

@ -7,9 +7,8 @@
(ns app.main.ui.dashboard.libraries
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.data.dashboard :as dd]
[app.main.features :as features]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
@ -18,35 +17,32 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc libraries-page
(mf/defc libraries-page*
{::mf/props :obj}
[{:keys [team] :as props}]
(let [files-map (mf/deref refs/dashboard-shared-files)
projects (mf/deref refs/dashboard-projects)
can-edit (-> team :permissions :can-edit)
[{:keys [team default-project]}]
(let [files
(mf/deref refs/shared-files)
default-project (->> projects vals (d/seek :is-default))
files
(mf/with-memo [files]
(->> (vals files)
(sort-by :modified-at)
(reverse)))
files (mf/with-memo [files-map]
(if (nil? files-map)
nil
(->> (vals files-map)
(sort-by :modified-at)
(reverse))))
can-edit
(-> team :permissions :can-edit)
components-v2 (features/use-feature "components/v2")
[rowref limit] (hooks/use-dynamic-grid-item-width 350)]
[rowref limit]
(hooks/use-dynamic-grid-item-width 350)]
(mf/with-effect [team]
(when team
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname)))))
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
(mf/with-effect []
(st/emit! (dd/fetch-shared-files (:id team))
(mf/with-effect [team]
(st/emit! (dtm/fetch-shared-files)
(dd/clear-selected-files)))
[:*
@ -58,6 +54,5 @@
:project default-project
:origin :libraries
:limit limit
:can-edit can-edit
:library-view? components-v2}]]]))
:can-edit can-edit}]]]))

View file

@ -6,6 +6,7 @@
(ns app.main.ui.dashboard.project-menu
(:require
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
@ -16,7 +17,6 @@
[app.main.ui.dashboard.import :as udi]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[rumext.v2 :as mf]))
(mf/defc project-menu*
@ -32,9 +32,9 @@
on-duplicate-success
(fn [new-project]
(st/emit! (ntf/success (tr "dashboard.success-duplicate-project"))
(rt/nav :dashboard-files
{:team-id (:team-id new-project)
:project-id (:id new-project)})))
(dcm/go-to-dashboard-files
:team-id (:team-id new-project)
:project-id (:id new-project))))
on-duplicate
(fn []
@ -46,7 +46,7 @@
on-move-success
(fn [team-id]
(st/emit! (dd/go-to-projects team-id)))
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))
on-move
(fn [team-id]
@ -57,9 +57,10 @@
delete-fn
(fn [_]
(st/emit! (ntf/success (tr "dashboard.success-delete-project"))
(dd/delete-project project)
(dd/go-to-projects (:team-id project))))
(let [team-id (:team-id project)]
(st/emit! (ntf/success (tr "dashboard.success-delete-project"))
(dd/delete-project project)
(dcm/go-to-dashboard-recent :team-id team-id))))
on-delete
#(st/emit!

View file

@ -8,9 +8,11 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.geom.point :as gpt]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [line-grid]]
@ -23,7 +25,6 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.storage :as storage]
[app.util.time :as dt]
[cuerdas.core :as str]
@ -43,8 +44,10 @@
(def ^:private menu-icon
(i/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header
{::mf/wrap [mf/memo]}
(mf/defc header*
{::mf/wrap [mf/memo]
::mf/props :obj
::mf/private true}
[{:keys [can-edit]}]
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
@ -60,7 +63,7 @@
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [team on-close]}]
(let [on-nav-members-click (mf/use-fn #(st/emit! (dd/go-to-team-members)))
(let [on-nav-members-click (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
on-invite
(mf/use-fn
@ -96,34 +99,35 @@
:aria-label (tr "labels.close")}
close-icon]]))
(def builtin-templates
(l/derived :builtin-templates st/state))
(mf/defc project-item
[{:keys [project first? team files can-edit] :as props}]
(mf/defc project-item*
{::mf/props :obj
::mf/private true}
[{:keys [project is-first team files can-edit]}]
(let [locale (mf/deref i18n/locale)
file-count (or (:count project) 0)
project-id (:id project)
is-draft-proyect (:is-default project)
team-id (:id team)
empty-state-viewer (and (not can-edit)
(= 0 file-count))
file-count (or (:count project) 0)
is-draft? (:is-default project)
empty? (and (not can-edit)
(= 0 file-count))
dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate)
local (mf/use-state {:menu-open false
:menu-pos nil
:edition? (= (:id project) edit-id)})
:edition (= (:id project) edit-id)})
[rowref limit]
(hooks/use-dynamic-grid-item-width)
[rowref limit] (hooks/use-dynamic-grid-item-width)
on-nav
(mf/use-fn
(mf/deps project-id team-id)
(mf/deps project-id)
(fn []
(st/emit! (rt/nav :dashboard-files
{:team-id team-id
:project-id project-id}))))
(st/emit! (dcm/go-to-dashboard-files :project-id project-id))))
toggle-pin
(mf/use-fn
(mf/deps project)
@ -152,7 +156,7 @@
(mf/use-fn #(swap! local assoc :menu-open false))
on-edit-open
(mf/use-fn #(swap! local assoc :edition? true))
(mf/use-fn #(swap! local assoc :edition true))
on-edit
(mf/use-fn
@ -162,15 +166,13 @@
(when-not (str/empty? name)
(st/emit! (-> (dd/rename-project (assoc project :name name))
(with-meta {::ev/origin "dashboard"}))))
(swap! local assoc :edition? false))))
(swap! local assoc :edition false))))
on-file-created
(mf/use-fn
(fn [data]
(let [pparams {:project-id (:project-id data)
:file-id (:id data)}
qparams {:page-id (get-in data [:data :pages 0])}]
(st/emit! (rt/nav :workspace pparams qparams)))))
(fn [{:keys [id data]}]
(let [page-id (get-in data [:pages 0])]
(st/emit! (dcm/go-to-workspace :file-id id :page-id page-id)))))
create-file
(mf/use-fn
@ -189,9 +191,9 @@
on-import
(mf/use-fn
(mf/deps project-id (:id team))
(mf/deps project-id)
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(st/emit! (dpj/fetch-files project-id)
(dd/fetch-recent-files)
(dd/fetch-projects)
(dd/clear-selected-files))))
@ -212,10 +214,10 @@
(on-menu-click event))))
title-width (/ 100 limit)]
[:article {:class (stl/css-case :dashboard-project-row true :first first?)}
[:article {:class (stl/css-case :dashboard-project-row true :first is-first)}
[:header {:class (stl/css :project)}
[:div {:class (stl/css :project-name-wrapper)}
(if (:edition? @local)
(if (:edition @local)
[:& inline-edition {:content (:name project)
:on-end on-edit}]
[:h2 {:on-click on-nav
@ -231,7 +233,6 @@
[:div {:class (stl/css :info-wrapper)}
;; We group these two spans under a div to avoid having extra space between them.
[:div
[:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))]
@ -274,13 +275,13 @@
:on-import on-import}])]]]
[:div {:class (stl/css :grid-container) :ref rowref}
(if empty-state-viewer
[:> empty-placeholder* {:title (if is-draft-proyect
(if ^boolean empty?
[:> empty-placeholder* {:title (if ^boolean is-draft?
(tr "dashboard.empty-placeholder-drafts-title")
(tr "dashboard.empty-placeholder-files-title"))
:class (stl/css :placeholder-placement)
:type 1
:subtitle (if is-draft-proyect
:subtitle (if ^boolean is-draft?
(tr "dashboard.empty-placeholder-drafts-subtitle")
(tr "dashboard.empty-placeholder-files-subtitle"))}]
@ -303,16 +304,19 @@
[:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")]
show-more-icon])]))
(def ref:recent-files
(l/derived :dashboard-recent-files st/state))
(def ^:private ref:recent-files
(l/derived :recent-files st/state))
(mf/defc projects-section
(mf/defc projects-section*
{::mf/props :obj}
[{:keys [team projects profile]}]
(let [projects (->> (vals projects)
(sort-by :modified-at)
(reverse))
(let [projects
(mf/with-memo [projects]
(->> projects
(sort-by :modified-at)
(reverse)))
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
@ -334,7 +338,7 @@
::ev/origin "dashboard"}))))]
(mf/with-effect [show-team-hero?]
(swap! storage/global assoc ::show-team-hero show-team-hero?))
(swap! storage/global assoc ::show-eam-hero show-team-hero?))
(mf/with-effect [team]
(let [tname (if (:is-default team)
@ -348,7 +352,7 @@
(when (seq projects)
[:*
[:& header {:can-edit can-edit}]
[:> header* {:can-edit can-edit}]
[:div {:class (stl/css :projects-container)}
[:*
(when (and show-team-hero?
@ -368,9 +372,9 @@
(->> (vals recent-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:& project-item {:project project
:team team
:files files
:can-edit can-edit
:first? (= project (first projects))
:key id}]))]]]])))
[:> project-item* {:project project
:team team
:files files
:can-edit can-edit
:is-first (= project (first projects))
:key id}]))]]]])))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.dashboard.search
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.data.dashboard :as dd]
[app.main.refs :as refs]
[app.main.store :as st]
@ -15,28 +16,43 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(mf/defc search-page
[{:keys [team search-term] :as props}]
(let [search-term (or search-term "")
result (mf/deref refs/dashboard-search-result)
[rowref limit] (hooks/use-dynamic-grid-item-width)]
(def ^:private ref:search-result
(l/derived :search-result st/state))
(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.search" tname))))))
(def ^:private ref:selected
(l/derived (fn [state]
;; we need to this because :dashboard-search-result is a list
;; of maps and we need a map of maps (using :id as key).
(let [files (d/index-by :id (:search-result state))]
(->> (get state :selected-files)
(refs/extract-selected-files files))))
st/state))
(mf/defc search-page*
{::mf/props :obj}
[{:keys [team search-term]}]
(let [search-term (d/nilv search-term "")
result (mf/deref ref:search-result)
selected (mf/deref ref:selected)
[rowref limit]
(hooks/use-dynamic-grid-item-width)]
(mf/with-effect [team]
(when team
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(dom/set-html-title (tr "title.dashboard.search" tname)))))
(mf/with-effect [search-term]
(st/emit! (dd/search {:search-term search-term})
(dd/clear-selected-files)))
(mf/use-effect
(mf/deps search-term)
(fn []
(st/emit! (dd/search {:search-term search-term})
(dd/clear-selected-files))))
[:*
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-search-title {:class (stl/css :dashboard-title)}
@ -62,6 +78,6 @@
:else
[:& grid {:files result
:hide-new? true
:selected-files selected
:origin :search
:limit limit}])]]))

View file

@ -7,16 +7,18 @@
(ns app.main.ui.dashboard.sidebar
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.auth :as da]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]]
@ -29,8 +31,6 @@
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
@ -74,31 +74,34 @@
edit-id (:project-for-edit dstate)
local* (mf/use-state
{:menu-open false
:menu-pos nil
:edition? (= (:id item) edit-id)
:dragging? false})
#(do {:menu-open false
:menu-pos nil
:edition? (= (:id item) edit-id)
:dragging? false}))
local (deref local*)
project-id (get item :id)
local @local*
on-click
(mf/use-fn
(mf/deps item)
(mf/deps project-id)
(fn []
(st/emit! (dd/go-to-files (:id item)))))
(st/emit! (dcm/go-to-dashboard-files :project-id project-id))))
on-key-down
(mf/use-fn
(mf/deps item)
(mf/deps project-id)
(fn [event]
(when (kbd/enter? event)
(st/emit! (dd/go-to-files (:id item))
(ts/schedule-on-idle
(fn []
(let [project-title (dom/get-element (str (:id item)))]
(when project-title
(dom/set-attribute! project-title "tabindex" "0")
(dom/focus! project-title)
(dom/set-attribute! project-title "tabindex" "-1")))))))))
(st/emit!
(dcm/go-to-dashboard-files :project-id project-id)
(ts/schedule-on-idle
(fn []
(when-let [title (dom/get-element (str project-id))]
(dom/set-attribute! title "tabindex" "0")
(dom/focus! title)
(dom/set-attribute! title "tabindex" "-1"))))))))
on-menu-click
(mf/use-fn
@ -148,9 +151,10 @@
on-drop-success
(mf/use-fn
(mf/deps (:id item))
#(st/emit! (ntf/success (tr "dashboard.success-move-file"))
(dd/go-to-files (:id item))))
(mf/deps project-id)
(fn [_]
(st/emit! (dcm/go-to-dashboard-files :project-id project-id)
(ntf/success (tr "dashboard.success-move-file")))))
on-drop
(mf/use-fn
@ -201,19 +205,18 @@
on-search-change
(mf/use-fn
(mf/deps team-id)
(fn [event]
(let [value (dom/get-target-val event)]
(emit! (dd/go-to-search value)))))
(emit! (dcm/go-to-dashboard-search :term value)))))
on-clear-click
(mf/use-fn
(mf/deps team-id)
(fn [e]
(emit! (dcm/go-to-dashboard-search))
(let [search-input (dom/get-element "search-input")]
(dom/clean-value! search-input)
(dom/focus! search-input)
(emit! (dd/go-to-search))
(dom/prevent-default e)
(dom/stop-propagation e))))
@ -278,7 +281,8 @@
(fn [event]
(let [team-id (-> (dom/get-current-target event)
(dom/get-data "value"))]
(st/emit! (dd/go-to-projects team-id)))))
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
handle-select-default
(mf/use-fn
@ -343,20 +347,22 @@
(mf/defc team-options-dropdown
[{:keys [team profile] :as props}]
(let [go-members #(st/emit! (dd/go-to-team-members))
go-invitations #(st/emit! (dd/go-to-team-invitations))
go-webhooks #(st/emit! (dd/go-to-team-webhooks))
go-settings #(st/emit! (dd/go-to-team-settings))
(let [go-members #(st/emit! (dcm/go-to-dashboard-members))
go-invitations #(st/emit! (dcm/go-to-dashboard-invitations))
go-webhooks #(st/emit! (dcm/go-to-dashboard-webhooks))
go-settings #(st/emit! (dcm/go-to-dashboard-settings))
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
members (get team :members)
permissions (get team :permissions)
can-rename? (or (:is-owner permissions)
(:is-admin permissions))
on-success
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(modal/hide)
(du/fetch-teams)))
;; FIXME: this should be handled in the event, not here
(let [team-id (:default-team-id profile)]
(rx/of (dcm/go-to-dashboard-recent :team-id team-id)
(modal/hide))))
on-error
(fn [{:keys [code] :as error}]
@ -377,15 +383,15 @@
(mf/deps on-success on-error)
(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}))))))
(st/emit! (dtm/leave-current-team (with-meta params
{:on-success on-success
:on-error on-error}))))))
delete-fn
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error})))))
(st/emit! (dtm/delete-team (with-meta team {:on-success on-success
:on-error on-error})))))
on-rename-clicked
(mf/use-fn
(mf/deps team)
@ -406,7 +412,7 @@
(mf/use-fn
(mf/deps team profile leave-fn)
(fn []
(st/emit! (dd/fetch-team-members (:id team))
(st/emit! (dtm/fetch-members)
(modal/show
{:type :leave-and-reassign
:profile profile
@ -590,6 +596,10 @@
(when (get-in team [:permissions :is-owner])
"teams-options-delete-team")]
;; _ (prn "--------------- sidebar-team-switch")
;; _ (app.common.pprint/pprint teams)
handle-show-team-click
(fn [event]
(dom/stop-propagation event)
@ -679,45 +689,45 @@
[:& team-options-dropdown {:team team
:profile profile}]]]))
(mf/defc sidebar-content
[{:keys [projects profile section team project search-term] :as props}]
(mf/defc sidebar-content*
{::mf/private true
::mf/props :obj}
[{:keys [projects profile section team project search-term default-project] :as props}]
(let [default-project-id
(->> (vals projects)
(d/seek :is-default)
(:id))
(get default-project :id)
projects? (= section :dashboard-projects)
team-id (get team :id)
projects? (= section :dashboard-recent)
fonts? (= section :dashboard-fonts)
libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
go-projects
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})))
(mf/use-fn #(st/emit! (dcm/go-to-dashboard-recent)))
go-projects-with-key
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})
(ts/schedule-on-idle
(fn []
(let [projects-title (dom/get-element "dashboard-projects-title")]
(when projects-title
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)
(ts/schedule-on-idle
(fn []
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
go-fonts
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)})))
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)))
go-fonts-with-key
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)})
#(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
@ -727,34 +737,31 @@
(dom/set-attribute! font-title "tabindex" "-1")))))))
go-drafts
(mf/use-fn
(mf/deps team default-project-id)
(mf/deps team-id default-project-id)
(fn []
(st/emit! (rt/nav :dashboard-files
{:team-id (:id team)
:project-id default-project-id}))))
(st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))))
go-drafts-with-key
(mf/use-fn
(mf/deps team default-project-id)
#(st/emit! (rt/nav :dashboard-files {:team-id (:id team)
:project-id default-project-id})
(ts/schedule-on-idle
(fn []
(let [drafts-title (dom/get-element "dashboard-drafts-title")]
(when drafts-title
(dom/set-attribute! drafts-title "tabindex" "0")
(dom/focus! drafts-title)
(dom/set-attribute! drafts-title "tabindex" "-1")))))))
(mf/deps team-id default-project-id)
(fn []
(st/emit! (dcm/go-to-dashboard-files :team-id team-id :project-id default-project-id))
(ts/schedule-on-idle
(fn []
(when-let [title (dom/get-element "dashboard-drafts-title")]
(dom/set-attribute! title "tabindex" "0")
(dom/focus! title)
(dom/set-attribute! title "tabindex" "-1"))))))
go-libs
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})))
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)))
go-libs-with-key
(mf/use-fn
(mf/deps team)
#(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
@ -763,7 +770,7 @@
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1")))))))
pinned-projects
(->> (vals projects)
(->> projects
(remove :is-default)
(filter :is-pinned))]
@ -826,11 +833,12 @@
pin-icon
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]))
(mf/defc profile-section
[{:keys [profile team] :as props}]
(mf/defc profile-section*
{::mf/props :obj}
[{:keys [profile team]}]
(let [show* (mf/use-state false)
show (deref show*)
photo (cf/resolve-profile-photo-url profile)
photo (cf/resolve-profile-photo-url profile)
on-click
(mf/use-fn
@ -875,14 +883,13 @@
(when (kbd/enter? event)
(reset! show* true))))
handle-close
on-close
(fn [event]
(dom/stop-propagation event)
(reset! show* false))
handle-key-down-profile
(mf/use-fn
(mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
(on-click :settings-profile event))))
@ -910,34 +917,27 @@
(show-release-notes))))
handle-feedback-click
(mf/use-fn
(mf/deps on-click)
#(on-click :settings-feedback %))
(mf/use-fn #(on-click :settings-feedback %))
handle-feedback-keydown
(mf/use-fn
(mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
(on-click :settings-feedback event))))
handle-logout-click
(mf/use-fn
(mf/deps on-click)
#(on-click (du/logout) %))
#(on-click (da/logout) %))
handle-logout-keydown
(mf/use-fn
(mf/deps on-click)
(fn [event]
(when (kbd/enter? event)
(on-click (du/logout) event))))
(on-click (da/logout) event))))
handle-set-profile
(mf/use-fn
(mf/deps on-click)
(fn [event]
(on-click :settings-profile event)))]
#(on-click :settings-profile %))]
[:*
(when (and team profile)
@ -959,7 +959,9 @@
:alt (:fullname profile)}]
[:span {:class (stl/css :profile-fullname)} (:fullname profile)]]
[:& dropdown-menu {:on-close handle-close :show show :list-class (stl/css :profile-dropdown)}
[:& dropdown-menu {:on-close on-close
:show show
:list-class (stl/css :profile-dropdown)}
[:li {:tab-index (if show "0" "-1")
:class (stl/css :profile-dropdown-item)
:on-click handle-set-profile
@ -1045,15 +1047,13 @@
:show? show-comments?
:on-show-comments handle-show-comments}])]]))
(mf/defc sidebar
{::mf/wrap-props false
(mf/defc sidebar*
{::mf/props :obj
::mf/wrap [mf/memo]}
[props]
(let [team (obj/get props "team")
profile (obj/get props "profile")]
[:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
[:> sidebar-content props]
[:& profile-section
{:profile profile
:team team}]]))
[{:keys [team profile] :as props}]
[:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
[:> sidebar-content* props]
[:> profile-section*
{:profile profile
:team team}]])

View file

@ -11,11 +11,11 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.config :as cfg]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
@ -31,6 +31,7 @@
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private arrow-icon
@ -61,10 +62,10 @@
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [section team]}]
(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)))
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
(let [on-nav-members (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
on-nav-settings (mf/use-fn #(st/emit! (dcm/go-to-dashboard-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dcm/go-to-dashboard-invitations)))
on-nav-webhooks (mf/use-fn #(st/emit! (dcm/go-to-dashboard-webhooks)))
route (mf/deref refs/route)
invite-email (-> route :query-params :invite-email)
@ -139,12 +140,12 @@
::mf/register-as :invite-members
::mf/props :obj}
[{:keys [team origin invite-email]}]
(let [members-map (mf/deref refs/dashboard-team-members)
perms (:permissions team)
(let [members (get team :members)
perms (get team :permissions)
team-id (get team :id)
roles (mf/with-memo [perms]
(get-available-roles perms))
team-id (:id team)
initial (mf/with-memo [team-id invite-email]
(if invite-email
@ -156,7 +157,7 @@
error-text (mf/use-state "")
current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
current-members-emails (into #{} (map (comp :email second)) members-map)
current-members-emails (into #{} (map :email) members)
on-success
(fn [_form {:keys [total]}]
@ -164,8 +165,8 @@
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))
(st/emit! (modal/hide)
(dd/fetch-team-members)
(dd/fetch-team-invitations)))
(dtm/fetch-members)
(dtm/fetch-invitations)))
on-error
(fn [_form cause]
@ -198,11 +199,11 @@
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
(st/emit! (-> (dtm/create-invitations (with-meta params mdata))
(with-meta {::ev/origin origin}))
(dd/fetch-team-invitations)
(dd/fetch-team-members (:id team)))))]
;; FIXME: looks duplicate
(dtm/fetch-invitations)
(dtm/fetch-members))))]
[:div {:class (stl/css-case :modal-team-container true
:modal-team-container-workspace (= origin :workspace)
@ -338,7 +339,8 @@
(when is-you?
[:li {:on-click on-leave
:class (stl/css :action-dropdown-item)
:key "is-you-option"} (tr "dashboard.leave-team")])
:key "is-you-option"}
(tr "dashboard.leave-team")])
(when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
[:li {:on-click on-delete
:class (stl/css :action-dropdown-item)
@ -346,18 +348,18 @@
(defn- set-role! [member-id role]
(let [params {:member-id member-id :role role}]
(st/emit! (dd/update-team-member-role params))))
(st/emit! (dtm/update-member-role params))))
(mf/defc team-member
(mf/defc team-member*
{::mf/wrap [mf/memo]
::mf/props :obj}
[{:keys [team member members profile]}]
[{:keys [team member total-members profile]}]
(let [member-id (:id member)
(let [member-id (:id member)
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
on-set-viewer (mf/use-fn (mf/deps member-id) (partial set-role! member-id :viewer))
owner? (dm/get-in team [:permissions :is-owner])
owner? (dm/get-in team [:permissions :is-owner])
on-set-owner
(mf/use-fn
@ -373,18 +375,12 @@
(st/emit! (modal/show params)))))
on-success
(mf/use-fn
(mf/deps profile)
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(modal/hide)
(du/fetch-teams))))
(mf/use-fn #(rx/of (dcm/go-to-dashboard-recent :team-id :default)))
on-error
(mf/use-fn
(fn [{:keys [code] :as error}]
(condp = code
:no-enough-members-for-leave
(rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))
@ -400,17 +396,17 @@
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error})))))
(st/emit! (dtm/delete-team (with-meta team {:on-success on-success
:on-error on-error})))))
on-leave-accepted
(mf/use-fn
(mf/deps on-success on-error)
(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}))))))
(st/emit! (dtm/leave-current-team (with-meta params
{:on-success on-success
:on-error on-error}))))))
on-leave-and-close
(mf/use-fn
@ -428,7 +424,7 @@
(mf/use-fn
(mf/deps profile team on-leave-accepted)
(fn []
(st/emit! (dd/fetch-team-members (:id team))
(st/emit! (dtm/fetch-members)
(modal/show
{:type :leave-and-reassign
:profile profile
@ -450,7 +446,7 @@
(mf/use-fn
(mf/deps member-id)
(fn []
(let [on-accept #(st/emit! (dd/delete-team-member {:member-id member-id}))
(let [on-accept #(st/emit! (dtm/delete-member {:member-id member-id}))
params {:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
@ -459,7 +455,7 @@
(st/emit! (modal/show params)))))
on-leave'
(cond (= 1 (count members)) on-leave-and-close
(cond (= 1 total-members) on-leave-and-close
(= true owner?) on-change-owner-and-leave
:else on-leave)]
@ -483,16 +479,26 @@
:on-delete on-delete
:on-leave on-leave'}]]]))
(mf/defc team-members
{::mf/props :obj}
[{:keys [members-map team profile]}]
(let [members (mf/with-memo [members-map]
(->> (vals members-map)
(sort-by :created-at)
(remove :is-owner)))
owner (mf/with-memo [members-map]
(->> (vals members-map)
(d/seek :is-owner)))]
(mf/defc team-members*
{::mf/props :obj
::mf/private true}
[{:keys [team profile]}]
(let [members (get team :members)
total-members
(count members)
owner
(mf/with-memo [members]
(d/seek :is-owner members))
members
(mf/with-memo [team]
(->> (:members team)
(sort-by :created-at)
(remove :is-owner)
(vec)))]
[:div {:class (stl/css :dashboard-table :team-members)}
[:div {:class (stl/css :table-header)}
@ -500,42 +506,39 @@
[:div {:class (stl/css :table-field :title-field-role)} (tr "labels.role")]]
[:div {:class (stl/css :table-rows)}
[:& team-member
[:> team-member*
{:member owner
:team team
:profile profile
:members members-map}]
:total-members total-members}]
(for [item members]
[:& team-member
[:> team-member*
{:member item
:team team
:profile profile
:key (:id item)
:members members-map}])]]))
:key (dm/str (:id item))
:total-members total-members}])]]))
(mf/defc team-members-page
(mf/defc team-members-page*
{::mf/props :obj}
[{:keys [team profile]}]
(let [members-map (mf/deref refs/dashboard-team-members)]
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-members"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-members"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect []
(st/emit! (dtm/fetch-members)))
(mf/with-effect [team]
(st/emit! (dd/fetch-team-members (:id team))))
[:*
[:& header {:section :dashboard-team-members :team team}]
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
[:& team-members
{:profile profile
:team team
:members-map members-map}]]]))
[:*
[:& header {:section :dashboard-team-members :team team}]
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
[:> team-members*
{:profile profile
:team team}]]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
@ -587,8 +590,9 @@
:on-click on-change'}
(tr "labels.viewer")]]]]))
(mf/defc invitation-actions
{::mf/props :obj}
(mf/defc invitation-actions*
{::mf/props :obj
::mf/private true}
[{:keys [invitation team-id]}]
(let [show? (mf/use-state false)
@ -622,15 +626,15 @@
(mf/deps email team-id)
(fn []
(let [params {:email email :team-id team-id}
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
(st/emit! (dtm/delete-invitation (with-meta params mdata))))))
on-resend-success
(mf/use-fn
(fn []
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations))))
(dtm/fetch-invitations))))
on-resend
(mf/use-fn
@ -643,7 +647,7 @@
{:on-success on-resend-success
:on-error on-error})]
(st/emit!
(-> (dd/invite-team-members params)
(-> (dtm/create-invitations params)
(with-meta {::ev/origin :team}))))))
on-copy-success
@ -660,7 +664,7 @@
{:on-success on-copy-success
:on-error on-error})]
(st/emit!
(-> (dd/copy-invitation-link params)
(-> (dtm/copy-invitation-link params)
(with-meta {::ev/origin :team}))))))
on-hide (mf/use-fn #(reset! show? false))
@ -694,17 +698,19 @@
role (:role invitation)
status (if expired? :expired :pending)
type (if expired? :warning :default)
badge-content (if (= status :expired)
(tr "labels.expired-invitation")
(tr "labels.pending-invitation"))
badge-content
(if (= status :expired)
(tr "labels.expired-invitation")
(tr "labels.pending-invitation"))
on-change-role
(mf/use-fn
(mf/deps email team-id)
(fn [role _event]
(let [params {:email email :team-id team-id :role role}
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
(st/emit! (dtm/update-invitation-role (with-meta params mdata))))))]
[:div {:class (stl/css :table-row :table-row-invitations)}
[:div {:class (stl/css :table-field :field-email)} email]
@ -720,25 +726,36 @@
[:& badge-notification {:type type :content badge-content}]]
[:div {:class (stl/css :table-field :field-actions)}
(when can-invite
[:& invitation-actions
(when ^boolean can-invite
[:> invitation-actions*
{:invitation invitation
:team-id team-id}])]]))
(mf/defc empty-invitation-table
[{:keys [can-invite] :as props}]
(mf/defc empty-invitation-table*
{::mf/props :obj
::mf/private true}
[{:keys [can-invite]}]
[:div {:class (stl/css :empty-invitations)}
[:span (tr "labels.no-invitations")]
(when can-invite
(when ^boolean can-invite
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
:tag-name "span"}])])
(mf/defc invitation-section
[{:keys [team invitations] :as props}]
(let [owner? (dm/get-in team [:permissions :is-owner])
admin? (dm/get-in team [:permissions :is-admin])
can-invite (or owner? admin?)
team-id (:id team)]
(def ^:private ref:invitations
(l/derived :invitations st/state))
(mf/defc invitation-section*
{::mf/props :obj
::mf/private true}
[{:keys [team]}]
(let [permissions (get team :permissions)
invitations (mf/deref ref:invitations)
team-id (get team :id)
owner? (get permissions :is-owner)
admin? (get permissions :is-admin)
can-invite? (or owner? admin?)]
[:div {:class (stl/css :invitations)}
[:div {:class (stl/css :table-header)}
@ -746,38 +763,34 @@
[:div {:class (stl/css :title-field-role)} (tr "labels.role")]
[:div {:class (stl/css :title-field-status)} (tr "labels.status")]]
(if (empty? invitations)
[:& empty-invitation-table {:can-invite can-invite}]
[:> empty-invitation-table* {:can-invite can-invite?}]
[:div {:class (stl/css :table-rows)}
(for [invitation invitations]
[:> invitation-row*
{:key (:email invitation)
:invitation invitation
:can-invite can-invite
:can-invite can-invite?
:team-id team-id}])])]))
(mf/defc team-invitations-page
(mf/defc team-invitations-page*
{::mf/props :obj}
[{:keys [team]}]
(let [invitations (mf/deref refs/dashboard-team-invitations)]
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-invitations"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-invitations"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect []
(st/emit! (dd/fetch-team-invitations)))
(mf/with-effect []
(st/emit! (dtm/fetch-invitations)))
[:*
[:& header {:section :dashboard-team-invitations
:team team}]
[:section {:class (stl/css :dashboard-team-invitations)}
;; TODO: We should consider adding a "loading state" here
;; with an (if (nil? invitations) [:& loading-state] [:& invitations])
(when-not (nil? invitations)
[:& invitation-section {:team team
:invitations invitations}])]]))
[:*
[:& header {:section :dashboard-team-invitations
:team team}]
[:section {:class (stl/css :dashboard-team-invitations)}
[:> invitation-section* {:team team}]]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBHOOKS SECTION
@ -811,9 +824,8 @@
(mf/use-fn
(fn [_]
(let [message (tr "dashboard.webhooks.create.success")]
(st/emit! (dd/fetch-team-webhooks)
(ntf/success message)
(modal/hide)))))
(rx/of (ntf/success message)
(modal/hide)))))
on-error
(mf/use-fn
@ -846,7 +858,7 @@
params {:uri (:uri cdata)
:mtype (:mtype cdata)
:is-active (:is-active cdata)}]
(st/emit! (dd/create-team-webhook
(st/emit! (dtm/create-webhook
(with-meta params mdata))))))
on-update-submit
@ -855,7 +867,7 @@
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
(st/emit! (dd/update-team-webhook
(st/emit! (dtm/update-webhook
(with-meta params mdata))))))
on-submit
@ -910,7 +922,7 @@
(tr "modals.edit-webhook.submit-label")
(tr "modals.create-webhook.submit-label"))}]]]]]]))
(mf/defc webhooks-hero
(mf/defc webhooks-hero*
{::mf/props :obj}
[]
[:div {:class (stl/css :webhooks-hero-container)}
@ -922,7 +934,7 @@
:on-click #(st/emit! (modal/show :webhook {}))}
(tr "dashboard.webhooks.create")]])
(mf/defc webhook-actions
(mf/defc webhook-actions*
{::mf/props :obj
::mf/private true}
[{:keys [on-edit on-delete can-edit]}]
@ -945,8 +957,10 @@
:class (stl/css :menu-disabled)}
[:> icon* {:id "menu"}]])))
(mf/defc webhook-item
{::mf/wrap [mf/memo]}
(mf/defc webhook-item*
{::mf/wrap [mf/memo]
::mf/props :obj
::mf/private true}
[{:keys [webhook permissions]}]
(let [error-code (:error-code webhook)
id (:id webhook)
@ -966,8 +980,8 @@
(mf/deps id)
(fn []
(let [params {:id id}
mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
(st/emit! (dd/delete-team-webhook (with-meta params mdata))))))
mdata {:on-success #(st/emit! (dtm/fetch-webhooks))}]
(st/emit! (dtm/delete-webhook (with-meta params mdata))))))
on-delete
(mf/use-fn
@ -1005,22 +1019,29 @@
(tr "labels.active")
(tr "labels.inactive"))]]
[:div {:class (stl/css :table-field :actions)}
[:& webhook-actions
[:> webhook-actions*
{:on-edit on-edit
:on-delete on-delete
:can-edit can-edit}]]]))
(mf/defc webhooks-list
{::mf/props :obj}
(mf/defc webhooks-list*
{::mf/props :obj
::mf/private true}
[{:keys [webhooks permissions]}]
[:div {:class (stl/css :table-rows :webhook-table)}
(for [webhook webhooks]
[:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])])
[:> webhook-item*
{:webhook webhook
:key (dm/str (:id webhook))
:permissions permissions}])])
(mf/defc team-webhooks-page
(def ^:private ref:webhooks
(l/derived :webhooks st/state))
(mf/defc webhooks-page*
{::mf/props :obj}
[{:keys [team]}]
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
(let [webhooks (mf/deref ref:webhooks)]
(mf/with-effect [team]
(dom/set-html-title
@ -1030,33 +1051,34 @@
(:name team)))))
(mf/with-effect [team]
(st/emit! (dd/fetch-team-webhooks)))
(st/emit! (dtm/fetch-webhooks)))
[:*
[:& header {:team team :section :dashboard-team-webhooks}]
[:section {:class (stl/css :dashboard-container :dashboard-team-webhooks)}
[:*
[:& webhooks-hero]
[:> webhooks-hero* {}]
(if (empty? webhooks)
[:div {:class (stl/css :webhooks-empty)}
[:div (tr "dashboard.webhooks.empty.no-webhooks")]
[:div (tr "dashboard.webhooks.empty.add-one")]]
[:& webhooks-list {:webhooks webhooks :permissions (:permissions team)}])]]]))
[:> webhooks-list*
{:webhooks webhooks
:permissions (:permissions team)}])]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETTINGS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc team-settings-page
(mf/defc team-settings-page*
{::mf/props :obj}
[{:keys [team]}]
(let [finput (mf/use-ref)
members-map (mf/deref refs/dashboard-team-members)
owner (->> (vals members-map)
(d/seek :is-owner))
members (get team :members)
stats (get team :stats)
stats (mf/deref refs/dashboard-team-stats)
owner (d/seek :is-owner members)
permissions (:permissions team)
can-edit (or (:is-owner permissions)
@ -1067,8 +1089,7 @@
on-file-selected
(fn [file]
(st/emit! (dd/update-team-photo file)))]
(st/emit! (dtm/update-team-photo file)))]
(mf/with-effect [team]
(dom/set-html-title (tr "title.team-settings"
@ -1076,11 +1097,9 @@
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect [team]
(let [team-id (:id team)]
(st/emit! (dd/fetch-team-members team-id)
(dd/fetch-team-stats team-id))))
(mf/with-effect []
(st/emit! (dtm/fetch-members)
(dtm/fetch-stats)))
[:*
[:& header {:section :dashboard-team-settings :team team}]
@ -1116,7 +1135,7 @@
[:div {:class (stl/css :block-content)}
user-icon
[:span {:class (stl/css :block-text)}
(tr "dashboard.num-of-members" (count members-map))]]]
(tr "dashboard.num-of-members" (count members))]]]
[:div {:class (stl/css :block)}
[:div {:class (stl/css :block-label)}

View file

@ -8,17 +8,17 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.team :as dtm]
[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.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@ -28,15 +28,15 @@
(defn- on-create-success
[_form response]
(let [msg "Team created successfully"]
(st/emit! (ntf/success msg)
(modal/hide)
(rt/nav :dashboard-projects {:team-id (:id response)}))))
(let [message "Team created successfully"
team-id (:id response)]
(st/emit! (ntf/success message)
(dcm/go-to-dashboard-recent :team-id team-id))))
(defn- on-update-success
[_form _response]
(let [msg "Team created successfully"]
(st/emit! (ntf/success msg)
(let [message "Team created successfully"]
(st/emit! (ntf/success message)
(modal/hide))))
(defn- on-error
@ -51,7 +51,7 @@
(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))
(st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :dashboard})))))
(defn- on-update-submit
@ -59,7 +59,7 @@
(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))
(st/emit! (dtm/update-team (with-meta team mdata))
(modal/hide))))
(defn- on-submit

View file

@ -9,8 +9,9 @@
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
@ -18,7 +19,6 @@
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.storage :as storage]
[okulary.core :as l]
[potok.v2.core :as ptk]
@ -43,9 +43,9 @@
:section section})
(when-not (some? project-id)
(rt/nav :dashboard-files
{:team-id team-id
:project-id default-project-id}))))]
(dcm/go-to-dashboard-recent
:team-id team-id
:project-id default-project-id))))]
(st/emit!
(ptk/event ::ev/event {::ev/name "import-template-launch"
@ -157,8 +157,8 @@
[:div {:class (stl/css :template-link-title)} (tr "dashboard.libraries-and-templates")]
[:div {:class (stl/css :template-link-text)} (tr "dashboard.libraries-and-templates.explore")]]]]]]))
(mf/defc templates-section
{::mf/wrap-props false}
(mf/defc templates-section*
{::mf/props :obj}
[{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]

View file

@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@ -25,17 +26,25 @@
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema:input}
[{:keys [icon class type external-ref] :rest props}]
(let [ref (or external-ref (mf/use-ref))
type (or type "text")
icon-class (stl/css-case :input true
:input-with-icon (some? icon))
props (mf/spread-props props {:class icon-class :ref ref :type type})
handle-icon-click (mf/use-fn (mf/deps ref)
(fn [_]
(let [input-node (mf/ref-val ref)]
(dom/select-node input-node)
(dom/focus! input-node))))]
[:> "span" {:class (dm/str class " " (stl/css :container))}
(when icon [:> icon* {:id icon :class (stl/css :icon) :on-click handle-icon-click}])
[:> "input" props]]))
[{:keys [icon class type] :rest props} ref]
(let [ref (or ref (mf/use-ref))
type (d/nilv type "text")
props (mf/spread-props props
:class (stl/css-case
:input true
:input-with-icon (some? icon))
:ref ref
:type type)
on-icon-click
(mf/use-fn
(mf/deps ref)
(fn [_event]
(let [input-node (mf/ref-val ref)]
(dom/select-node input-node)
(dom/focus! input-node))))]
[:> :span {:class (dm/str class " " (stl/css :container))}
(when (some? icon)
[:> icon* {:id icon :class (stl/css :icon) :on-click on-icon-click}])
[:> :input props]]))

View file

@ -7,9 +7,9 @@
(ns app.main.ui.onboarding.newsletter
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.events :as-alias ev]
[app.main.data.event :as-alias ev]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]

View file

@ -11,8 +11,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.events :as-alias ev]
[app.main.data.users :as du]
[app.main.data.event :as-alias ev]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]

View file

@ -9,15 +9,15 @@
(:require
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.users :as du]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.profile :as du]
[app.main.data.team :as dtm]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@ -84,7 +84,7 @@
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
:onboarding-viewed true})
(when go-to-team?
(rt/nav :dashboard-projects {:team-id team-id}))))))
(dcm/go-to-dashboard-recent :team-id team-id))))))
on-error
(mf/use-fn
@ -117,7 +117,7 @@
(let [mdata {:on-success on-success
:on-error on-error}
params {:name name}]
(st/emit! (-> (dd/create-team (with-meta params mdata))
(st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :onboarding-without-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
@ -133,7 +133,7 @@
(let [mdata {:on-success on-success
:on-error on-error}]
(st/emit! (-> (dd/create-team-with-invitations (with-meta params mdata))
(st/emit! (-> (dtm/create-team-with-invitations (with-meta params mdata))
(with-meta {::ev/origin :onboarding-with-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"

View file

@ -7,7 +7,7 @@
(ns app.main.ui.releases
(:require
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10]

View file

@ -7,34 +7,17 @@
(ns app.main.ui.routes
(:require
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.users :as du]
[app.main.data.team :as dtm]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::section ::us/keyword)
(s/def ::index ::us/integer)
(s/def ::token (s/nilable ::us/not-empty-string))
(s/def ::share-id ::us/uuid)
(s/def ::viewer-path-params
(s/keys :req-un [::file-id]))
(s/def ::viewer-query-params
(s/keys :opt-un [::index ::share-id ::section ::page-id]))
(s/def ::any any?)
(def routes
[["/auth"
["/login" :auth-login]
@ -53,11 +36,10 @@
["/access-tokens" :settings-access-tokens]]
["/frame-preview" :frame-preview]
["/view/:file-id"
{:name :viewer
:conform
{:path-params ::viewer-path-params
:query-params ::viewer-query-params}}]
["/view" :viewer]
["/view/:file-id" :viewer-legacy]
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
@ -65,33 +47,32 @@
;; Used for export
["/render-sprite/:file-id" :render-sprite]
["/dashboard/team/:team-id"
["/members" :dashboard-team-members]
["/invitations" :dashboard-team-invitations]
["/webhooks" :dashboard-team-webhooks]
["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects]
["/dashboard"
["/members" :dashboard-members]
["/invitations" :dashboard-invitations]
["/webhooks" :dashboard-webhooks]
["/settings" :dashboard-settings]
["/recent" :dashboard-recent]
["/search" :dashboard-search]
["/fonts" :dashboard-fonts]
["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]]
["/files" :dashboard-files]]
["/workspace/:project-id/:file-id" :workspace]])
["/dashboard/team/:team-id"
["/members" :dashboard-legacy-team-members]
["/invitations" :dashboard-legacy-team-invitations]
["/webhooks" :dashboard-legacy-team-webhooks]
["/settings" :dashboard-legacy-team-settings]
["/projects" :dashboard-legacy-projects]
["/search" :dashboard-legacy-search]
["/fonts" :dashboard-legacy-fonts]
["/fonts/providers" :dashboard-legacy-font-providers]
["/libraries" :dashboard-legacy-libraries]
["/projects/:project-id" :dashboard-legacy-files]]
(defn- match-path
[router path]
(when-let [match (rt/match router path)]
(if-let [conform (get-in match [:data :conform])]
(let [spath (get conform :path-params ::any)
squery (get conform :query-params ::any)]
(try
(-> (dissoc match :params)
(assoc :path-params (us/conform spath (get match :path-params))
:query-params (us/conform squery (get match :query-params))))
(catch :default _
nil)))
match)))
["/workspace" :workspace]
["/workspace/:project-id/:file-id" :workspace-legacy]])
(defn on-navigate
[router path]
@ -99,8 +80,9 @@
[base-path qs] (str/split path "?")
location-path (dm/str (.-origin location) (.-pathname location))
valid-location? (= location-path (dm/str cf/public-uri))
match (match-path router path)
match (rt/match router path)
empty-path? (or (= base-path "") (= base-path "/"))]
(cond
(not valid-location?)
(st/emit! (rt/assign-exception {:type :not-found}))
@ -119,7 +101,11 @@
(st/emit! (rt/nav :auth-login))
empty-path?
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)} (u/query-string->map qs)))
(let [team-id (or (dtm/get-last-team-id)
(:default-team-id profile))]
(st/emit! (rt/nav :dashboard-recent
(-> (u/query-string->map qs)
(assoc :team-id team-id)))))
:else
(st/emit! (rt/assign-exception {:type :not-found})))))))))

View file

@ -9,6 +9,7 @@
(:require
[app.main.data.dashboard.shortcuts :as sc]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
@ -20,7 +21,6 @@
[app.main.ui.settings.profile :refer [profile-page]]
[app.main.ui.settings.sidebar :refer [sidebar]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[rumext.v2 :as mf]))
(mf/defc header

View file

@ -10,7 +10,7 @@
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.forms :as fm]

View file

@ -10,7 +10,7 @@
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]

View file

@ -9,7 +9,7 @@
(:require
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
@ -24,7 +24,7 @@
(defn- on-success
[profile]
(st/emit! (ntf/success (tr "notifications.profile-saved"))
(du/profile-fetched profile)))
(du/initialize-profile profile)))
(defn- on-submit
[form _event]

View file

@ -9,7 +9,7 @@
(:require
[app.common.schema :as sm]
[app.main.data.notifications :as ntf]
[app.main.data.users :as udu]
[app.main.data.profile :as udu]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.dom :as dom]

View file

@ -11,7 +11,7 @@
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]

View file

@ -8,15 +8,16 @@
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.team :as dtm]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.dashboard.sidebar :refer [profile-section]]
[app.main.ui.dashboard.sidebar :refer [profile-section*]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@ -26,6 +27,7 @@
(def ^:private feedback-icon
(i/icon-xref :feedback (stl/css :feedback-icon)))
;; FIXME: move to common
(def ^:private go-settings-profile
#(st/emit! (rt/nav :settings-profile)))
@ -58,12 +60,13 @@
options? (= section :settings-options)
feedback? (= section :settings-feedback)
access-tokens? (= section :settings-access-tokens)
team-id (du/get-current-team-id profile)
team-id (or (dtm/get-last-team-id)
(:default-team-id profile))
go-dashboard
(mf/use-fn
(mf/deps team-id)
#(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))]
#(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))]
[:div {:class (stl/css :sidebar-content)}
[:div {:class (stl/css :sidebar-content-section)}
@ -119,5 +122,5 @@
[:div {:class (stl/css :dashboard-sidebar :settings)}
[:& sidebar-content {:profile profile
:section section}]
[:& profile-section {:profile profile}]])
[:> profile-section* {:profile profile}]])

View file

@ -11,22 +11,22 @@
[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.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[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 :as register]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.sidebar :refer [sidebar*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.header :as viewer.header]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@ -213,7 +213,8 @@
(mf/use-fn
(mf/deps profile)
(fn []
(st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))))
(let [team-id (:default-team-id profile)]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
on-success
(mf/use-fn
@ -233,7 +234,7 @@
{:team-id team-id})
mdata {:on-success on-success
:on-error on-error}]
(st/emit! (dc/create-team-access-request
(st/emit! (dcm/create-team-access-request
(with-meta params mdata))))))]
[:*
@ -267,7 +268,7 @@
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
[:& sidebar
[:> sidebar*
{:team nil
:projects []
:project (:default-project-id profile)

View file

@ -25,7 +25,7 @@
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]]
[app.main.ui.viewer.comments :refer [comments-layer comments-sidebar*]]
[app.main.ui.viewer.header :as header]
[app.main.ui.viewer.inspect :as inspect]
[app.main.ui.viewer.interactions :as interactions]
@ -129,8 +129,8 @@
:comment-sidebar show-sidebar?}]
(when show-sidebar?
[:& comments-sidebar
{:users users
[:> comments-sidebar*
{:profiles users
:frame frame
:page page}])]))
@ -274,9 +274,9 @@
:page page
:zoom zoom}])]])
(mf/defc viewer-content
{::mf/wrap-props false}
[{:keys [data page-id share-id section index interactions-mode share] :as props}]
(mf/defc viewer-content*
{::mf/props :obj}
[{:keys [data page-id share-id section index interactions-mode share]}]
(let [{:keys [file users project permissions]} data
allowed (or
(= section :interactions)
@ -620,8 +620,8 @@
;; --- Component: Viewer
(mf/defc viewer
{::mf/wrap-props false}
(mf/defc viewer*
{::mf/props :obj}
[{:keys [file-id share-id page-id] :as props}]
(mf/with-effect [file-id page-id share-id]
(let [params {:file-id file-id
@ -630,9 +630,10 @@
(st/emit! (dv/initialize params))
(fn []
(st/emit! (dv/finalize params)))))
(if-let [data (mf/deref refs/viewer-data)]
(let [props (obj/merge props #js {:data data :key (dm/str file-id)})]
[:> viewer-content props])
[:> viewer-content* props])
[:> loader* {:title (tr "labels.loading")
:overlay true}]))

View file

@ -14,7 +14,7 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.comments :as cmt]
@ -220,7 +220,7 @@
{:thread thread
:position-modifier modifier1
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
:users users
:profiles users
:zoom zoom}])
(when-let [draft (:draft local)]
@ -231,10 +231,11 @@
:on-submit on-draft-submit
:zoom zoom}])]]]))
(mf/defc comments-sidebar
[{:keys [users frame page]}]
(mf/defc comments-sidebar*
{::mf/props :obj}
[{:keys [profiles frame page]}]
(let [profile (mf/deref refs/profile)
local (mf/deref refs/comments-local)
local (mf/deref refs/comments-local)
threads-map (mf/deref refs/comment-threads)
threads (->> (vals threads-map)
(dcm/apply-filters local profile)
@ -242,4 +243,8 @@
(gsh/has-point? frame position))))]
[:aside {:class (stl/css :comments-sidebar)}
[:div {:class (stl/css :settings-bar-inside)}
[:& wc/comments-sidebar {:from-viewer true :users users :threads threads :page-id (:id page)}]]]))
[:> wc/comments-sidebar*
{:from-viewer true
:profiles profiles
:threads threads
:page-id (:id page)}]]]))

View file

@ -18,7 +18,7 @@
[app.main.ui.formats :as fmt]
[app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-menu]]
[app.main.ui.viewer.interactions :refer [flows-menu* interactions-menu]]
[app.main.ui.viewer.interactions :refer [flows-menu* interactions-menu*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
@ -173,7 +173,8 @@
:interactions [:*
(when index
[:> flows-menu* {:page page :index index}])
[:& interactions-menu {:interactions-mode interactions-mode}]]
[:> interactions-menu*
{:interactions-mode interactions-mode}]]
:comments [:& comments-menu]
[:div {:class (stl/css :view-options)}])

View file

@ -13,7 +13,7 @@
[app.common.geom.shapes :as gsh]
[app.common.types.shape-tree :as ctst]
[app.config :as cfg]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
@ -30,6 +30,7 @@
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@ -49,13 +50,26 @@
</body>
</html>")
;; FIXME: this code need to be refactored
(defn get-viewer-objects
([]
(let [route (deref refs/route)
page-id (:page-id (:query-params route))]
(get-viewer-objects page-id)))
([page-id]
(l/derived
(fn [state]
(let [objects (refs/get-viewer-objects state page-id)]
objects))
st/state =)))
(defn- use-objects [from]
(let [page-objects-ref
(mf/with-memo [from]
(if (= from :workspace)
;; FIXME: fix naming consistency issues
refs/workspace-page-objects
(refs/get-viewer-objects)))]
(get-viewer-objects)))]
(mf/deref page-objects-ref)))
(defn- shapes->images

View file

@ -9,7 +9,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.types.component :as ctk]
[app.main.data.events :as ev]
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.shape-icon :as sir]

View file

@ -267,7 +267,8 @@
(when (= flow-id (:id current-flow))
[:span {:class (stl/css :icon)} i/tick])])]]])))
(mf/defc interactions-menu
(mf/defc interactions-menu*
{::mf/props :obj}
[{:keys [interactions-mode]}]
(let [show-dropdown? (mf/use-state false)
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
@ -281,6 +282,7 @@
(keyword))]
(dom/stop-propagation event)
(st/emit! (dv/set-interactions-mode mode)))))]
[:div {:on-click toggle-dropdown
:class (stl/css :view-options)}
[:span {:class (stl/css :dropdown-title)} (tr "viewer.header.interactions")]

Some files were not shown because too many files have changed in this diff Show more