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)] (if-let [media-id (:media-id row)]
(-> row (-> row
(dissoc :media-id) (dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id))) (assoc :thumbnail-id media-id))
(dissoc row :media-id)))) (dissoc row :media-id))))
(map #(assoc % :library-summary (get-library-summary cfg %))) (map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data)))))) (map #(dissoc % :data))))))

View file

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

View file

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

View file

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

View file

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

View file

@ -18,11 +18,18 @@
java.nio.ByteBuffer))) java.nio.ByteBuffer)))
(defn uuid (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] [s]
#?(:clj (UUID/fromString s) #?(:clj (UUID/fromString s)
:cljs (c/uuid 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 (defn next
[] []
#?(:clj (UUIDv8/create) #?(:clj (UUIDv8/create)

View file

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

View file

@ -6,6 +6,7 @@
"~:modified-at": "~m1714045654874", "~:modified-at": "~m1714045654874",
"~:name": "New File 2", "~:name": "New File 2",
"~:revn": 1, "~:revn": 1,
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false "~:is-shared": false
}, },
{ {
@ -15,6 +16,7 @@
"~:modified-at": "~m1713519762931", "~:modified-at": "~m1713519762931",
"~:name": "New File 1", "~:name": "New File 1",
"~:revn": 1, "~:revn": 1,
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
"~:is-shared": false "~: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=*", "get-project-files?project-id=*",
"dashboard/get-project-files.json", "dashboard/get-project-files.json",
); );
await this.mockRPC(/assets\/by-id/gi, "dashboard/thumbnail.png", {
contentType: "image/png",
});
} }
async setupNewProject() { async setupNewProject() {
@ -207,60 +211,64 @@ export class DashboardPage extends BaseWebSocketPage {
async goToDashboard() { async goToDashboard() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.anyTeamId}/projects`, `#/dashboard/recent?team-id=${DashboardPage.anyTeamId}`,
); );
await expect(this.mainHeading).toBeVisible(); await expect(this.mainHeading).toBeVisible();
} }
async goToSecondTeamDashboard() { async goToSecondTeamDashboard() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/projects`, `#/dashboard/recent?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSecondTeamMembersSection() { async goToSecondTeamMembersSection() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/members`, `#/dashboard/members?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSecondTeamInvitationsSection() { async goToSecondTeamInvitationsSection() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/invitations`, `#/dashboard/invitations?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSecondTeamWebhooksSection() { async goToSecondTeamWebhooksSection() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`, `#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSecondTeamWebhooksSection() { async goToSecondTeamWebhooksSection() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`, `#/dashboard/webhooks?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSecondTeamSettingsSection() { async goToSecondTeamSettingsSection() {
await this.page.goto( await this.page.goto(
`#/dashboard/team/${DashboardPage.secondTeamId}/settings`, `#/dashboard/settings?team-id=${DashboardPage.secondTeamId}`,
); );
} }
async goToSearch() { async goToSearch() {
await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/search`); await this.page.goto(
`#/dashboard/search?team-id=${DashboardPage.anyTeamId}`,
);
} }
async goToDrafts() { async goToDrafts() {
await this.page.goto( 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"); await expect(this.mainHeading).toHaveText("Drafts");
} }
async goToFonts() { 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"); await expect(this.mainHeading).toHaveText("Fonts");
} }

View file

@ -85,7 +85,7 @@ export class ViewerPage extends BaseWebSocketPage {
pageId = ViewerPage.anyPageId, pageId = ViewerPage.anyPageId,
} = {}) { } = {}) {
await this.page.goto( 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(); this.#ws = await this.waitForNotificationsWebSocket();

View file

@ -36,6 +36,14 @@ export class WorkspacePage extends BaseWebSocketPage {
"get-team?id=*", "get-team?id=*",
"workspace/get-team-default.json", "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( await BaseWebSocketPage.mockRPC(
page, page,
"get-profiles-for-file-comments?file-id=*", "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 anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b";
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
@ -83,7 +92,7 @@ export class WorkspacePage extends BaseWebSocketPage {
pageId = WorkspacePage.anyPageId, pageId = WorkspacePage.anyPageId,
} = {}) { } = {}) {
await this.page.goto( 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(); this.#ws = await this.waitForNotificationsWebSocket();

View file

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

View file

@ -10,8 +10,9 @@
[app.common.logging :as log] [app.common.logging :as log]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.events :as ev] [app.main.data.auth :as da]
[app.main.data.users :as du] [app.main.data.event :as ev]
[app.main.data.profile :as dp]
[app.main.data.websocket :as ws] [app.main.data.websocket :as ws]
[app.main.errors] [app.main.errors]
[app.main.features :as feat] [app.main.features :as feat]
@ -66,14 +67,14 @@
authenticated user; proceed to fetch teams." authenticated user; proceed to fetch teams."
[stream] [stream]
(rx/merge (rx/merge
(rx/of (du/fetch-profile)) (rx/of (dp/fetch-profile))
(->> stream (->> stream
(rx/filter (ptk/type? ::profile-fetched)) (rx/filter dp/profile-fetched?)
(rx/take 1) (rx/take 1)
(rx/map deref) (rx/map deref)
(rx/mapcat (fn [profile] (rx/mapcat (fn [profile]
(if (du/is-authenticated? profile) (if (dp/is-authenticated? profile)
(rx/of (du/fetch-teams)) (rx/of (dp/initialize-profile profile))
(rx/empty)))) (rx/empty))))
(rx/observe-on :async)))) (rx/observe-on :async))))
@ -92,19 +93,24 @@
(initialize-profile stream) (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 ;; Once profile is fetched, initialize all penpot application
;; routes ;; routes
(->> stream (->> stream
(rx/filter du/profile-fetched?) (rx/filter dp/profile-fetched?)
(rx/take 1) (rx/take 1)
(rx/map #(rt/init-routes))) (rx/map #(rt/init-routes)))
;; Once profile fetched and the current user is authenticated, ;; Once profile fetched and the current user is authenticated,
;; proceed to initialize the websockets connection. ;; proceed to initialize the websockets connection.
(->> stream (->> stream
(rx/filter du/profile-fetched?) (rx/filter dp/profile-fetched?)
(rx/map deref) (rx/map deref)
(rx/filter du/is-authenticated?) (rx/filter dp/is-authenticated?)
(rx/take 1) (rx/take 1)
(rx/map #(ws/initialize))))))) (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.schema :as sm]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid] [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.data.workspace.state-helpers :as wsh]
[app.main.repo :as rp] [app.main.repo :as rp]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@ -603,3 +603,18 @@
(filter (fn [comment] (some #(= % (:frame-id comment)) frame-ids?))) (filter (fn [comment] (some #(= % (:frame-id comment)) frame-ids?)))
(map update-comment-thread-frame) (map update-comment-thread-frame)
(rx/from)))))) (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.common.types.team :as ctt]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps]
[app.main.features :as features] [app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.util.dom :as-alias dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(declare go-to-dashboard-recent)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK ;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -233,12 +237,165 @@
(watch [_ state _] (watch [_ state _]
(when (= :removed change) (when (= :removed change)
(let [message (tr "dashboard.removed-from-team" team-name) (let [message (tr "dashboard.removed-from-team" team-name)
profile (:profile state)] team-id (-> state :profile :default-team-id)]
(rx/concat (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)) (->> (rx/of (ntf/info message))
;; Delay so the navigation can finish ;; Delay so the navigation can finish
(rx/delay 250)))))))) (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.files.helpers :as cfh]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.main.data.common :as dcm]
[app.main.data.common :as dc] [app.main.data.event :as ev]
[app.main.data.events :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.websocket :as dws] [app.main.data.websocket :as dws]
[app.main.features :as features] [app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.sse :as sse] [app.util.sse :as sse]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[clojure.set :as set] [clojure.set :as set]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -43,143 +34,38 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects) (declare fetch-projects)
(declare fetch-team-members)
(declare process-message) (declare process-message)
(defn initialize (defn initialize
[{:keys [id]}] []
(dm/assert! (uuid? id))
(ptk/reify ::initialize (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 ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stopper (rx/filter (ptk/type? ::finalize) stream) (let [stopper (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)] profile-id (:profile-id state)]
(->> (rx/merge (->> (rx/merge
;; fetch teams must be first in case the team doesn't exist (rx/of (fetch-projects)
(ptk/watch (du/fetch-teams) state stream) (df/fetch-fonts))
(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)
(->> stream (->> stream
(rx/filter (ptk/type? ::dws/message)) (rx/filter (ptk/type? ::dws/message))
(rx/map deref) (rx/map deref)
(rx/filter (fn [{:keys [topic] :as msg}] (rx/filter (fn [{:keys [topic] :as msg}]
(or (= topic uuid/zero) (or (= topic uuid/zero)
(= topic profile-id)))) (= topic profile-id))))
(rx/map process-message)) (rx/map process-message)
(rx/ignore)))
;; 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/take-until stopper)))))) (rx/take-until stopper))))))
(defn finalize (defn finalize
[params] []
(ptk/data-event ::finalize params)) (ptk/data-event ::finalize {}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team) ;; 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 ;; --- EVENT: fetch-projects
(defn projects-fetched (defn projects-fetched
@ -187,8 +73,10 @@
(ptk/reify ::projects-fetched (ptk/reify ::projects-fetched
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [projects (d/index-by :id projects)] (reduce (fn [state {:keys [id] :as project}]
(assoc state :dashboard-projects projects))))) (update-in state [:projects id] merge project))
state
projects))))
(defn fetch-projects (defn fetch-projects
[] []
@ -201,82 +89,28 @@
;; --- EVENT: search ;; --- EVENT: search
(defn search-result-fetched (def ^:private schema:search-params
[result]
(ptk/reify ::search-result-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-search-result result))))
(def schema:search-params
[:map {:closed true} [:map {:closed true}
[:search-term [:maybe :string]]]) [:search-term [:maybe :string]]])
(def ^:private check-search-params
(sm/check-fn schema:search-params))
(defn search (defn search
[params] [params]
(dm/assert! schema:search-params params) (let [params (check-search-params params)]
(ptk/reify ::search (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
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (dissoc state :search-result))
(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)))))))
(defn fetch-files ptk/WatchEvent
[{:keys [project-id] :as params}] (watch [_ state _]
(dm/assert! (uuid? project-id)) (let [team-id (:current-team-id state)
(ptk/reify ::fetch-files params (assoc params :team-id team-id)]
ptk/WatchEvent (->> (rp/cmd! :search-files params)
(watch [_ _ _] (rx/map (fn [result]
(->> (rp/cmd! :get-project-files {:project-id project-id}) #(assoc % :search-result result)))))))))
(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)))))))
;; --- EVENT: recent-files ;; --- EVENT: recent-files
@ -287,8 +121,8 @@
(update [_ state] (update [_ state]
(let [files (d/index-by :id files)] (let [files (d/index-by :id files)]
(-> state (-> state
(assoc :dashboard-recent-files files) (assoc :recent-files files)
(update :dashboard-files d/merge files)))))) (update :files d/merge files))))))
(defn fetch-recent-files (defn fetch-recent-files
[] []
@ -325,27 +159,22 @@
(ptk/reify ::clear-file-select (ptk/reify ::clear-file-select
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :dashboard-local (-> state
assoc :selected-files #{} (dissoc :selected-files)
:selected-project nil (dissoc :selected-project)
:menu-open false (update :dashboard-local dissoc :menu-open :menu-pos)))))
:menu-pos nil))))
(defn toggle-file-select (defn toggle-file-select
[{:keys [id project-id] :as file}] [{:keys [id project-id] :as file}]
(ptk/reify ::toggle-file-select (ptk/reify ::toggle-file-select
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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) (if (or (nil? selected-project-id)
(= selected-project-id project-id)) (= selected-project-id project-id))
(update state :dashboard-local (-> state
(fn [local] (update :selected-files #(if (contains? % id) (disj % id) (conj % id)))
(-> local (assoc :selected-project project-id))
(update :selected-files #(if (contains? % id)
(disj % id)
(conj % id)))
(assoc :selected-project project-id))))
state))))) state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -399,320 +228,6 @@
;; Data Modification ;; 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 ;; --- EVENT: create-project
(defn- project-created (defn- project-created
@ -724,7 +239,7 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:dashboard-projects id] project) (assoc-in [:projects id] project)
(assoc-in [:dashboard-local :project-for-edit] id))))) (assoc-in [:dashboard-local :project-for-edit] id)))))
(defn create-project (defn create-project
@ -732,7 +247,7 @@
(ptk/reify ::create-project (ptk/reify ::create-project
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [projects (get state :dashboard-projects) (let [projects (get state :projects)
unames (cfh/get-used-names projects) unames (cfh/get-used-names projects)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")) name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
team-id (:current-team-id state) team-id (:current-team-id state)
@ -753,7 +268,7 @@
(ptk/reify ::project-duplicated (ptk/reify ::project-duplicated
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:dashboard-projects id] project)))) (assoc-in state [:projects id] project))))
(defn duplicate-project (defn duplicate-project
[{:keys [id name] :as params}] [{:keys [id name] :as params}]
@ -803,11 +318,11 @@
(ptk/reify ::toggle-project-pin (ptk/reify ::toggle-project-pin
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 ptk/WatchEvent
(watch [_ state _] (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])] params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/cmd! :update-project-pin params) (->> (rp/cmd! :update-project-pin params)
(rx/ignore)))))) (rx/ignore))))))
@ -820,7 +335,7 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(update-in [:dashboard-projects id :name] (constantly name)) (update-in [:projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit))) (update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent ptk/WatchEvent
@ -836,7 +351,7 @@
(ptk/reify ::delete-project (ptk/reify ::delete-project
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :dashboard-projects dissoc id)) (update state :projects dissoc id))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -850,7 +365,7 @@
(ptk/reify ::file-deleted (ptk/reify ::file-deleted
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:dashboard-projects project-id :count] dec)))) (update-in state [:projects project-id :count] dec))))
(defn delete-file (defn delete-file
[{:keys [id project-id] :as params}] [{:keys [id project-id] :as params}]
@ -858,9 +373,9 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(d/update-when :dashboard-files dissoc id) (d/update-when :files dissoc id)
(d/update-when :dashboard-shared-files dissoc id) (d/update-when :shared-files dissoc id)
(d/update-when :dashboard-recent-files dissoc id))) (d/update-when :recent-files dissoc id)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -882,9 +397,9 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(d/update-in-when [:dashboard-files id :name] (constantly name)) (d/update-in-when [:files id :name] (constantly name))
(d/update-in-when [:dashboard-shared-files id :name] (constantly name)) (d/update-in-when [:shared-files id :name] (constantly name))
(d/update-in-when [:dashboard-recent-files id :name] (constantly name)))) (d/update-in-when [:recent-files id :name] (constantly name))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -906,10 +421,10 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared)) (d/update-in-when [:files id :is-shared] (constantly is-shared))
(d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)) (d/update-in-when [:recent-files id :is-shared] (constantly is-shared))
(cond-> (not is-shared) (cond-> (not is-shared)
(d/update-when :dashboard-shared-files dissoc id)))) (d/update-when :shared-files dissoc id))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -928,8 +443,8 @@
(= file-id (:id %)) (= file-id (:id %))
(assoc :thumbnail-id thumbnail-id)))))] (assoc :thumbnail-id thumbnail-id)))))]
(-> state (-> state
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id) (d/update-in-when [: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 [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files)))))) (d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file ;; --- EVENT: create-file
@ -946,9 +461,9 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:dashboard-files id] file) (assoc-in [:files id] file)
(assoc-in [:dashboard-recent-files id] file) (assoc-in [:recent-files id] file)
(update-in [:dashboard-projects project-id :count] inc))))) (update-in [:projects project-id :count] inc)))))
(defn create-file (defn create-file
[{:keys [project-id name] :as params}] [{:keys [project-id name] :as params}]
@ -963,7 +478,7 @@
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params) on-error rx/throw}} (meta params)
files (get state :dashboard-files) files (get state :files)
unames (cfh/get-used-names files) unames (cfh/get-used-names files)
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
features (-> (features/get-team-enabled-features state) features (-> (features/get-team-enabled-features state)
@ -1015,14 +530,14 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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] update-project (fn [project delta op]
(-> project (-> project
(update :count #(op % (count ids))) (update :count #(op % (count ids)))
(assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))] (assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))]
(-> state (-> state
(d/update-in-when [:dashboard-projects origin-project] update-project 0 -) (d/update-in-when [:projects origin-project] update-project 0 -)
(d/update-in-when [:dashboard-projects project-id] update-project 10 +)))) (d/update-in-when [:projects project-id] update-project 10 +))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -1063,123 +578,6 @@
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (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 (defn create-element
[] []
(ptk/reify ::create-element (ptk/reify ::create-element
@ -1190,10 +588,10 @@
pparams (:path-params route) pparams (:path-params route)
in-project? (contains? pparams :project-id) in-project? (contains? pparams :project-id)
name (if in-project? name (if in-project?
(let [files (get state :dashboard-files) (let [files (get state :files)
unames (cfh/get-used-names files)] unames (cfh/get-used-names files)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) (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)] unames (cfh/get-used-names projects)]
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")))) (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
params (if in-project? params (if in-project?
@ -1203,7 +601,7 @@
:team-id team-id}) :team-id team-id})
action-name (if in-project? :create-file :create-project) action-name (if in-project? :create-file :create-project)
action (if in-project? file-created project-created) 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? (when can-edit?
(->> (rp/cmd! action-name params) (->> (rp/cmd! action-name params)
@ -1214,10 +612,9 @@
(ptk/reify ::open-selected-file (ptk/reify ::open-selected-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [files (get-in state [:dashboard-local :selected-files])] (let [[file-id :as files] (get state :selected-files)]
(if (= 1 (count files)) (if (= 1 (count files))
(let [file (get-in state [:dashboard-files (first files)])] (rx/of (dcm/go-to-workspace :file-id file-id))
(rx/of (go-to-workspace file)))
(rx/empty)))))) (rx/empty))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1229,13 +626,13 @@
(ptk/reify ::handle-change-team-role (ptk/reify ::handle-change-team-role
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dc/change-team-role params) (rx/of (dcm/change-team-role params)
(modal/hide))))) (modal/hide)))))
(defn- process-message (defn- process-message
[{:keys [type] :as msg}] [{:keys [type] :as msg}]
(case type (case type
:notification (dc/handle-notification msg) :notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role 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)) nil))

View file

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

View file

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

View file

@ -7,7 +7,7 @@
(ns app.main.data.exports.assets (ns app.main.data.exports.assets
(:require (:require
[app.common.uuid :as uuid] [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.modal :as modal]
[app.main.data.persistence :as dwp] [app.main.data.persistence :as dwp]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]

View file

@ -10,7 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [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.data.modal :as modal]
[app.main.features :as features] [app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]

View file

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

View file

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

View file

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

View file

@ -4,24 +4,21 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.data.users (ns app.main.data.profile
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [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.media :as di]
[app.main.data.notifications :as ntf] [app.main.data.team :as-alias dtm]
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.plugins.register :as register] [app.main.router :as rt]
[app.util.i18n :as i18n :refer [tr]] [app.plugins.register :as plugins.register]
[app.util.router :as rt] [app.util.i18n :as i18n]
[app.util.storage :as storage] [app.util.storage :as storage]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -40,7 +37,7 @@
[:lang {:optional true} :string] [:lang {:optional true} :string]
[:theme {:optional true} :string]]) [:theme {:optional true} :string]])
(def check-profile! (def check-profile
(sm/check-fn schema:profile)) (sm/check-fn schema:profile))
;; --- HELPERS ;; --- HELPERS
@ -49,98 +46,31 @@
[{:keys [id]}] [{:keys [id]}]
(and (uuid? id) (not= id uuid/zero))) (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 ;; --- EVENT: fetch-profile
(declare logout) (defn initialize-profile
"Initialize profile state, only logged-in profile data should be
(def profile-fetched? passed to this event"
(ptk/type? ::profile-fetched))
(defn profile-fetched
[{:keys [id] :as profile}] [{:keys [id] :as profile}]
(ptk/reify ::profile-fetched (ptk/reify ::initialize-profile
IDeref IDeref
(-deref [_] profile) (-deref [_] profile)
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(cond-> state (-> state
(is-authenticated? profile) (assoc :profile-id id)
(-> (assoc :profile-id id) (assoc :profile profile)))
(assoc :profile profile))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [profile (:profile state) (let [profile (:profile state)]
email (:email profile) (swap! storage/user assoc :profile profile)
previous-profile (:profile storage/user) (i18n/set-locale! (:lang profile))
previous-email (:email previous-profile)] (plugins.register/init)))))
(when profile
(swap! storage/user assoc :profile profile)
(i18n/set-locale! (:lang profile))
(when (not= previous-email email)
(set-current-team! nil))
(register/init)))))) (def profile-fetched?
(ptk/type? ::profile-fetched))
(defn- on-fetch-profile-exception (defn- on-fetch-profile-exception
[cause] [cause]
@ -160,213 +90,9 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/cmd! :get-profile) (->> (rp/cmd! :get-profile)
(rx/map profile-fetched) (rx/map (partial ptk/data-event ::profile-fetched))
(rx/catch on-fetch-profile-exception))))) (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 ;; --- Update Profile
(defn persist-profile (defn persist-profile
@ -386,7 +112,7 @@
[data] [data]
(dm/assert! (dm/assert!
"expected valid profile data" "expected valid profile data"
(check-profile! data)) (check-profile data))
(ptk/reify ::update-profile (ptk/reify ::update-profile
ptk/WatchEvent ptk/WatchEvent
@ -589,6 +315,9 @@
;; --- EVENT: request-account-deletion ;; --- EVENT: request-account-deletion
(def profile-deleted?
(ptk/type? ::profile-deleted))
(defn request-account-deletion (defn request-account-deletion
[params] [params]
(ptk/reify ::request-account-deletion (ptk/reify ::request-account-deletion
@ -599,7 +328,8 @@
on-success identity}} (meta params)] on-success identity}} (meta params)]
(->> (rp/cmd! :delete-profile {}) (->> (rp/cmd! :delete-profile {})
(rx/tap on-success) (rx/tap on-success)
(rx/map logged-out) (rx/map (fn [_]
(ptk/data-event ::profile-deleted params)))
(rx/catch on-error) (rx/catch on-error)
(rx/delay-at-least 300)))))) (rx/delay-at-least 300))))))
@ -652,16 +382,6 @@
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (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 ;; --- EVENT: fetch-team-webhooks
(defn access-tokens-fetched (defn access-tokens-fetched
@ -716,28 +436,3 @@
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (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.transit :as t]
[app.common.types.shape-tree :as ctt] [app.common.types.shape-tree :as ctt]
[app.common.types.shape.interactions :as ctsi] [app.common.types.shape.interactions :as ctsi]
[app.main.data.comments :as dcm] [app.common.uuid :as uuid]
[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.fonts :as df]
[app.main.features :as features] [app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt]
[app.util.globals :as ug] [app.util.globals :as ug]
[app.util.router :as rt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -32,7 +34,7 @@
{:zoom 1 {:zoom 1
:fullscreen? false :fullscreen? false
:interactions-mode :show-on-click :interactions-mode :show-on-click
:interactions-show? false :show-interactions false
:comments-mode :all :comments-mode :all
:comments-show :unresolved :comments-show :unresolved
:selected #{} :selected #{}
@ -55,7 +57,7 @@
[:page-id {:optional true} ::sm/uuid]]) [:page-id {:optional true} ::sm/uuid]])
(defn initialize (defn initialize
[{:keys [file-id share-id interactions-show?] :as params}] [{:keys [file-id share-id] :as params}]
(dm/assert! (dm/assert!
"expected valid params" "expected valid params"
(sm/check schema:initialize params)) (sm/check schema:initialize params))
@ -65,13 +67,13 @@
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
(assoc :current-share-id share-id)
(update :viewer-local (update :viewer-local
(fn [lstate] (fn [lstate]
(if (nil? lstate) (if (nil? lstate)
default-local-state default-local-state
lstate))) lstate)))
(assoc-in [:viewer-local :share-id] share-id) (assoc-in [:viewer-local :share-id] share-id)))
(assoc-in [:viewer-local :interactions-show?] interactions-show?)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -256,12 +258,9 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [zoom-type (get-in state [:viewer-local :zoom-type]) (let [zoom-type (get-in state [:viewer-local :zoom-type])
route (:route state) params (rt/get-params state)]
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type))))))) (rx/of (rt/nav :viewer (assoc params :zoom zoom-type)))))))
(def increase-zoom (def increase-zoom
(ptk/reify ::increase-zoom (ptk/reify ::increase-zoom
@ -293,14 +292,17 @@
(ptk/reify ::zoom-to-fit (ptk/reify ::zoom-to-fit
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [srect (as-> (get-in state [:route :query-params :page-id]) % (let [params (rt/get-params state)
(get-in state [:viewer :pages % :frames]) page-id (some-> (:page-id params) uuid/parse)
(nth % (get-in state [:route :query-params :index])) index (some-> (:index params) parse-long)
(get % :selrect))
orig-size (get-in state [:viewer-local :viewport-size]) frames (dm/get-in state [:viewer :pages page-id :frames])
wdiff (/ (:width orig-size) (:width srect)) srect (-> (nth frames index)
hdiff (/ (:height orig-size) (:height srect)) (get :selrect))
minzoom (min wdiff hdiff)] osize (dm/get-in state [:viewer-local :viewport-size])
wdiff (/ (:width osize) (:width srect))
hdiff (/ (:height osize) (:height srect))
minzoom (min wdiff hdiff)]
(-> state (-> state
(assoc-in [:viewer-local :zoom] minzoom) (assoc-in [:viewer-local :zoom] minzoom)
(assoc-in [:viewer-local :zoom-type] :fit)))) (assoc-in [:viewer-local :zoom-type] :fit))))
@ -312,17 +314,25 @@
(ptk/reify ::zoom-to-fill (ptk/reify ::zoom-to-fill
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [srect (as-> (get-in state [:route :query-params :page-id]) %
(get-in state [:viewer :pages % :frames]) (let [params (rt/get-params state)
(nth % (get-in state [:route :query-params :index])) page-id (some-> (:page-id params) uuid/parse)
(get % :selrect)) index (some-> (:index params) parse-long)
orig-size (get-in state [:viewer-local :viewport-size])
wdiff (/ (:width orig-size) (:width srect)) frames (dm/get-in state [:viewer :pages page-id :frames])
hdiff (/ (:height orig-size) (:height srect)) srect (-> (nth frames index)
maxzoom (max wdiff hdiff)] (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 (-> state
(assoc-in [:viewer-local :zoom] maxzoom) (assoc-in [:viewer-local :zoom] maxzoom)
(assoc-in [:viewer-local :zoom-type] :fill)))) (assoc-in [:viewer-local :zoom-type] :fill))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring)))) (watch [_ _ _] (rx/of update-zoom-querystring))))
@ -376,16 +386,15 @@
(-> state (-> state
(dissoc :viewer-animations) (dissoc :viewer-animations)
(assoc :viewer-overlays []))) (assoc :viewer-overlays [])))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)
qparams (:query-params route) index (some-> params :index parse-long)]
pparams (:path-params route)
index (:index qparams)]
(when (pos? index) (when (pos? index)
(rx/of (rx/of
(dcm/close-thread) (dcmt/close-thread)
(rt/nav :viewer pparams (assoc qparams :index (dec index))))))))) (rt/nav :viewer (assoc params :index (dec index)))))))))
(def select-next-frame (def select-next-frame
(ptk/reify ::select-next-frame (ptk/reify ::select-next-frame
@ -396,30 +405,25 @@
(assoc :viewer-overlays []))) (assoc :viewer-overlays [])))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)
pparams (:path-params route) index (some-> params :index parse-long)
qparams (:query-params route) page-id (some-> params :page-id parse-uuid)
page-id (:page-id qparams)
index (:index qparams)
total (count (get-in state [:viewer :pages page-id :frames]))] total (count (get-in state [:viewer :pages page-id :frames]))]
(when (< index (dec total)) (when (< index (dec total))
(rx/of (rx/of
(dcm/close-thread) (dcmt/close-thread)
(rt/nav :viewer pparams (assoc qparams :index (inc index))))))))) (rt/nav :viewer params (assoc params :index (inc index)))))))))
(def select-first-frame (def select-first-frame
(ptk/reify ::select-first-frame (ptk/reify ::select-first-frame
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)]
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rx/of
(dcm/close-thread) (dcmt/close-thread)
(rt/nav :viewer pparams (assoc qparams :index 0))))))) (rt/nav :viewer (assoc params :index 0)))))))
(def valid-interaction-modes (def valid-interaction-modes
#{:hide :show :show-on-click}) #{:hide :show :show-on-click})
@ -434,17 +438,14 @@
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:viewer-local :interactions-mode] mode) (assoc-in [:viewer-local :interactions-mode] mode)
(assoc-in [:viewer-local :interactions-show?] (case mode (assoc-in [:viewer-local :show-interactions] (case mode
:hide false :hide false
:show true :show true
:show-on-click false)))) :show-on-click false))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)]
screen (-> route :data :name keyword) (rx/of (rt/nav :viewer (assoc params :interactions-mode mode)))))))
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :interactions-mode mode)))))))
(declare flash-done) (declare flash-done)
@ -453,7 +454,7 @@
(ptk/reify ::flash-interactions (ptk/reify ::flash-interactions
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :interactions-show?] true)) (assoc-in state [:viewer-local :show-interactions] true))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
@ -466,7 +467,7 @@
(ptk/reify ::flash-done (ptk/reify ::flash-done
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :interactions-show?] false)))) (assoc-in state [:viewer-local :show-interactions] false))))
(defn set-nav-scroll (defn set-nav-scroll
[scroll] [scroll]
@ -500,11 +501,8 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)]
screen (-> route :data :name keyword) (rx/of (rt/nav :viewer (assoc params :index index)))))))
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :index index)))))))
(defn go-to-frame (defn go-to-frame
([frame-id] ([frame-id]
@ -573,10 +571,8 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)]
pparams (:path-params route) (rx/of (rt/nav :viewer (assoc params :section section)))))))
qparams (:query-params route)]
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
;; --- Overlays ;; --- Overlays
@ -771,9 +767,8 @@
(ptk/reify ::go-to-dashboard (ptk/reify ::go-to-dashboard
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (get-in state [:viewer :project :team-id]) (let [team-id (get-in state [:viewer :project :team-id])]
params {:team-id team-id}] (rx/of (dcm/go-to-dashboard-recent :team-id team-id))))))
(rx/of (rt/nav :dashboard-projects params))))))
(defn go-to-page (defn go-to-page
[page-id] [page-id]
@ -784,13 +779,10 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (-> (rt/get-params state)
pparams (:path-params route) (assoc :index 0)
qparams (-> (:query-params route) (assoc :page-id page-id))]
(assoc :index 0) (rx/of (rt/nav :viewer params))))))
(assoc :page-id page-id))
rname (get-in route [:data :name])]
(rx/of (rt/nav rname pparams qparams))))))
(defn go-to-workspace (defn go-to-workspace
([] (go-to-workspace nil)) ([] (go-to-workspace nil))
@ -798,14 +790,16 @@
(ptk/reify ::go-to-workspace (ptk/reify ::go-to-workspace
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [params (rt/get-params state)
project-id (get-in state [:viewer :project :id]) file-id (get-in state [:viewer :file :id])
file-id (get-in state [:viewer :file :id]) team-id (get-in state [:viewer :project :team-id])
saved-page-id (get-in route [:query-params :page-id]) page-id (or page-id
pparams {:project-id project-id :file-id file-id} (some-> (:page-id params) uuid/parse))
qparams {:page-id (or page-id saved-page-id)}] params {:page-id page-id
(rx/of (rt/nav-new-window* :file-id file-id
{:rname :workspace :team-id team-id}
:path-params pparams name (dm/str "workspace-" file-id)]
:query-params qparams
:name (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 (ns app.main.data.viewer.shortcuts
(:require (:require
[app.main.data.common :as dcm]
[app.main.data.shortcuts :as ds] [app.main.data.shortcuts :as ds]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st])) [app.main.store :as st]))
@ -69,7 +70,7 @@
:open-workspace {:tooltip "G W" :open-workspace {:tooltip "G W"
:command "g w" :command "g w"
:subsections [:navigation-viewer] :subsections [:navigation-viewer]
:fn #(st/emit! (dv/go-to-workspace))}}) :fn #(st/emit! (dcm/go-to-workspace))}})
(defn get-tooltip [shortcut] (defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut)) (assert (contains? shortcuts shortcut) (str shortcut))

View file

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

View file

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

View file

@ -12,7 +12,7 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.main.data.changes :as dwc] [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] [app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))

View file

@ -17,7 +17,7 @@
[app.common.types.shape.interactions :as ctsi] [app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.changes :as dch] [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.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]

View file

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

View file

@ -27,7 +27,8 @@
[app.config :as cf] [app.config :as cf]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
[app.main.data.comments :as dc] [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.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.workspace :as-alias dw] [app.main.data.workspace :as-alias dw]
@ -40,14 +41,15 @@
[app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.zoom :as dwz]
[app.main.features :as features] [app.main.features :as features]
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.util.color :as uc] [app.util.color :as uc]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as storage] [app.util.storage :as storage]
[app.util.time :as dt] [app.util.time :as dt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@ -684,21 +686,49 @@
(rx/of (when can-detach? (rx/of (when can-detach?
(dch/commit-changes changes))))))) (dch/commit-changes changes)))))))
(defn nav-to-component-file (defn go-to-component-file
[file-id component] [file-id component]
(dm/assert! (uuid? file-id)) (dm/assert! (uuid? file-id))
(dm/assert! (some? component)) (dm/assert! (some? component))
(ptk/reify ::nav-to-component-file (ptk/reify ::nav-to-component-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [project-id (get-in state [:workspace-libraries file-id :project-id]) (let [params (-> (rt/get-params state)
path-params {:project-id project-id (assoc :file-id file-id)
:file-id file-id} (assoc :page-id (:main-instance-page component))
query-params {:page-id (:main-instance-page component) (assoc :component-id (:id component)))]
:component-id (:id component)}] (rx/of (rt/nav :workspace params :new-window? true))))))
(rx/of (rt/nav-new-window* {:rname :workspace
:path-params path-params
:query-params query-params})))))) (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 (defn library-thumbnails-fetched
[thumbnails] [thumbnails]
@ -1117,9 +1147,11 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(rp/cmd! :ignore-file-library-sync-status (let [file-id (:current-file-id state)]
{:file-id (get-in state [:workspace-file :id]) (->> (rp/cmd! :ignore-file-library-sync-status
:date (dt/now)})))) {:file-id file-id
:date (dt/now)})
(rx/ignore))))))
(defn assets-need-sync (defn assets-need-sync
"Get a lazy sequence of all the assets of each type in the library that have "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) (->> (rp/cmd! :set-file-shared params)
(rx/ignore)))))) (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 ;; --- Link and unlink Files
(defn link-file-to-library (defn link-file-to-library

View file

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

View file

@ -18,7 +18,7 @@
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.changes :as dch] [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.modal :as md]
[app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.data.workspace.specialized-panel :as-alias dwsp]

View file

@ -21,7 +21,7 @@
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.changes :as dch] [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.colors :as cl]
[app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.grid-layout.editor :as dwge]
[app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.modifiers :as dwm]
@ -110,13 +110,15 @@
:undo-group undo-group}))) :undo-group undo-group})))
(rx/empty)))))) (rx/empty))))))
(defn initialize (defn initialize-shape-layout
[] []
(ptk/reify ::initialize (ptk/reify ::initialize-shape-layout
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(let [stopper (rx/filter (ptk/type? ::finalize) stream)] (let [stopper (rx/filter (ptk/type? ::finalize-shape-layout) stream)]
(->> 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/filter (ptk/type? :layout/update))
(rx/map deref) (rx/map deref)
;; We buffer the updates to the layout so if there are many changes at the same time ;; 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})))) (update-layout-positions {:ids ids}))))
(rx/take-until stopper)))))) (rx/take-until stopper))))))
(defn finalize (defn finalize-shape-layout
[] []
(ptk/reify ::finalize)) (ptk/data-event ::finalize-shape-layout))
(defn create-layout-from-id (defn create-layout-from-id
[id type & {:keys [from-frame? calculate-params?] :or {from-frame? false calculate-params? true}}] [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.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
[app.main.data.comments :as dc] [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.edition :as dwe]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]

View file

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

View file

@ -27,10 +27,6 @@
(-> (lookup-page state page-id) (-> (lookup-page state page-id)
(get :objects)))) (get :objects))))
(defn lookup-viewer-objects
([state page-id]
(dm/get-in state [:viewer :pages page-id :objects])))
(defn lookup-library-objects (defn lookup-library-objects
[state file-id page-id] [state file-id page-id]
(dm/get-in state [:workspace-libraries file-id :data :pages-index page-id :objects])) (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.text :as txt]
[app.common.types.modifiers :as ctm] [app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid] [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.common :as dwc]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.modifiers :as dwm]
@ -27,7 +27,7 @@
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.features :as features] [app.main.features :as features]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.util.router :as rt] [app.main.router :as rt]
[app.util.text-editor :as ted] [app.util.text-editor :as ted]
[app.util.text.content.styles :as styles] [app.util.text.content.styles :as styles]
[app.util.timers :as ts] [app.util.timers :as ts]

View file

@ -13,8 +13,8 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
[app.main.data.common :as dcm]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.util.router :as rt]
[app.util.time :as dt] [app.util.time :as dt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -290,14 +290,8 @@
(ptk/reify ::assure-valid-current-page (ptk/reify ::assure-valid-current-page
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [current_page (:current-page-id state) (let [page-id (:current-page-id state)
pages (get-in state [:workspace-data :pages]) pages (dm/get-in state [:workspace-data :pages])]
exists? (some #(= current_page %) pages) (if (contains? pages page-id)
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?
(rx/empty) (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 (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [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.persistence :as dwp]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -25,7 +26,7 @@
(declare fetch-versions) (declare fetch-versions)
(defn init-version-state (defn init-version-state
[file-id] []
(ptk/reify ::init-version-state (ptk/reify ::init-version-state
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -33,7 +34,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (fetch-versions file-id))))) (rx/of (fetch-versions)))))
(defn update-version-state (defn update-version-state
[version-state] [version-state]
@ -43,123 +44,90 @@
(update state :workspace-versions merge version-state)))) (update state :workspace-versions merge version-state))))
(defn fetch-versions (defn fetch-versions
[file-id] []
(dm/assert! (uuid? file-id))
(ptk/reify ::fetch-versions (ptk/reify ::fetch-versions
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id}) (let [file-id (:current-file-id state)]
(rx/map #(update-version-state {:status :loaded :data %})))))) (->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/map #(update-version-state {:status :loaded :data %})))))))
(defn create-version (defn create-version
[file-id] []
(dm/assert! (uuid? file-id))
(ptk/reify ::create-version (ptk/reify ::create-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(let [label (dt/format (dt/now) :date-full)] (let [label (dt/format (dt/now) :date-full)
file-id (:current-file-id state)]
;; Force persist before creating snapshot, otherwise we could loss changes ;; Force persist before creating snapshot, otherwise we could loss changes
(rx/concat (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/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %))) (rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label})) (rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
(rx/mapcat (rx/mapcat
(fn [{:keys [id]}] (fn [{:keys [id]}]
(rx/of (rx/of (update-version-state {:editing id})
(update-version-state {:editing id}) (fetch-versions))))))))))
(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)))))))
(defn rename-version (defn rename-version
[file-id id label] [id label]
(dm/assert! (uuid? file-id)) (assert (uuid? id) "expected valid uuid for `id`")
(dm/assert! (uuid? id)) (assert (sm/valid-text? label) "expected not empty string for `label`")
(dm/assert! (and (string? label) (d/not-empty? label)))
(ptk/reify ::rename-version (ptk/reify ::rename-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(rx/merge (let [file-id (:current-file-id state)]
(rx/of (update-version-state {:editing false})) (rx/merge
(->> (rp/cmd! :update-file-snapshot {:id id :label label}) (rx/of (update-version-state {:editing false})
(rx/map #(fetch-versions file-id))) (ptk/event ::ev/event {::ev/name "rename-version"
(rx/of (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 (defn restore-version
[project-id file-id id origin] [id origin]
(dm/assert! (uuid? project-id)) (assert (uuid? id) "expected valid uuid for `id`")
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(ptk/reify ::restore-version (ptk/reify ::restore-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(rx/concat (let [file-id (:current-file-id state)]
(rx/of ::dwp/force-persist) (rx/concat
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) (rx/of ::dwp/force-persist)
(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"}))
:snapshot ;; FIXME: we should abstract this
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"})) (->> (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 (when-let [name (case origin
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})) :version "restore-pin-version"
:snapshot "restore-autosave"
(rx/empty)))))) :plugin "restore-version-plugin"
nil)]
(rx/of (ptk/event ::ev/event {::ev/name name}))))))))
(defn delete-version (defn delete-version
[file-id id] [id]
(dm/assert! (uuid? file-id)) (assert (uuid? id) "expected valid uuid for `id`")
(dm/assert! (uuid? id))
(ptk/reify ::delete-version (ptk/reify ::delete-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/cmd! :delete-file-snapshot {:id id}) (->> (rp/cmd! :delete-file-snapshot {:id id})
(rx/map #(fetch-versions file-id)))))) (rx/map fetch-versions)))))
(defn pin-version (defn pin-version
[file-id id] [id]
(dm/assert! (uuid? file-id)) (assert (uuid? id) "expected valid uuid for `id`")
(dm/assert! (uuid? id))
(ptk/reify ::pin-version (ptk/reify ::pin-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -168,8 +136,82 @@
params {:id id params {:id id
:label (dt/format (:created-at version) :date-full)}] :label (dt/format (:created-at version) :date-full)}]
(rx/concat (->> (rp/cmd! :update-file-snapshot params)
(->> (rp/cmd! :update-file-snapshot params) (rx/mapcat (fn [_]
(rx/mapcat #(rx/of (update-version-state {:editing id}) (rx/of (update-version-state {:editing id})
(fetch-versions file-id)))) (fetch-versions)
(rx/of (ptk/event ::ev/event {::ev/name "pin-version"}))))))) (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.exceptions :as ex]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.auth :as da]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.main.store :as st]
[app.util.globals :as glob] [app.util.globals :as glob]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts] [app.util.timers :as ts]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -116,7 +117,7 @@
(if show-oops? (if show-oops?
(st/async-emit! (rt/assign-exception e)) (st/async-emit! (rt/assign-exception e))
(do (do
(st/emit! (du/logout {:capture-redirect true})) (st/emit! (da/logout))
(ts/schedule 500 #(st/emit! (ntf/warn msg))))))) (ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
;; Error that happens on an active business model validation does not ;; Error that happens on an active business model validation does not
@ -141,7 +142,7 @@
:timeout 3000}))) :timeout 3000})))
(= code :vern-conflict) (= code :vern-conflict)
(st/emit! (reload-file)) (st/emit! (ptk/event ::dw/reload-current-file))
:else :else
(st/async-emit! (rt/assign-exception error)))) (st/async-emit! (rt/assign-exception error))))
@ -212,7 +213,6 @@
(ts/schedule (ts/schedule
#(st/emit! (rt/assign-exception error)))) #(st/emit! (rt/assign-exception error))))
(defn- redirect-to-dashboard (defn- redirect-to-dashboard
[] []
(let [team-id (:current-team-id @st/state) (let [team-id (:current-team-id @st/state)

View file

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

View file

@ -11,7 +11,7 @@
[app.common.transit :as t] [app.common.transit :as t]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [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.http :as http]
[app.util.sse :as sse] [app.util.sse :as sse]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]

View file

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

View file

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

View file

@ -6,14 +6,20 @@
(ns app.main.ui (ns app.main.ui
(:require (:require
[app.common.data :as d]
[app.config :as cf] [app.config :as cf]
[app.main.data.common :as dcm]
[app.main.data.team :as dtm]
[app.main.refs :as refs] [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.context :as ctx]
[app.main.ui.debug.icons-preview :refer [icons-preview]] [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.error-boundary :refer [error-boundary*]]
[app.main.ui.exports.files] [app.main.ui.exports.files]
[app.main.ui.frame-preview :as frame-preview] [app.main.ui.frame-preview :as frame-preview]
[app.main.ui.icons :as i]
[app.main.ui.notifications :as notifications] [app.main.ui.notifications :as notifications]
[app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]] [app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]]
[app.main.ui.onboarding.questions :refer [questions-modal]] [app.main.ui.onboarding.questions :refer [questions-modal]]
@ -22,6 +28,8 @@
[app.main.ui.static :as static] [app.main.ui.static :as static]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def auth-page (def auth-page
@ -31,23 +39,117 @@
(mf/lazy-component app.main.ui.auth.verify-token/verify-token)) (mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def viewer-page (def viewer-page
(mf/lazy-component app.main.ui.viewer/viewer)) (mf/lazy-component app.main.ui.viewer/viewer*))
(def dashboard-page (def dashboard-page
(mf/lazy-component app.main.ui.dashboard/dashboard)) (mf/lazy-component app.main.ui.dashboard/dashboard*))
(def settings-page (def settings-page
(mf/lazy-component app.main.ui.settings/settings)) (mf/lazy-component app.main.ui.settings/settings))
(def workspace-page (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/props :obj
::mf/private true} ::mf/private true}
[{:keys [route profile]}] [{:keys [route profile]}]
(let [{:keys [data params]} route (let [{:keys [data params]} route
props (get profile :props) props (get profile :props)
section (get data :name)
show-question-modal? show-question-modal?
(and (contains? cf/flags :onboarding) (and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props)) (not (:onboarding-viewed props))
@ -72,7 +174,7 @@
(not= "0.0" (:main cf/version)))] (not= "0.0" (:main cf/version)))]
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
(case (:name data) (case section
(:auth-login (:auth-login
:auth-register :auth-register
:auth-register-validate :auth-register-validate
@ -96,66 +198,53 @@
[:& icons-preview]) [:& icons-preview])
(:dashboard-search (:dashboard-search
:dashboard-projects :dashboard-recent
:dashboard-files :dashboard-files
:dashboard-libraries :dashboard-libraries
:dashboard-fonts :dashboard-fonts
:dashboard-font-providers :dashboard-font-providers
:dashboard-team-members :dashboard-members
:dashboard-team-invitations :dashboard-invitations
:dashboard-team-webhooks :dashboard-webhooks
:dashboard-team-settings) :dashboard-settings)
[:? (let [params (get params :query)
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}] team-id (some-> params :team-id uuid)
#_[:& app.main.ui.onboarding/onboarding-templates-modal] project-id (some-> params :project-id uuid)
#_[:& app.main.ui.onboarding/onboarding-modal] search-term (some-> params :search-term)
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal] 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 (cond
show-question-modal? show-question-modal?
[:& questions-modal] [:& questions-modal]
show-newsletter-modal? show-newsletter-modal?
[:& onboarding-newsletter] [:& onboarding-newsletter]
show-team-modal? show-team-modal?
[:& onboarding-team-modal {:go-to-team? true}] [:& onboarding-team-modal {:go-to-team? true}]
show-release-modal? show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}]) [:& release-notes-modal {:version (:main cf/version)}])
[:& dashboard-page {:route route :profile profile}]] [:> team-container* {:team-id team-id}
:viewer [:> dashboard-page {:profile profile
(let [{:keys [query-params path-params]} route :section section
{:keys [index share-id section page-id interactions-mode frame-id share] :team-id team-id
:or {section :interactions interactions-mode :show-on-click}} query-params :search-term search-term
{:keys [file-id]} path-params] :plugin-url plugin-url
[:? {} :project-id project-id}]]])
(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}])])
:workspace :workspace
(let [project-id (some-> params :path :project-id uuid) (let [params (get params :query)
file-id (some-> params :path :file-id uuid) team-id (some-> params :team-id uuid)
page-id (some-> params :query :page-id uuid) file-id (some-> params :file-id uuid)
layout (some-> params :query :layout keyword)] page-id (some-> params :page-id uuid)
layout (some-> params :layout keyword)]
[:? {} [:? {}
(when (cf/external-feature-flag "onboarding-03" "test") (when (cf/external-feature-flag "onboarding-03" "test")
(cond (cond
@ -171,11 +260,87 @@
show-release-modal? show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])) [:& release-notes-modal {:version (:main cf/version)}]))
[:& workspace-page {:project-id project-id [:> team-container* {:team-id team-id}
:file-id file-id [:> workspace-page {:team-id team-id
:page-id page-id :file-id file-id
:layout-name layout :page-id page-id
:key file-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/frame-preview] [:& frame-preview/frame-preview]
@ -199,4 +364,4 @@
[:> error-boundary* {:fallback static/internal-error*} [:> error-boundary* {:fallback static/internal-error*}
[:& notifications/current-notification] [:& notifications/current-notification]
(when route (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-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm] [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.store :as st]
[app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery :refer [recovery-page]]
@ -19,7 +19,6 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc auth (mf/defc auth
{::mf/props :obj} {::mf/props :obj}
[{:keys [route]}] [{:keys [route]}]
@ -35,7 +34,7 @@
(mf/with-effect [error] (mf/with-effect [error]
(when error (when error
(st/emit! (du/show-redirect-error error)))) (st/emit! (da/show-redirect-error error))))
[:main {:class (stl/css :auth-section)} [:main {:class (stl/css :auth-section)}
(when show-login-icon (when show-login-icon

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm] [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.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.timers :as tm] [app.util.timers :as tm]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.main.data.dashboard :as dd] [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.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.errors :as errors] [app.main.errors :as errors]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
(ns app.main.ui.releases (ns app.main.ui.releases
(:require (:require
[app.main.data.modal :as modal] [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.store :as st]
[app.main.ui.releases.common :as rc] [app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10] [app.main.ui.releases.v1-10]

View file

@ -7,34 +7,17 @@
(ns app.main.ui.routes (ns app.main.ui.routes
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.spec :as us]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.users :as du] [app.main.data.team :as dtm]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.util.router :as rt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.v2.core :as ptk])) [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 (def routes
[["/auth" [["/auth"
["/login" :auth-login] ["/login" :auth-login]
@ -53,11 +36,10 @@
["/access-tokens" :settings-access-tokens]] ["/access-tokens" :settings-access-tokens]]
["/frame-preview" :frame-preview] ["/frame-preview" :frame-preview]
["/view/:file-id"
{:name :viewer ["/view" :viewer]
:conform
{:path-params ::viewer-path-params ["/view/:file-id" :viewer-legacy]
:query-params ::viewer-query-params}}]
(when *assert* (when *assert*
["/debug/icons-preview" :debug-icons-preview]) ["/debug/icons-preview" :debug-icons-preview])
@ -65,33 +47,32 @@
;; Used for export ;; Used for export
["/render-sprite/:file-id" :render-sprite] ["/render-sprite/:file-id" :render-sprite]
["/dashboard/team/:team-id" ["/dashboard"
["/members" :dashboard-team-members] ["/members" :dashboard-members]
["/invitations" :dashboard-team-invitations] ["/invitations" :dashboard-invitations]
["/webhooks" :dashboard-team-webhooks] ["/webhooks" :dashboard-webhooks]
["/settings" :dashboard-team-settings] ["/settings" :dashboard-settings]
["/projects" :dashboard-projects] ["/recent" :dashboard-recent]
["/search" :dashboard-search] ["/search" :dashboard-search]
["/fonts" :dashboard-fonts] ["/fonts" :dashboard-fonts]
["/fonts/providers" :dashboard-font-providers] ["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries] ["/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 ["/workspace" :workspace]
[router path] ["/workspace/:project-id/:file-id" :workspace-legacy]])
(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)))
(defn on-navigate (defn on-navigate
[router path] [router path]
@ -99,8 +80,9 @@
[base-path qs] (str/split path "?") [base-path qs] (str/split path "?")
location-path (dm/str (.-origin location) (.-pathname location)) location-path (dm/str (.-origin location) (.-pathname location))
valid-location? (= location-path (dm/str cf/public-uri)) 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 "/"))] empty-path? (or (= base-path "") (= base-path "/"))]
(cond (cond
(not valid-location?) (not valid-location?)
(st/emit! (rt/assign-exception {:type :not-found})) (st/emit! (rt/assign-exception {:type :not-found}))
@ -119,7 +101,11 @@
(st/emit! (rt/nav :auth-login)) (st/emit! (rt/nav :auth-login))
empty-path? 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 :else
(st/emit! (rt/assign-exception {:type :not-found}))))))))) (st/emit! (rt/assign-exception {:type :not-found})))))))))

View file

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

View file

@ -10,7 +10,7 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]] [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]

View file

@ -10,7 +10,7 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]

View file

@ -9,7 +9,7 @@
(:require (:require
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]] [app.main.ui.notifications.context-notification :refer [context-notification]]

View file

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

View file

@ -9,7 +9,7 @@
(:require (:require
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.notifications :as ntf] [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.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.util.dom :as dom] [app.util.dom :as dom]

View file

@ -11,7 +11,7 @@
[app.config :as cf] [app.config :as cf]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.file-uploader :refer [file-uploader]]

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.data.comments :as dcm] [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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.comments :as cmt] [app.main.ui.comments :as cmt]
@ -220,7 +220,7 @@
{:thread thread {:thread thread
:position-modifier modifier1 :position-modifier modifier1
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)} :viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
:users users :profiles users
:zoom zoom}]) :zoom zoom}])
(when-let [draft (:draft local)] (when-let [draft (:draft local)]
@ -231,10 +231,11 @@
:on-submit on-draft-submit :on-submit on-draft-submit
:zoom zoom}])]]])) :zoom zoom}])]]]))
(mf/defc comments-sidebar (mf/defc comments-sidebar*
[{:keys [users frame page]}] {::mf/props :obj}
[{:keys [profiles frame page]}]
(let [profile (mf/deref refs/profile) (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-map (mf/deref refs/comment-threads)
threads (->> (vals threads-map) threads (->> (vals threads-map)
(dcm/apply-filters local profile) (dcm/apply-filters local profile)
@ -242,4 +243,8 @@
(gsh/has-point? frame position))))] (gsh/has-point? frame position))))]
[:aside {:class (stl/css :comments-sidebar)} [:aside {:class (stl/css :comments-sidebar)}
[:div {:class (stl/css :settings-bar-inside)} [: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.formats :as fmt]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.viewer.comments :refer [comments-menu]] [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.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l] [okulary.core :as l]
@ -173,7 +173,8 @@
:interactions [:* :interactions [:*
(when index (when index
[:> flows-menu* {:page page :index index}]) [:> flows-menu* {:page page :index index}])
[:& interactions-menu {:interactions-mode interactions-mode}]] [:> interactions-menu*
{:interactions-mode interactions-mode}]]
:comments [:& comments-menu] :comments [:& comments-menu]
[:div {:class (stl/css :view-options)}]) [:div {:class (stl/css :view-options)}])

View file

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

View file

@ -9,7 +9,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.component :as ctk] [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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.shape-icon :as sir] [app.main.ui.components.shape-icon :as sir]

View file

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