diff --git a/common/src/app/common/types/profile.cljc b/common/src/app/common/types/profile.cljc
new file mode 100644
index 0000000000..24272cab67
--- /dev/null
+++ b/common/src/app/common/types/profile.cljc
@@ -0,0 +1,23 @@
+;; 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.common.types.profile
+ (:require
+ [app.common.schema :as sm]
+ [app.common.time :as cm]))
+
+(def schema:profile
+ [:map {:title "Profile"}
+ [:id ::sm/uuid]
+ [:created-at {:optional true} ::cm/inst]
+ [:fullname {:optional true} :string]
+ [:email {:optional true} :string]
+ [:lang {:optional true} :string]
+ [:theme {:optional true} :string]
+ [:photo-id {:optional true} ::sm/uuid]
+ ;; Only present on resolved profile objects, the resolve process
+ ;; takes the photo-id or geneates an image from the name
+ [:photo-url {:optional true} :string]])
diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs
index 75a42c0e26..223942df86 100644
--- a/frontend/src/app/main/data/profile.cljs
+++ b/frontend/src/app/main/data/profile.cljs
@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.spec :as us]
+ [app.common.types.profile :refer [schema:profile]]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.event :as ev]
@@ -27,16 +28,6 @@
;; --- SCHEMAS
-(def ^:private
- schema:profile
- [:map {:title "Profile"}
- [:id ::sm/uuid]
- [:created-at {:optional true} :any]
- [:fullname {:optional true} :string]
- [:email {:optional true} :string]
- [:lang {:optional true} :string]
- [:theme {:optional true} :string]])
-
(def check-profile
(sm/check-fn schema:profile))
diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs
index ce17291f7a..dd46c3cf16 100644
--- a/frontend/src/app/main/data/workspace/versions.cljs
+++ b/frontend/src/app/main/data/workspace/versions.cljs
@@ -27,9 +27,9 @@
(declare fetch-versions)
-(defn init-version-state
+(defn init-versions-state
[]
- (ptk/reify ::init-version-state
+ (ptk/reify ::init-versions-state
ptk/UpdateEvent
(update [_ state]
(assoc state :workspace-versions default-state))
@@ -38,9 +38,9 @@
(watch [_ _ _]
(rx/of (fetch-versions)))))
-(defn update-version-state
+(defn update-versions-state
[version-state]
- (ptk/reify ::update-version-state
+ (ptk/reify ::update-versions-state
ptk/UpdateEvent
(update [_ state]
(update state :workspace-versions merge version-state))))
@@ -52,7 +52,7 @@
(watch [_ state _]
(when-let [file-id (:current-file-id state)]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
- (rx/map #(update-version-state {:status :loaded :data %})))))))
+ (rx/map #(update-versions-state {:status :loaded :data %})))))))
(defn create-version
[]
@@ -73,7 +73,7 @@
(rx/mapcat #(rp/cmd! :create-file-snapshot {:file-id file-id :label label}))
(rx/mapcat
(fn [{:keys [id]}]
- (rx/of (update-version-state {:editing id})
+ (rx/of (update-versions-state {:editing id})
(fetch-versions))))))))))
(defn rename-version
@@ -86,7 +86,7 @@
(watch [_ state _]
(let [file-id (:current-file-id state)]
(rx/merge
- (rx/of (update-version-state {:editing false})
+ (rx/of (update-versions-state {:editing nil})
(ptk/event ::ev/event {::ev/name "rename-version"
:file-id file-id}))
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
@@ -144,7 +144,7 @@
(->> (rp/cmd! :update-file-snapshot params)
(rx/mapcat (fn [_]
- (rx/of (update-version-state {:editing id})
+ (rx/of (update-versions-state {:editing id})
(fetch-versions)
(ptk/event ::ev/event {::ev/name "pin-version"})))))))))
diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index 0fddb71e97..33eaff367f 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -27,13 +27,13 @@
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]]
[app.main.ui.ds.notifications.toast :refer [toast*]]
- [app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
[app.main.ui.ds.product.avatar :refer [avatar*]]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.ds.product.loader :refer [loader*]]
- [app.main.ui.ds.product.user-milestone :refer [user-milestone*]]
+ [app.main.ui.ds.product.milestone :refer [milestone*]]
+ [app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
[app.main.ui.ds.storybook :as sb]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.main.ui.ds.utilities.date :refer [date*]]
@@ -41,7 +41,6 @@
[app.util.i18n :as i18n]
[rumext.v2 :as mf]))
-
(i18n/init! cf/translations)
(def default
@@ -72,8 +71,8 @@
:Swatch swatch*
:Cta cta*
:Avatar avatar*
- :AutosavedMilestone autosaved-milestone*
- :UserMilestone user-milestone*
+ :Milestone milestone*
+ :MilestoneGroup milestone-group*
:Date date*
;; meta / misc
:meta
diff --git a/frontend/src/app/main/ui/ds/product/avatar.cljs b/frontend/src/app/main/ui/ds/product/avatar.cljs
index fe52a47ab5..86a995966d 100644
--- a/frontend/src/app/main/ui/ds/product/avatar.cljs
+++ b/frontend/src/app/main/ui/ds/product/avatar.cljs
@@ -9,37 +9,44 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.schema :as sm]
+ [app.common.types.profile :refer [schema:profile]]
+ [app.config :as cfg]
[app.util.avatars :as avatars]
- [rumext.v2 :as mf]))
+ [rumext.v2 :as mf]
+ [rumext.v2.util :as mfu]))
(def ^:private schema:avatar
[:map
[:class {:optional true} :string]
[:tag {:optional true} :string]
- [:name {:optional true} [:maybe :string]]
- [:url {:optional true} [:maybe :string]]
- [:color {:optional true} [:maybe :string]]
+ [:profile schema:profile]
[:selected {:optional true} :boolean]
[:variant {:optional true}
[:maybe [:enum "S" "M" "L"]]]])
-(mf/defc avatar*
- {::mf/schema schema:avatar}
+(defn- get-url
+ [{:keys [photo-url photo-id fullname]}]
+ (or photo-url
+ (some-> photo-id cfg/resolve-media)
+ (avatars/generate {:name fullname})))
- [{:keys [tag class name color url selected variant] :rest props}]
- (let [variant (or variant "S")
- url (if (and (some? url) (d/not-empty? url))
- url
- (avatars/generate {:name name :color color}))]
- [:> (or tag "div")
- {:class (d/append-class
- class
- (stl/css-case :avatar true
- :avatar-small (= variant "S")
- :avatar-medium (= variant "M")
- :avatar-large (= variant "L")
- :is-selected selected))
- :style {"--avatar-color" color}
- :title name}
+(mf/defc avatar*
+ {::mf/schema (sm/schema schema:avatar)}
+ [{:keys [tag class profile selected variant]}]
+ (let [variant (d/nilv variant "S")
+ profile (if (object? profile)
+ (mfu/bean profile)
+ profile)
+ href (mf/with-memo [profile]
+ (get-url profile))
+ class' (stl/css-case :avatar true
+ :avatar-small (= variant "S")
+ :avatar-medium (= variant "M")
+ :avatar-large (= variant "L")
+ :is-selected selected)]
+ [:> (d/nilv tag "div")
+ {:class [class class']
+ :title (:fullname profile)}
[:div {:class (stl/css :avatar-image)}
- [:img {:alt name :src url}]]]))
+ [:img {:alt (:fullname profile) :src href}]]]))
diff --git a/frontend/src/app/main/ui/ds/product/avatar.stories.jsx b/frontend/src/app/main/ui/ds/product/avatar.stories.jsx
index b6e6f72dfa..6bbecd3448 100644
--- a/frontend/src/app/main/ui/ds/product/avatar.stories.jsx
+++ b/frontend/src/app/main/ui/ds/product/avatar.stories.jsx
@@ -13,9 +13,6 @@ export default {
url: {
control: { type: "text" },
},
- color: {
- control: { type: "color" },
- },
variant: {
options: ["S", "M", "L"],
control: { type: "select" },
@@ -27,11 +24,20 @@ export default {
args: {
name: "Ada Lovelace",
url: "/images/avatar-blue.jpg",
- color: "#79d4ff",
variant: "S",
selected: false,
},
- render: ({ ...args }) => ,
+ render: ({name, url, ...args }) => {
+ const profile = {
+ id: "00000000-0000-0000-0000-000000000000",
+ fullname: name
+ };
+ if (url) {
+ profile.photoUrl = url;
+ };
+
+ return ;
+ }
};
export const Default = {};
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.cljs b/frontend/src/app/main/ui/ds/product/milestone.cljs
similarity index 54%
rename from frontend/src/app/main/ui/ds/product/user_milestone.cljs
rename to frontend/src/app/main/ui/ds/product/milestone.cljs
index 94ba862b3a..177acf17c2 100644
--- a/frontend/src/app/main/ui/ds/product/user_milestone.cljs
+++ b/frontend/src/app/main/ui/ds/product/milestone.cljs
@@ -4,83 +4,84 @@
;;
;; Copyright (c) KALEIDOS INC
-(ns app.main.ui.ds.product.user-milestone
+(ns app.main.ui.ds.product.milestone
(:require-macros
[app.main.style :as stl])
(:require
+ [app.common.schema :as sm]
[app.common.time :as ct]
+ [app.common.types.profile :refer [schema:profile]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.product.avatar :refer [avatar*]]
- [app.main.ui.ds.utilities.date :refer [valid-date?]]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.object :as obj]
[rumext.v2 :as mf]))
+(def ^:private schema:callback
+ [:maybe [:fn fn?]])
+
(def ^:private schema:milestone
[:map
[:class {:optional true} :string]
[:active {:optional true} :boolean]
[:editing {:optional true} :boolean]
[:locked {:optional true} :boolean]
- [:user
- [:map
- [:name {:optional true} [:maybe :string]]
- [:avatar {:optional true} [:maybe :string]]
- [:color {:optional true} [:maybe :string]]]]
+ [:profile {:optional true} schema:profile]
[:label :string]
- [:date [:fn valid-date?]]
- [:onOpenMenu {:optional true} [:maybe [:fn fn?]]]
- [:onFocusInput {:optional true} [:maybe [:fn fn?]]]
- [:onBlurInput {:optional true} [:maybe [:fn fn?]]]
- [:onKeyDownInput {:optional true} [:maybe [:fn fn?]]]])
+ [:created-at ::ct/inst]
+ [:on-open-menu {:optional true} schema:callback]
+ [:on-focus-menu {:optional true} schema:callback]
+ [:on-blur-menu {:optional true} schema:callback]
+ [:on-key-down-input {:optional true} schema:callback]])
-(mf/defc user-milestone*
- {::mf/schema schema:milestone}
- [{:keys [class active editing locked user label date
- onOpenMenu onFocusInput onBlurInput onKeyDownInput] :rest props}]
- (let [class' (stl/css-case :milestone true
- :is-selected active)
- props (mf/spread-props props {:class [class class']
- :data-testid "milestone"})
- date (if (ct/inst? date)
- date
- (ct/inst date))]
+(mf/defc milestone*
+ {::mf/schema (sm/schema schema:milestone)}
+ [{:keys [class active editing locked label created-at profile
+ on-open-menu on-focus-input on-blur-input on-key-down-input] :rest props}]
+ (let [class'
+ (stl/css-case :milestone true
+ :is-selected active)
+ props
+ (mf/spread-props props
+ {:class [class class']
+ :data-testid "milestone"})
+ created-at
+ (if (ct/inst? created-at)
+ created-at
+ (ct/inst created-at))]
[:> :div props
- [:> avatar* {:name (obj/get user "name")
- :url (obj/get user "avatar")
- :color (obj/get user "color")
+ [:> avatar* {:profile profile
:variant "S"
:class (stl/css :avatar)}]
- (if editing
+ (if ^boolean editing
[:> input*
{:class (stl/css :name-input)
:variant "seamless"
:default-value label
:auto-focus true
- :on-focus onFocusInput
- :on-blur onBlurInput
- :on-key-down onKeyDownInput}]
+ :on-focus on-focus-input
+ :on-blur on-blur-input
+ :on-key-down on-key-down-input}]
[:div {:class (stl/css :name-wrapper)}
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label]
(when locked
[:> i/icon* {:icon-id i/lock :class (stl/css :lock-icon)}])])
[:*
- [:time {:date-time (ct/format-inst date :iso)
+ [:time {:date-time (ct/format-inst created-at :iso)
:class (stl/css :date)}
- (ct/timeago date)]
+ (ct/timeago created-at)]
[:div {:class (stl/css :milestone-buttons)}
[:> icon-button* {:class (stl/css :menu-button)
:variant "ghost"
:icon "menu"
:aria-label (tr "workspace.versions.version-menu")
- :on-click onOpenMenu}]]]]))
+ :on-click on-open-menu}]]]]))
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.scss b/frontend/src/app/main/ui/ds/product/milestone.scss
similarity index 100%
rename from frontend/src/app/main/ui/ds/product/user_milestone.scss
rename to frontend/src/app/main/ui/ds/product/milestone.scss
diff --git a/frontend/src/app/main/ui/ds/product/milestone.stories.jsx b/frontend/src/app/main/ui/ds/product/milestone.stories.jsx
new file mode 100644
index 0000000000..1af1762f26
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/milestone.stories.jsx
@@ -0,0 +1,59 @@
+import * as React from "react";
+import Components from "@target/components";
+
+const { Milestone } = Components;
+
+export default {
+ title: "Product/Milestones/Milestone",
+ component: Milestone,
+
+ argTypes: {
+ profileName: {
+ control: { type: "text" },
+ },
+ profileAvatar: {
+ control: { type: "text" },
+ },
+ label: {
+ control: { type: "text" },
+ },
+ createdAt: {
+ control: { type: "date" },
+ },
+ active: {
+ control: { type: "boolean" },
+ },
+ editing: {
+ control: { type: "boolean" },
+ },
+ locked: {
+ control: { type: "boolean" },
+ },
+ },
+ args: {
+ label: "Milestone 1",
+ profileName: "Ada Lovelace",
+ profileAvatar: "/images/avatar-blue.jpg",
+ createdAt: 1735686000000,
+ active: false,
+ editing: false,
+ },
+ render: ({ profileName, profileAvatar, profileColor, createdAt, ...args }) => {
+ const profile = {
+ id: "00000000-0000-0000-0000-000000000000",
+ fullname: profileName
+ };
+
+ if (profileAvatar) {
+ profile.photoUrl = profileAvatar;
+ }
+
+ if (createdAt instanceof Number) {
+ createdAt = new Date(createdAt);
+ }
+
+ return ;
+ },
+};
+
+export const Default = {};
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs b/frontend/src/app/main/ui/ds/product/milestone_group.cljs
similarity index 55%
rename from frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
rename to frontend/src/app/main/ui/ds/product/milestone_group.cljs
index c3eb610edf..41a2694108 100644
--- a/frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
+++ b/frontend/src/app/main/ui/ds/product/milestone_group.cljs
@@ -4,58 +4,78 @@
;;
;; Copyright (c) KALEIDOS INC
-(ns app.main.ui.ds.product.autosaved-milestone
+(ns app.main.ui.ds.product.milestone-group
(:require-macros
- [app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.common.schema :as sm]
+ [app.common.time :as cm]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
- [app.main.ui.ds.utilities.date :refer [date* valid-date?]]
+ [app.main.ui.ds.utilities.date :refer [date*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
-(def ^:private schema:milestone
+(def ^:private schema:milestone-group
[:map
[:class {:optional true} :string]
[:active {:optional true} :boolean]
- [:versionToggled {:optional true} :boolean]
[:label :string]
- [:autosavedMessage :string]
- [:snapshots [:vector [:fn valid-date?]]]])
+ [:snapshots [:vector ::cm/inst]]])
-(mf/defc autosaved-milestone*
- {::mf/schema schema:milestone}
- [{:keys [class active versionToggled label autosavedMessage snapshots
- onClickSnapshotMenu onToggleExpandSnapshots] :rest props}]
- (let [class (d/append-class class (stl/css-case :milestone true :is-selected active))
- props (mf/spread-props props {:class class :data-testid "milestone"})
+(mf/defc milestone-group*
+ {::mf/schema (sm/schema schema:milestone-group)}
+ [{:keys [class active label snapshots on-menu-click] :rest props}]
+ (let [class'
+ (stl/css-case :milestone true
+ :is-selected active)
- handle-click-menu
+ props
+ (mf/spread-props props
+ {:class [class class']
+ :data-testid "milestone"})
+
+ open*
+ (mf/use-state false)
+
+ open?
+ (deref open*)
+
+ on-toggle-visibility
+ (mf/use-fn (fn [] (swap! open* not)))
+
+ on-menu-click
(mf/use-fn
- (mf/deps onClickSnapshotMenu)
+ (mf/deps on-menu-click)
(fn [event]
(let [index (-> (dom/get-current-target event)
(dom/get-data "index")
(d/parse-integer))]
- (when onClickSnapshotMenu
- (onClickSnapshotMenu event index)))))]
- [:> "div" props
+ (when (fn? on-menu-click)
+ (on-menu-click index event)))))]
+
+ [:> :div props
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label]
[:div {:class (stl/css :snapshots)}
[:button {:class (stl/css :toggle-snapshots)
:aria-label (tr "workspace.versions.expand-snapshot")
- :on-click onToggleExpandSnapshots}
+ :on-click on-toggle-visibility}
[:> i/icon* {:icon-id i/clock :class (stl/css :icon-clock)}]
- [:> text* {:as "span" :typography t/body-medium :class (stl/css :toggle-message)} autosavedMessage]
- [:> i/icon* {:icon-id i/arrow :class (stl/css-case :icon-arrow true :icon-arrow-toggled versionToggled)}]]
+ [:> text* {:as "span"
+ :typography t/body-medium
+ :class (stl/css :toggle-message)}
+ (tr "workspace.versions.autosaved.entry" (count snapshots))]
+ [:> i/icon* {:icon-id i/arrow
+ :class (stl/css-case :icon-arrow true
+ :icon-arrow-toggled open?)}]]
- (when versionToggled
+ (when ^boolean open?
(for [[idx d] (d/enumerate snapshots)]
[:div {:key (dm/str "entry-" idx)
:class (stl/css :version-entry)}
@@ -65,5 +85,5 @@
:icon "menu"
:aria-label (tr "workspace.versions.version-menu")
:data-index idx
- :on-click handle-click-menu}]]))]]))
+ :on-click on-menu-click}]]))]]))
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.scss b/frontend/src/app/main/ui/ds/product/milestone_group.scss
similarity index 100%
rename from frontend/src/app/main/ui/ds/product/autosaved_milestone.scss
rename to frontend/src/app/main/ui/ds/product/milestone_group.scss
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx b/frontend/src/app/main/ui/ds/product/milestone_group.stories.jsx
similarity index 55%
rename from frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx
rename to frontend/src/app/main/ui/ds/product/milestone_group.stories.jsx
index 204185c46a..293ad54528 100644
--- a/frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx
+++ b/frontend/src/app/main/ui/ds/product/milestone_group.stories.jsx
@@ -1,11 +1,11 @@
import * as React from "react";
import Components from "@target/components";
-const { AutosavedMilestone } = Components;
+const { MilestoneGroup } = Components;
export default {
- title: "Product/Milestones/Autosaved",
- component: AutosavedMilestone,
+ title: "Product/Milestones/MilestoneGroup",
+ component: MilestoneGroup,
argTypes: {
label: {
@@ -27,17 +27,10 @@ export default {
args: {
label: "Milestone 1",
active: false,
- versionToggled: false,
- snapshots: [1737452413841, 1737452422063, 1737452431603],
- autosavedMessage: "3 autosave versions",
+ snapshots: [1737452413841, 1737452422063, 1737452431603]
},
render: ({ ...args }) => {
- const user = {
- name: args.userName,
- avatar: args.userAvatar,
- color: args.userColor,
- };
- return ;
+ return ;
},
};
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx b/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx
deleted file mode 100644
index ab55121078..0000000000
--- a/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import * as React from "react";
-import Components from "@target/components";
-
-const { UserMilestone } = Components;
-
-export default {
- title: "Product/Milestones/User",
- component: UserMilestone,
-
- argTypes: {
- userName: {
- control: { type: "text" },
- },
- userAvatar: {
- control: { type: "text" },
- },
- userColor: {
- control: { type: "color" },
- },
- label: {
- control: { type: "text" },
- },
- date: {
- control: { type: "date" },
- },
- active: {
- control: { type: "boolean" },
- },
- editing: {
- control: { type: "boolean" },
- },
- autosaved: {
- control: { type: "boolean" },
- },
- versionToggled: {
- control: { type: "boolean" },
- },
- snapshots: {
- control: { type: "object" },
- },
- },
- args: {
- label: "Milestone 1",
- userName: "Ada Lovelace",
- userAvatar: "/images/avatar-blue.jpg",
- userColor: "#79d4ff",
- date: 1735686000000,
- active: false,
- editing: false,
- },
- render: ({ ...args }) => {
- const user = {
- name: args.userName,
- avatar: args.userAvatar,
- color: args.userColor,
- };
- return ;
- },
-};
-
-export const Default = {};
diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
index a832ef92a0..2f8088f742 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
@@ -20,9 +20,9 @@
[app.main.ui.dashboard.subscription :refer [get-subscription-type]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
- [app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
[app.main.ui.ds.product.cta :refer [cta*]]
- [app.main.ui.ds.product.user-milestone :refer [user-milestone*]]
+ [app.main.ui.ds.product.milestone :refer [milestone*]]
+ [app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@@ -31,10 +31,10 @@
[okulary.core :as l]
[rumext.v2 :as mf]))
-(def versions
+(def ^:private versions
(l/derived :workspace-versions st/state))
-(defn get-versions-stored-days
+(defn- get-versions-stored-days
[team]
(let [subscription-type (get-subscription-type (:subscription team))]
(cond
@@ -42,7 +42,7 @@
(= subscription-type "enterprise") 90
:else 7)))
-(defn get-versions-warning-subtext
+(defn- get-versions-warning-subtext
[team]
(let [subscription-type (get-subscription-type (:subscription team))
is-owner? (-> team :permissions :is-owner)
@@ -58,333 +58,330 @@
(tr "subscription.workspace.versions.warning.subtext-member" email-owner email-owner))
(tr "workspace.versions.warning.subtext" support-email))))
-(defn group-snapshots
+(defn- group-snapshots
[data]
(->> (concat
(->> data
- (filterv #(= "user" (:created-by %)))
+ (filter #(= "user" (:created-by %)))
(map #(assoc % :type :version)))
(->> data
- (filterv #(= "system" (:created-by %)))
+ (filter #(= "system" (:created-by %)))
(group-by #(ct/format-inst (:created-at %) :iso-date))
(map (fn [[day entries]]
{:type :snapshot
:created-at (ct/inst day)
:snapshots entries}))))
(sort-by :created-at)
+ (map-indexed (fn [index item]
+ (assoc item :index index)))
(reverse)))
-(mf/defc version-entry
- [{:keys [entry profile current-profile on-restore-version on-delete-version on-rename-version on-lock-version on-unlock-version editing?]}]
- (let [show-menu? (mf/use-state false)
-
- handle-open-menu
- (mf/use-fn
- (fn []
- (reset! show-menu? true)))
-
- handle-close-menu
- (mf/use-fn
- (fn []
- (reset! show-menu? false)))
-
- handle-rename-version
- (mf/use-fn
- (mf/deps entry)
- (fn []
- (st/emit! (dwv/update-version-state {:editing (:id entry)}))))
-
- handle-restore-version
- (mf/use-fn
- (mf/deps entry on-restore-version)
- (fn []
- (when on-restore-version
- (on-restore-version (:id entry)))))
-
- handle-delete-version
- (mf/use-callback
- (mf/deps entry on-delete-version)
- (fn []
- (when on-delete-version
- (on-delete-version (:id entry)))))
-
- handle-lock-version
- (mf/use-callback
- (mf/deps entry on-lock-version)
- (fn []
- (when on-lock-version
- (on-lock-version (:id entry)))))
-
- handle-unlock-version
- (mf/use-callback
- (mf/deps entry on-unlock-version)
- (fn []
- (when on-unlock-version
- (on-unlock-version (:id entry)))))
-
- handle-name-input-focus
- (mf/use-fn
- (fn [event]
- (dom/select-text! (dom/get-target event))))
-
- handle-name-input-blur
- (mf/use-fn
- (mf/deps entry on-rename-version)
- (fn [event]
- (let [label (str/trim (dom/get-target-val event))]
- (when (and (not (str/empty? label))
- (some? on-rename-version))
- (on-rename-version (:id entry) label))
- (st/emit! (dwv/update-version-state {:editing nil})))))
-
- handle-name-input-key-down
- (mf/use-fn
- (mf/deps handle-name-input-blur)
- (fn [event]
- (cond
- (kbd/enter? event)
- (handle-name-input-blur event)
-
- (kbd/esc? event)
- (st/emit! (dwv/update-version-state {:editing nil})))))]
-
- [:li {:class (stl/css :version-entry-wrap)}
- [:> user-milestone* {:label (:label entry)
- :user #js {:name (:fullname profile)
- :avatar (cfg/resolve-profile-photo-url profile)
- :color (:color profile)}
- :editing editing?
- :date (:created-at entry)
- :locked (boolean (:locked-by entry))
- :onOpenMenu handle-open-menu
- :onFocusInput handle-name-input-focus
- :onBlurInput handle-name-input-blur
- :onKeyDownInput handle-name-input-key-down}]
-
- [:& dropdown {:show @show-menu? :on-close handle-close-menu}
- (let [current-user-id (:id current-profile)
- version-creator-id (:profile-id entry)
- locked-by-id (:locked-by entry)
- is-version-creator? (= current-user-id version-creator-id)
- is-locked? (some? locked-by-id)
- is-locked-by-me? (= current-user-id locked-by-id)
- can-rename? is-version-creator?
- can-lock? (and is-version-creator? (not is-locked?))
- can-unlock? (and is-version-creator? is-locked-by-me?)
- can-delete? (or (not is-locked?) (and is-locked? is-locked-by-me?))]
- [:ul {:class (stl/css :version-options-dropdown)}
- (when can-rename?
- [:li {:class (stl/css :menu-option)
- :role "button"
- :on-click handle-rename-version} (tr "labels.rename")])
- [:li {:class (stl/css :menu-option)
- :role "button"
- :on-click handle-restore-version} (tr "labels.restore")]
- (cond
- can-unlock?
- [:li {:class (stl/css :menu-option)
- :role "button"
- :on-click handle-unlock-version} (tr "labels.unlock")]
- can-lock?
- [:li {:class (stl/css :menu-option)
- :role "button"
- :on-click handle-lock-version} (tr "labels.lock")])
- (when can-delete?
- [:li {:class (stl/css :menu-option)
- :role "button"
- :on-click handle-delete-version} (tr "labels.delete")])])]]))
-
-(mf/defc snapshot-entry
- [{:keys [index is-expanded entry on-toggle-expand on-pin-snapshot on-restore-snapshot]}]
-
- (let [open-menu (mf/use-state nil)
- entry-ref (mf/use-ref nil)
-
- handle-toggle-expand
- (mf/use-fn
- (mf/deps index on-toggle-expand)
- (fn []
- (when on-toggle-expand
- (on-toggle-expand index))))
-
- handle-pin-snapshot
- (mf/use-fn
- (mf/deps on-pin-snapshot)
- (fn [event]
- (let [node (dom/get-current-target event)
- id (-> (dom/get-data node "id") uuid/parse)]
- (when on-pin-snapshot (on-pin-snapshot id)))))
-
- handle-restore-snapshot
- (mf/use-fn
- (mf/deps on-restore-snapshot)
- (fn [event]
- (let [node (dom/get-current-target event)
- id (-> (dom/get-data node "id") uuid/parse)]
- (when on-restore-snapshot (on-restore-snapshot id)))))
-
-
- handle-open-snapshot-menu
- (mf/use-fn
- (mf/deps entry)
- (fn [event index]
- (let [snapshot (nth (:snapshots entry) index)
- current-bb (-> entry-ref mf/ref-val dom/get-bounding-rect :top)
- target-bb (-> event dom/get-target dom/get-bounding-rect :top)
- offset (+ (- target-bb current-bb) 32)]
- (swap! open-menu assoc
- :snapshot (:id snapshot)
- :offset offset))))]
-
- [:li {:ref entry-ref :class (stl/css :version-entry-wrap)}
- [:> autosaved-milestone*
- {:label (tr "workspace.versions.autosaved.version"
- (ct/format-inst (:created-at entry) :localized-date))
- :autosavedMessage (tr "workspace.versions.autosaved.entry" (count (:snapshots entry)))
- :snapshots (mapv :created-at (:snapshots entry))
- :versionToggled is-expanded
- :onClickSnapshotMenu handle-open-snapshot-menu
- :onToggleExpandSnapshots handle-toggle-expand}]
-
- [:& dropdown {:show (some? @open-menu)
- :on-close #(reset! open-menu nil)}
- [:ul {:class (stl/css :version-options-dropdown)
- :style {"--offset" (dm/str (:offset @open-menu) "px")}}
- [:li {:class (stl/css :menu-option)
- :role "button"
- :data-id (dm/str (:snapshot @open-menu))
- :on-click handle-restore-snapshot}
- (tr "workspace.versions.button.restore")]
- [:li {:class (stl/css :menu-option)
- :role "button"
- :data-id (dm/str (:snapshot @open-menu))
- :on-click handle-pin-snapshot}
- (tr "workspace.versions.button.pin")]]]]))
-
-(mf/defc versions-toolbox*
- []
- (let [profiles (mf/deref refs/profiles)
- profile (mf/deref refs/profile)
- team (mf/deref refs/team)
-
- expanded (mf/use-state #{})
-
- {:keys [status data editing]}
- (mf/deref versions)
-
- ;; Store users that have a version
- data-users
- (mf/use-memo
- (mf/deps data)
- (fn []
- (into #{} (keep (fn [{:keys [created-by profile-id]}]
- (when (= "user" created-by) profile-id))) data)))
- data
- (mf/use-memo
- (mf/deps @versions)
- (fn []
- (->> data
- (filter #(or (not (:filter @versions))
- (and
- (= "user" (:created-by %))
- (= (:filter @versions) (:profile-id %)))))
- (group-snapshots))))
-
- handle-create-version
- (mf/use-fn
- (fn []
- (st/emit! (dwv/create-version))))
-
- handle-toggle-expand
- (mf/use-fn
- (fn [id]
- (swap! expanded
- (fn [expanded]
- (let [has-element? (contains? expanded id)]
- (cond-> expanded
- has-element? (disj id)
- (not has-element?) (conj id)))))))
-
- handle-rename-version
- (mf/use-fn
- (fn [id label]
- (st/emit! (dwv/rename-version id label))))
-
-
- handle-restore-version
- (mf/use-fn
- (fn [origin id]
- (st/emit!
- (ntf/dialog
+(defn- open-restore-version-dialog
+ [origin id]
+ (st/emit! (ntf/dialog
:content (tr "workspace.versions.restore-warning")
:controls :inline-actions
:cancel {:label (tr "workspace.updates.dismiss")
:callback #(st/emit! (ntf/hide))}
:accept {:label (tr "labels.restore")
:callback #(st/emit! (dwv/restore-version id origin))}
- :tag :restore-dialog))))
+ :tag :restore-dialog)))
- handle-restore-version-pinned
+(mf/defc version-entry*
+ {::mf/private true}
+ [{:keys [entry current-profile on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
+ (let [show-menu? (mf/use-state false)
+ profiles (mf/deref refs/profiles)
+
+ created-by (get profiles (:profile-id entry))
+
+ on-open-menu
+ (mf/use-fn #(reset! show-menu? true))
+
+ on-close-menu
+ (mf/use-fn #(reset! show-menu? false))
+
+ on-edit
(mf/use-fn
- (mf/deps handle-restore-version)
- (fn [id]
- (handle-restore-version :version id)))
+ (mf/deps on-edit entry)
+ (fn [event]
+ (on-edit (:id entry) event)))
- handle-restore-version-snapshot
+ on-restore
(mf/use-fn
- (mf/deps handle-restore-version)
- (fn [id]
- (handle-restore-version :snapshot id)))
+ (mf/deps entry on-restore)
+ (fn []
+ (when (fn? on-restore)
+ (on-restore (:id entry)))))
- handle-delete-version
+ on-delete
+ (mf/use-callback
+ (mf/deps entry on-delete)
+ (fn [event]
+ (when (fn? on-delete)
+ (on-delete (:id entry) event))))
+
+ on-lock
+ (mf/use-callback
+ (mf/deps entry on-lock)
+ (fn []
+ (when on-lock
+ (on-lock (:id entry)))))
+
+ on-unlock
+ (mf/use-callback
+ (mf/deps entry on-unlock)
+ (fn []
+ (when on-unlock
+ (on-unlock (:id entry)))))
+
+ on-name-input-focus
+ (mf/use-fn
+ (fn [event]
+ (dom/select-text! (dom/get-target event))))
+
+ on-name-input-blur
+ (mf/use-fn
+ (mf/deps entry on-rename on-cancel-edit)
+ (fn [event]
+ (let [label (str/trim (dom/get-target-val event))]
+ (if (and (not (str/empty? label))
+ (fn? on-rename))
+ (on-rename (:id entry) label event)
+ (on-cancel-edit (:id entry) event)))))
+
+ on-name-input-key-down
+ (mf/use-fn
+ (mf/deps entry on-cancel-edit on-name-input-blur)
+ (fn [event]
+ (cond
+ (kbd/enter? event)
+ (on-name-input-blur event)
+
+ (kbd/esc? event)
+ (when (fn? on-cancel-edit)
+ (on-cancel-edit (:id entry) event)))))]
+
+ [:li {:class (stl/css :version-entry-wrap)}
+ [:> milestone* {:label (:label entry)
+ :profile created-by
+ :editing is-editing
+ :created-at (:created-at entry)
+ :locked (some? (:locked-by entry))
+ :on-open-menu on-open-menu
+ :on-focus-input on-name-input-focus
+ :on-blur-input on-name-input-blur
+ :on-key-down-input on-name-input-key-down}]
+
+ [:& dropdown {:show @show-menu?
+ :on-close on-close-menu}
+ (let [current-user-id (:id current-profile)
+ locked-by-id (:locked-by entry)
+ im-the-owner? (= current-user-id (:id created-by))
+ is-locked-by-me? (= current-user-id locked-by-id)
+ is-locked? (some? locked-by-id)
+ can-delete? (or (not is-locked?)
+ (and is-locked?
+ is-locked-by-me?))]
+ [:ul {:class (stl/css :version-options-dropdown)}
+ (when im-the-owner?
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-edit}
+ (tr "labels.rename")])
+
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-restore}
+ (tr "labels.restore")]
+
+ (cond
+ is-locked-by-me?
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-unlock}
+ (tr "labels.unlock")]
+
+ (and im-the-owner? (not is-locked?))
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-lock}
+ (tr "labels.lock")])
+
+ (when can-delete?
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-delete}
+ (tr "labels.delete")])])]]))
+
+(mf/defc snapshot-entry*
+ [{:keys [entry on-pin-snapshot on-restore-snapshot]}]
+
+ (let [open-menu* (mf/use-state nil)
+ entry-ref (mf/use-ref nil)
+
+ on-pin-snapshot
+ (mf/use-fn
+ (mf/deps on-pin-snapshot)
+ (fn [event]
+ (let [node (dom/get-current-target event)
+ id (-> node
+ (dom/get-data "id")
+ (uuid/parse))]
+ (when (fn? on-pin-snapshot)
+ (on-pin-snapshot id event)))))
+
+ on-restore-snapshot
+ (mf/use-fn
+ (mf/deps on-restore-snapshot)
+ (fn [event]
+ (let [node (dom/get-current-target event)
+ id (-> node
+ (dom/get-data "id")
+ (uuid/parse))]
+ (when (fn? on-restore-snapshot)
+ (on-restore-snapshot id event)))))
+
+ on-open-snapshot-menu
+ (mf/use-fn
+ (mf/deps entry)
+ (fn [index event]
+ (let [snapshot (nth (:snapshots entry) index)
+ current-bb (-> entry-ref mf/ref-val dom/get-bounding-rect :top)
+ target-bb (-> event dom/get-target dom/get-bounding-rect :top)
+ offset (+ (- target-bb current-bb) 32)]
+ (swap! open-menu* assoc
+ :snapshot (:id snapshot)
+ :offset offset))))]
+
+ [:li {:ref entry-ref :class (stl/css :version-entry-wrap)}
+ [:> milestone-group*
+ {:label (tr "workspace.versions.autosaved.version"
+ (ct/format-inst (:created-at entry) :localized-date))
+ :snapshots (mapv :created-at (:snapshots entry))
+ :on-menu-click on-open-snapshot-menu}]
+
+ [:& dropdown {:show (some? @open-menu*)
+ :on-close #(reset! open-menu* nil)}
+ [:ul {:class (stl/css :version-options-dropdown)
+ :style {"--offset" (dm/str (:offset @open-menu*) "px")}}
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :data-id (dm/str (:snapshot @open-menu*))
+ :on-click on-restore-snapshot}
+ (tr "workspace.versions.button.restore")]
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :data-id (dm/str (:snapshot @open-menu*))
+ :on-click on-pin-snapshot}
+ (tr "workspace.versions.button.pin")]]]]))
+
+(mf/defc versions-toolbox*
+ []
+ (let [profiles (mf/deref refs/profiles)
+ profile (mf/deref refs/profile)
+ team (mf/deref refs/team)
+
+ {:keys [status data editing] :as state}
+ (mf/deref versions)
+
+ users
+ (mf/with-memo [data]
+ (into #{}
+ (keep (fn [{:keys [created-by profile-id]}]
+ (when (= "user" created-by)
+ profile-id)))
+ data))
+
+ entries
+ (mf/with-memo [state]
+ (->> (:data state)
+ (filter #(or (not (:filter state))
+ (and (= "user" (:created-by %))
+ (= (:filter state) (:profile-id %)))))
+ (group-snapshots)))
+
+ on-create-version
+ (mf/use-fn
+ (fn [] (st/emit! (dwv/create-version))))
+
+ on-edit-version
+ (mf/use-fn
+ (fn [id _event]
+ (st/emit! (dwv/update-versions-state {:editing id}))))
+
+ on-cancel-version-edition
+ (mf/use-fn
+ (fn [_id _event]
+ (st/emit! (dwv/update-versions-state {:editing nil}))))
+
+ on-rename-version
+ (mf/use-fn
+ (fn [id label]
+ (st/emit! (dwv/rename-version id label))))
+
+ on-restore-version
+ (mf/use-fn
+ (fn [id _event]
+ (open-restore-version-dialog :version id)))
+
+ on-restore-snapshot
+ (mf/use-fn
+ (fn [id _event]
+ (open-restore-version-dialog :snapshot id)))
+
+ on-delete-version
(mf/use-fn
(fn [id]
(st/emit! (dwv/delete-version id))))
- handle-pin-version
+ on-pin-version
(mf/use-fn
- (fn [id]
- (st/emit! (dwv/pin-version id))))
+ (fn [id] (st/emit! (dwv/pin-version id))))
- handle-lock-version
+ on-lock-version
(mf/use-fn
(fn [id]
(st/emit! (dwv/lock-version id))))
- handle-unlock-version
+ on-unlock-version
(mf/use-fn
(fn [id]
(st/emit! (dwv/unlock-version id))))
- handle-change-filter
+ on-change-filter
(mf/use-fn
(fn [filter]
(cond
(= :all filter)
- (st/emit! (dwv/update-version-state {:filter nil}))
+ (st/emit! (dwv/update-versions-state {:filter nil}))
(= :own filter)
- (st/emit! (dwv/update-version-state {:filter (:id profile)}))
+ (st/emit! (dwv/update-versions-state {:filter (:id profile)}))
:else
- (st/emit! (dwv/update-version-state {:filter filter})))))]
+ (st/emit! (dwv/update-versions-state {:filter filter})))))
+
+ options
+ (mf/with-memo [users profile]
+ (let [current-profile-id (get profile :id)]
+ (into [{:value :all :label (tr "workspace.versions.filter.all")}
+ {:value :own :label (tr "workspace.versions.filter.mine")}]
+ (keep (fn [id]
+ (when (not= id current-profile-id)
+ (when-let [fullname (-> profiles (get id) (get :fullname))]
+ {:value id :label (tr "workspace.versions.filter.user" fullname)}))))
+ users)))]
(mf/with-effect []
- (st/emit! (dwv/init-version-state)))
+ (st/emit! (dwv/init-versions-state)))
[:div {:class (stl/css :version-toolbox)}
[:& select
{:default-value :all
:aria-label (tr "workspace.versions.filter.label")
- :options (into [{:value :all :label (tr "workspace.versions.filter.all")}
- {:value :own :label (tr "workspace.versions.filter.mine")}]
- (->> data-users
- (keep
- (fn [id]
- (let [{:keys [fullname]} (get profiles id)]
- (when (not= id (:id profile))
- {:value id :label (tr "workspace.versions.filter.user" fullname)}))))))
- :on-change handle-change-filter}]
+ :options options
+ :on-change on-change-filter}]
(cond
(= status :loading)
@@ -397,7 +394,7 @@
(tr "workspace.versions.button.save")
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.versions.button.save")
- :on-click handle-create-version
+ :on-click on-create-version
:icon "pin"}]]
(if (empty? data)
@@ -406,28 +403,26 @@
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.empty")]]
[:ul {:class (stl/css :versions-entries)}
- (for [[idx-entry entry] (->> data (map-indexed vector))]
+ (for [entry entries]
(case (:type entry)
:version
- [:& version-entry {:key idx-entry
- :entry entry
- :editing? (= (:id entry) editing)
- :profile (get profiles (:profile-id entry))
- :current-profile profile
- :on-rename-version handle-rename-version
- :on-restore-version handle-restore-version-pinned
- :on-delete-version handle-delete-version
- :on-lock-version handle-lock-version
- :on-unlock-version handle-unlock-version}]
+ [:> version-entry* {:key (:index entry)
+ :entry entry
+ :is-editing (= (:id entry) editing)
+ :current-profile profile
+ :on-edit on-edit-version
+ :on-cancel-edit on-cancel-version-edition
+ :on-rename on-rename-version
+ :on-restore on-restore-version
+ :on-delete on-delete-version
+ :on-lock on-lock-version
+ :on-unlock on-unlock-version}]
:snapshot
- [:& snapshot-entry {:key idx-entry
- :index idx-entry
- :entry entry
- :is-expanded (contains? @expanded idx-entry)
- :on-toggle-expand handle-toggle-expand
- :on-restore-snapshot handle-restore-version-snapshot
- :on-pin-snapshot handle-pin-version}]
+ [:> snapshot-entry* {:key (:index entry)
+ :entry entry
+ :on-restore-snapshot on-restore-snapshot
+ :on-pin-snapshot on-pin-version}]
nil))])