mirror of
https://github.com/penpot/penpot.git
synced 2026-01-15 18:00:21 -05:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e916c97491 | ||
|
|
cdabf0d6b9 | ||
|
|
ff43d43020 | ||
|
|
0ae8cb4979 | ||
|
|
fc1495fdd1 | ||
|
|
74622919f6 | ||
|
|
4b4b160ea8 | ||
|
|
2baab838e4 | ||
|
|
29d0499725 | ||
|
|
d99f4f62ea | ||
|
|
90f545ae6d | ||
|
|
b295b79565 | ||
|
|
ffee6c63eb | ||
|
|
0ec1bb7a22 | ||
|
|
2944860696 | ||
|
|
8d6791105a | ||
|
|
f051137098 | ||
|
|
675a31796c | ||
|
|
8dcd538bd2 | ||
|
|
384ad2e6fa | ||
|
|
c090a11e5b | ||
|
|
f6b367cdca | ||
|
|
5b9d2663c0 | ||
|
|
5e5c105d92 | ||
|
|
9c2c2fec6a | ||
|
|
56160cf64d | ||
|
|
c45a105186 | ||
|
|
f364666d48 | ||
|
|
7facd69039 | ||
|
|
b0fea30770 | ||
|
|
17015c5353 | ||
|
|
ba721def26 | ||
|
|
f9af7f0f09 | ||
|
|
56476acc19 | ||
|
|
67489c0bb9 | ||
|
|
272edec3c6 | ||
|
|
78addf00b4 | ||
|
|
2cddbc8a3d | ||
|
|
7e44ae62a2 |
@@ -1,6 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.1.0
|
||||
## 2.1.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
|
||||
|
||||
## 2.1.0 - Things can only get better!
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -54,6 +60,7 @@
|
||||
- Fix 'Detach instance' shortcut is not working [Taiga #8102](https://tree.taiga.io/project/penpot/issue/8102)
|
||||
- Fix import file message does not detect 0 as error [Taiga #6824](https://tree.taiga.io/project/penpot/issue/6824)
|
||||
- Image Color Library is not persisted when exporting/importing in .zip [Taiga #8131](https://tree.taiga.io/project/penpot/issue/8131)
|
||||
- Fix export files including libraries [Taiga #8266](https://tree.taiga.io/project/penpot/issue/8266)
|
||||
|
||||
## 2.0.3
|
||||
|
||||
|
||||
@@ -30,4 +30,10 @@
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}
|
||||
{:id "prototype-examples"
|
||||
:name "Prototipe template"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Design system example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}]
|
||||
|
||||
@@ -420,12 +420,6 @@
|
||||
|
||||
(defn- get-info
|
||||
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
|
||||
(when-let [error (get params :error)]
|
||||
(ex/raise :type :internal
|
||||
:code :error-on-retrieving-code
|
||||
:error-id error
|
||||
:error-desc (get params :error_description)))
|
||||
|
||||
(let [state (get params :state)
|
||||
code (get params :code)
|
||||
state (tokens/verify props {:token state :iss :oauth})
|
||||
@@ -609,9 +603,11 @@
|
||||
(defn- callback-handler
|
||||
[cfg request]
|
||||
(try
|
||||
(let [info (get-info cfg request)
|
||||
profile (get-profile cfg info)]
|
||||
(process-callback cfg request info profile))
|
||||
(if-let [error (dm/get-in request [:params :error])]
|
||||
(redirect-with-error "unable-to-auth" error)
|
||||
(let [info (get-info cfg request)
|
||||
profile (get-profile cfg info)]
|
||||
(process-callback cfg request info profile)))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "error on oauth process" :cause cause)
|
||||
(redirect-with-error "unable-to-auth" (ex-message cause)))))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as fval]
|
||||
[app.common.logging :as l]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -331,54 +332,12 @@
|
||||
|
||||
(defn embed-assets
|
||||
[cfg data file-id]
|
||||
(letfn [(walk-map-form [form state]
|
||||
(cond
|
||||
(uuid? (:fill-color-ref-file form))
|
||||
(do
|
||||
(vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)])
|
||||
(assoc form :fill-color-ref-file file-id))
|
||||
|
||||
(uuid? (:stroke-color-ref-file form))
|
||||
(do
|
||||
(vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)])
|
||||
(assoc form :stroke-color-ref-file file-id))
|
||||
|
||||
(uuid? (:typography-ref-file form))
|
||||
(do
|
||||
(vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)])
|
||||
(assoc form :typography-ref-file file-id))
|
||||
|
||||
(uuid? (:component-file form))
|
||||
(do
|
||||
(vswap! state conj [(:component-file form) :components (:component-id form)])
|
||||
(assoc form :component-file file-id))
|
||||
|
||||
:else
|
||||
form))
|
||||
|
||||
(process-group-of-assets [data [lib-id items]]
|
||||
;; NOTE: there is a possibility that shape refers to an
|
||||
;; non-existant file because the file was removed. In this
|
||||
;; case we just ignore the asset.
|
||||
(if-let [lib (get-file cfg lib-id)]
|
||||
(reduce (partial process-asset lib) data items)
|
||||
data))
|
||||
|
||||
(process-asset [lib data [bucket asset-id]]
|
||||
(let [asset (get-in lib [:data bucket asset-id])
|
||||
;; Add a special case for colors that need to have
|
||||
;; correctly set the :file-id prop (pending of the
|
||||
;; refactor that will remove it).
|
||||
asset (cond-> asset
|
||||
(= bucket :colors) (assoc :file-id file-id))]
|
||||
(update data bucket assoc asset-id asset)))]
|
||||
|
||||
(let [assets (volatile! [])]
|
||||
(walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data)
|
||||
(->> (deref assets)
|
||||
(filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id))))
|
||||
(d/group-by first rest)
|
||||
(reduce (partial process-group-of-assets) data)))))
|
||||
(let [library-ids (get-libraries cfg [file-id])]
|
||||
(reduce (fn [data library-id]
|
||||
(let [library (get-file cfg library-id)]
|
||||
(ctf/absorb-assets data (:data library))))
|
||||
data
|
||||
library-ids)))
|
||||
|
||||
(defn- fix-version
|
||||
[file]
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
(.writeLong output (long data))
|
||||
(swap! *position* + 8))
|
||||
|
||||
|
||||
(defn read-long!
|
||||
[^DataInputStream input]
|
||||
(let [v (.readLong input)]
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
[params]
|
||||
(d/without-nils
|
||||
{:external-session-id (::rpc/external-session-id params)
|
||||
:event-origin (::rpc/external-event-origin params)
|
||||
:triggered-by (::rpc/handler-name params)}))
|
||||
|
||||
;; --- SPECS
|
||||
@@ -147,18 +148,20 @@
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
|
||||
session-id (get params ::rpc/external-session-id)
|
||||
props (-> (or (::replace-props resultm)
|
||||
(-> params
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
session-id (get params ::rpc/external-session-id)
|
||||
event-origin (get params ::rpc/external-event-origin)
|
||||
props (-> (or (::replace-props resultm)
|
||||
(-> params
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))]
|
||||
|
||||
|
||||
@@ -343,7 +343,6 @@
|
||||
::wrk/tasks
|
||||
{:sendmail (ig/ref ::email/handler)
|
||||
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
||||
:orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler)
|
||||
:file-gc (ig/ref :app.tasks.file-gc/handler)
|
||||
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
|
||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||
@@ -388,9 +387,6 @@
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.orphan-teams-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.delete-object/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
@@ -479,9 +475,6 @@
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :objects-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :orphan-teams-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-gc-deleted}
|
||||
|
||||
|
||||
@@ -80,11 +80,13 @@
|
||||
(::actoken/profile-id request))
|
||||
|
||||
session-id (rreq/get-header request "x-external-session-id")
|
||||
event-origin (rreq/get-header request "x-event-origin")
|
||||
|
||||
data (-> params
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::external-session-id session-id)
|
||||
(assoc ::external-event-origin event-origin)
|
||||
(assoc ::session/id (::session/id request))
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
@@ -366,13 +366,13 @@
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
(declare ^:private get-owned-teams-with-participants)
|
||||
(declare ^:private get-owned-teams)
|
||||
|
||||
(sv/defmethod ::delete-profile
|
||||
{::doc/added "1.0"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [teams (get-owned-teams-with-participants conn profile-id)
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
@@ -384,37 +384,39 @@
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
|
||||
(doseq [{:keys [id]} teams]
|
||||
(db/update! conn :team
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}))
|
||||
|
||||
;; Mark profile deleted immediatelly
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
|
||||
;; Schedule cascade deletion to a worker
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :delete-object
|
||||
::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id profile-id}})
|
||||
|
||||
(rph/with-transform {} (session/delete-fn cfg)))))
|
||||
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def sql:owned-teams
|
||||
"with owner_teams as (
|
||||
select tpr.team_id as id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.is_owner is true
|
||||
and tpr.profile_id = ?
|
||||
"WITH owner_teams AS (
|
||||
SELECT tpr.team_id AS id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS TRUE
|
||||
AND tpr.profile_id = ?
|
||||
)
|
||||
select tpr.team_id as id,
|
||||
count(tpr.profile_id) - 1 as participants
|
||||
from team_profile_rel as tpr
|
||||
where tpr.team_id in (select id from owner_teams)
|
||||
and tpr.profile_id != ?
|
||||
group by 1")
|
||||
SELECT tpr.team_id AS id,
|
||||
count(tpr.profile_id) - 1 AS participants
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id from owner_teams)
|
||||
GROUP BY 1")
|
||||
|
||||
(defn- get-owned-teams-with-participants
|
||||
(defn get-owned-teams
|
||||
[conn profile-id]
|
||||
(db/exec! conn [sql:owned-teams profile-id profile-id]))
|
||||
(db/exec! conn [sql:owned-teams profile-id]))
|
||||
|
||||
(def ^:private sql:profile-existence
|
||||
"select exists (select * from profile
|
||||
|
||||
@@ -357,10 +357,12 @@
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))]
|
||||
(create-team cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:features features))))))
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
team (create-team cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:features features))]
|
||||
(with-meta team
|
||||
{::audit/props {:id (:id team)}})))))
|
||||
|
||||
(defn create-team
|
||||
"This is a complete team creation process, it creates the team
|
||||
@@ -867,7 +869,6 @@
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
(def ^:private schema:create-team-with-invitations
|
||||
@@ -881,7 +882,7 @@
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-with-invitations}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
@@ -894,7 +895,8 @@
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
team (create-team cfg params)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
emails (into #{} (map profile/clean-email) emails)
|
||||
context (audit/params->context params)]
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(->> emails
|
||||
@@ -918,6 +920,14 @@
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(audit/submit! cfg
|
||||
{::audit/type "action"
|
||||
::audit/name "create-team"
|
||||
::audit/profile-id profile-id
|
||||
::audit/props {:name name
|
||||
:features features}
|
||||
::audit/context context})
|
||||
|
||||
(audit/submit! cfg
|
||||
{::audit/type "command"
|
||||
::audit/name "create-team-invitations"
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [conn] :as cfg}
|
||||
{:keys [::rpc/profile-id token]}
|
||||
{:keys [::rpc/profile-id token] :as params}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
(us/verify! ::team-invitation-claims claims)
|
||||
@@ -169,13 +169,20 @@
|
||||
;; if we have logged-in user and it matches the invitation we proceed
|
||||
;; with accepting the invitation and joining the current profile to the
|
||||
;; invited team.
|
||||
(let [profile (accept-invitation cfg claims invitation profile)]
|
||||
(-> (assoc claims :state :created)
|
||||
(rph/with-meta {::audit/name "accept-team-invitation"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/props {:team-id (:team-id claims)
|
||||
:role (:role claims)
|
||||
:invitation-id (:id invitation)}})))
|
||||
(let [context (audit/params->context params)
|
||||
props {:team-id (:team-id claims)
|
||||
:role (:role claims)
|
||||
:invitation-id (:id invitation)}]
|
||||
|
||||
(accept-invitation cfg claims invitation profile)
|
||||
(audit/submit! cfg
|
||||
{::audit/type "action"
|
||||
::audit/name "accept-team-invitation"
|
||||
::audit/profile-id profile-id
|
||||
::audit/props props
|
||||
::audit/context context})
|
||||
|
||||
(assoc claims :state :created))
|
||||
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token
|
||||
|
||||
@@ -502,8 +502,6 @@
|
||||
|
||||
:restored)
|
||||
|
||||
|
||||
|
||||
(defn- restore-project*
|
||||
[{:keys [::db/conn] :as cfg} project-id]
|
||||
|
||||
@@ -535,6 +533,24 @@
|
||||
|
||||
:restored)
|
||||
|
||||
(defn- restore-profile*
|
||||
[{:keys [::db/conn] :as cfg} profile-id]
|
||||
(db/update! conn :profile
|
||||
{:deleted-at nil}
|
||||
{:id profile-id})
|
||||
|
||||
(doseq [{:keys [id]} (profile/get-owned-teams conn profile-id)]
|
||||
(restore-team* cfg id))
|
||||
|
||||
:restored)
|
||||
|
||||
|
||||
(defn restore-deleted-profile!
|
||||
"Mark a team and all related objects as not deleted"
|
||||
[profile-id]
|
||||
(let [profile-id (h/parse-uuid profile-id)]
|
||||
(db/tx-run! main/system restore-profile* profile-id)))
|
||||
|
||||
(defn restore-deleted-team!
|
||||
"Mark a team and all related objects as not deleted"
|
||||
[team-id]
|
||||
@@ -562,6 +578,15 @@
|
||||
(assoc ::wrk/params {:object :team
|
||||
:deleted-at (dt/now)
|
||||
:id team-id})))))
|
||||
(defn delete-profile!
|
||||
"Mark a profile for deletion"
|
||||
[profile-id]
|
||||
(let [profile-id (h/parse-uuid profile-id)]
|
||||
(wrk/invoke! (-> main/system
|
||||
(assoc ::wrk/task :delete-object)
|
||||
(assoc ::wrk/params {:object :profile
|
||||
:deleted-at (dt/now)
|
||||
:id profile-id})))))
|
||||
(defn delete-project!
|
||||
"Mark a project for deletion"
|
||||
[project-id]
|
||||
@@ -582,6 +607,15 @@
|
||||
:deleted-at (dt/now)
|
||||
:id file-id})))))
|
||||
|
||||
(defn process-deleted-profiles-cascade
|
||||
[]
|
||||
(->> (db/exec! main/system ["select id, deleted_at from profile where deleted_at is not null"])
|
||||
(run! (fn [{:keys [id deleted-at]}]
|
||||
(wrk/invoke! (-> main/system
|
||||
(assoc ::wrk/task :delete-object)
|
||||
(assoc ::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id id})))))))
|
||||
|
||||
(defn process-deleted-teams-cascade
|
||||
[]
|
||||
@@ -593,7 +627,6 @@
|
||||
:deleted-at deleted-at
|
||||
:id id})))))))
|
||||
|
||||
|
||||
(defn process-deleted-projects-cascade
|
||||
[]
|
||||
(->> (db/exec! main/system ["select id, deleted_at from project where deleted_at is not null"])
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -21,7 +23,9 @@
|
||||
(defmethod delete-object :file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})]
|
||||
(l/trc :hint "marking for deletion" :rel "file" :id (str id))
|
||||
(l/trc :hint "marking for deletion" :rel "file" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
|
||||
(db/update! conn :file
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}
|
||||
@@ -53,7 +57,9 @@
|
||||
|
||||
(defmethod delete-object :project
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "project" :id (str id))
|
||||
(l/trc :hint "marking for deletion" :rel "project" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
|
||||
(db/update! conn :project
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}
|
||||
@@ -68,7 +74,8 @@
|
||||
|
||||
(defmethod delete-object :team
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "team" :id (str id))
|
||||
(l/trc :hint "marking for deletion" :rel "team" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
(db/update! conn :team
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}
|
||||
@@ -87,6 +94,20 @@
|
||||
:object :project
|
||||
:deleted-at deleted-at)))))
|
||||
|
||||
(defmethod delete-object :profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "profile" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(doseq [team (profile/get-owned-teams conn id)]
|
||||
(delete-object cfg (assoc team
|
||||
:object :team
|
||||
:deleted-at deleted-at))))
|
||||
|
||||
(defmethod delete-object :default
|
||||
[_cfg props]
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
;; Mark as deleted the storage object
|
||||
(some->> photo-id (sto/touch-object! storage))
|
||||
|
||||
;; And finally, permanently delete the profile. The
|
||||
;; relevant objects will be deleted using DELETE
|
||||
;; CASCADE database triggers. This may leave orphan
|
||||
;; teams, but there is a special task for deleting
|
||||
;; orphaned teams.
|
||||
(db/delete! conn :profile {:id id})
|
||||
|
||||
(inc total))
|
||||
@@ -269,15 +264,15 @@
|
||||
0)))
|
||||
|
||||
(def ^:private deletion-proc-vars
|
||||
[#'delete-file-media-objects!
|
||||
[#'delete-profiles!
|
||||
#'delete-file-media-objects!
|
||||
#'delete-file-data-fragments!
|
||||
#'delete-file-object-thumbnails!
|
||||
#'delete-file-thumbnails!
|
||||
#'delete-files!
|
||||
#'delete-projects!
|
||||
#'delete-fonts!
|
||||
#'delete-teams!
|
||||
#'delete-profiles!])
|
||||
#'delete-teams!])
|
||||
|
||||
(defn- execute-proc!
|
||||
"A generic function that executes the specified proc iterativelly
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
;; 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.tasks.orphan-teams-gc
|
||||
"A maintenance task that performs orphan teams GC."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:get-orphan-teams
|
||||
"SELECT t.id
|
||||
FROM team AS t
|
||||
LEFT JOIN team_profile_rel AS tpr
|
||||
ON (t.id = tpr.team_id)
|
||||
WHERE tpr.profile_id IS NULL
|
||||
AND t.deleted_at IS NULL
|
||||
ORDER BY t.created_at ASC
|
||||
FOR UPDATE OF t
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-orphan-teams
|
||||
"Find all orphan teams (with no members) and mark them for
|
||||
deletion (soft delete)."
|
||||
[{:keys [::db/conn] :as cfg}]
|
||||
(let [deleted-at (dt/now)]
|
||||
(->> (db/cursor conn sql:get-orphan-teams)
|
||||
(map :id)
|
||||
(reduce (fn [total team-id]
|
||||
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
|
||||
|
||||
(db/update! conn :team
|
||||
{:deleted-at deleted-at}
|
||||
{:id team-id})
|
||||
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :delete-object)
|
||||
(assoc ::wrk/params {:object :team
|
||||
:deleted-at deleted-at
|
||||
:id team-id})))
|
||||
(inc total))
|
||||
0))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(l/inf :hint "gc started" :rollback? (boolean (:rollback? props)))
|
||||
(let [total (delete-orphan-teams cfg)]
|
||||
(l/inf :hint "task finished"
|
||||
:teams total
|
||||
:rollback? (boolean (:rollback? props)))
|
||||
|
||||
(when (:rollback? props)
|
||||
(db/rollback! conn))
|
||||
|
||||
{:processed total})))))
|
||||
@@ -127,7 +127,7 @@
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))))))
|
||||
|
||||
(t/deftest profile-deletion-simple
|
||||
(t/deftest profile-deletion-1
|
||||
(let [prof (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
@@ -153,23 +153,22 @@
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (count (:result out)))))
|
||||
|
||||
;; execute permanent deletion task
|
||||
(let [result (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (= 1 (:processed result))))
|
||||
|
||||
(let [row (th/db-get :team
|
||||
{:id (:default-team-id prof)}
|
||||
{::db/remove-deleted false})]
|
||||
(t/is (nil? (:deleted-at row))))
|
||||
|
||||
(let [result (th/run-task! :orphan-teams-gc {:min-age 0})]
|
||||
(t/is (= 1 (:processed result))))
|
||||
(th/run-pending-tasks!)
|
||||
|
||||
(let [row (th/db-get :team
|
||||
{:id (:default-team-id prof)}
|
||||
{::db/remove-deleted false})]
|
||||
(t/is (dt/instant? (:deleted-at row))))
|
||||
|
||||
;; execute permanent deletion task
|
||||
(let [result (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (= 4 (:processed result))))
|
||||
|
||||
(let [row (th/db-get :team
|
||||
{:id (:default-team-id prof)}
|
||||
{::db/remove-deleted false})]
|
||||
(t/is (nil? row)))
|
||||
|
||||
;; query profile after delete
|
||||
(let [params {::th/type :get-profile
|
||||
::rpc/profile-id (:id prof)}
|
||||
@@ -178,6 +177,178 @@
|
||||
(let [result (:result out)]
|
||||
(t/is (= uuid/zero (:id result)))))))
|
||||
|
||||
(t/deftest profile-deletion-2
|
||||
(let [prof1 (th/create-profile* 1)
|
||||
prof2 (th/create-profile* 2)
|
||||
file1 (th/create-file* 1 {:profile-id (:id prof1)
|
||||
:project-id (:default-project-id prof1)
|
||||
:is-shared false})
|
||||
team1 (th/create-team* 1 {:profile-id (:id prof1)})
|
||||
|
||||
role1 (th/create-team-role* {:team-id (:id team1)
|
||||
:profile-id (:id prof2)
|
||||
|
||||
:role :editor})]
|
||||
;; Assert all roles for team
|
||||
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
|
||||
(t/is (= 2 (count roles))))
|
||||
|
||||
;; Request profile to be deleted
|
||||
(let [params {::th/type :delete-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
|
||||
(let [error (:error out)
|
||||
edata (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type edata) :validation))
|
||||
(t/is (= (:code edata) :owner-teams-with-people))))))
|
||||
|
||||
(t/deftest profile-deletion-3
|
||||
(let [prof1 (th/create-profile* 1)
|
||||
prof2 (th/create-profile* 2)
|
||||
prof3 (th/create-profile* 3)
|
||||
file1 (th/create-file* 1 {:profile-id (:id prof1)
|
||||
:project-id (:default-project-id prof1)
|
||||
:is-shared false})
|
||||
team1 (th/create-team* 1 {:profile-id (:id prof1)})
|
||||
|
||||
role1 (th/create-team-role* {:team-id (:id team1)
|
||||
:profile-id (:id prof2)
|
||||
:role :editor})
|
||||
role2 (th/create-team-role* {:team-id (:id team1)
|
||||
:profile-id (:id prof3)
|
||||
:role :editor})]
|
||||
|
||||
;; Assert all roles for team
|
||||
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
|
||||
(t/is (= 3 (count roles))))
|
||||
|
||||
;; Request profile to be deleted (it should fail)
|
||||
(let [params {::th/type :delete-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
|
||||
(let [error (:error out)
|
||||
edata (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type edata) :validation))
|
||||
(t/is (= (:code edata) :owner-teams-with-people))))
|
||||
|
||||
;; Leave team by role 1
|
||||
(let [params {::th/type :leave-team
|
||||
::rpc/profile-id (:id prof2)
|
||||
:id (:id team1)}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; Request profile to be deleted (it should fail)
|
||||
(let [params {::th/type :delete-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(let [error (:error out)
|
||||
edata (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type edata) :validation))
|
||||
(t/is (= (:code edata) :owner-teams-with-people))))
|
||||
|
||||
;; Leave team by role 0 (the default) and reassing owner to role 3
|
||||
;; without reassinging it (should fail)
|
||||
(let [params {::th/type :leave-team
|
||||
::rpc/profile-id (:id prof1)
|
||||
;; :reassign-to (:id prof3)
|
||||
:id (:id team1)}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
|
||||
(let [error (:error out)
|
||||
edata (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type edata) :validation))
|
||||
(t/is (= (:code edata) :owner-cant-leave-team))))
|
||||
|
||||
;; Leave team by role 0 (the default) and reassing owner to role 3
|
||||
(let [params {::th/type :leave-team
|
||||
::rpc/profile-id (:id prof1)
|
||||
:reassign-to (:id prof3)
|
||||
:id (:id team1)}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; Request profile to be deleted
|
||||
(let [params {::th/type :delete-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; query files after profile soft deletion
|
||||
(let [params {::th/type :get-project-files
|
||||
::rpc/profile-id (:id prof1)
|
||||
:project-id (:default-project-id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (count (:result out)))))
|
||||
|
||||
(th/run-pending-tasks!)
|
||||
|
||||
;; execute permanent deletion task
|
||||
(let [result (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (= 4 (:processed result))))
|
||||
|
||||
(let [row (th/db-get :team
|
||||
{:id (:default-team-id prof1)}
|
||||
{::db/remove-deleted false})]
|
||||
(t/is (nil? row)))
|
||||
|
||||
;; query profile after delete
|
||||
(let [params {::th/type :get-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(let [result (:result out)]
|
||||
(t/is (= uuid/zero (:id result)))))))
|
||||
|
||||
|
||||
(t/deftest profile-deletion-4
|
||||
(let [prof1 (th/create-profile* 1)
|
||||
file1 (th/create-file* 1 {:profile-id (:id prof1)
|
||||
:project-id (:default-project-id prof1)
|
||||
:is-shared false})
|
||||
team1 (th/create-team* 1 {:profile-id (:id prof1)})
|
||||
team2 (th/create-team* 2 {:profile-id (:id prof1)})]
|
||||
|
||||
;; Request profile to be deleted
|
||||
(let [params {::th/type :delete-profile
|
||||
::rpc/profile-id (:id prof1)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
(th/run-pending-tasks!)
|
||||
|
||||
(let [rows (th/db-exec! ["select id,name,deleted_at from team where deleted_at is not null"])]
|
||||
(t/is (= 3 (count rows))))
|
||||
|
||||
;; execute permanent deletion task
|
||||
(let [result (th/run-task! :objects-gc {:min-age 0})]
|
||||
(t/is (= 8 (:processed result))))))
|
||||
|
||||
|
||||
(t/deftest email-blacklist-1
|
||||
(t/is (false? (email.blacklist/enabled? th/*system*)))
|
||||
(t/is (true? (email.blacklist/enabled? (assoc th/*system* :app.email/blacklist []))))
|
||||
|
||||
@@ -633,19 +633,24 @@
|
||||
"Find all assets of a library that are used in the file, and
|
||||
move them to the file local library."
|
||||
[file-data library-data]
|
||||
(let [used-components (find-asset-type-usages file-data library-data :component)
|
||||
used-colors (find-asset-type-usages file-data library-data :color)
|
||||
used-typographies (find-asset-type-usages file-data library-data :typography)]
|
||||
(let [used-components (find-asset-type-usages file-data library-data :component)
|
||||
file-data (cond-> file-data
|
||||
(d/not-empty? used-components)
|
||||
(absorb-components used-components library-data))
|
||||
;; Note that absorbed components may also be using colors
|
||||
;; and typographies. This is the reason of doing this first
|
||||
;; and accumulating file data for the next ones.
|
||||
|
||||
(cond-> file-data
|
||||
(d/not-empty? used-components)
|
||||
(absorb-components used-components library-data)
|
||||
used-colors (find-asset-type-usages file-data library-data :color)
|
||||
file-data (cond-> file-data
|
||||
(d/not-empty? used-colors)
|
||||
(absorb-colors used-colors))
|
||||
|
||||
(d/not-empty? used-colors)
|
||||
(absorb-colors used-colors)
|
||||
|
||||
(d/not-empty? used-typographies)
|
||||
(absorb-typographies used-typographies))))
|
||||
used-typographies (find-asset-type-usages file-data library-data :typography)
|
||||
file-data (cond-> file-data
|
||||
(d/not-empty? used-typographies)
|
||||
(absorb-typographies used-typographies))]
|
||||
file-data))
|
||||
|
||||
;; Debug helpers
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 318 KiB |
@@ -299,7 +299,19 @@
|
||||
(ptk/reify ::libraries-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :builtin-templates libraries))))
|
||||
(let [templates-a-b-test? (cf/external-feature-flag "dashboard-01" "test")
|
||||
remove-ids (if templates-a-b-test?
|
||||
#{"wireframing-kit" "prototype-examples" "plants-app" "penpot-design-system"}
|
||||
#{"prototype-examples" "penpot-design-system"})
|
||||
libraries (cond->> libraries
|
||||
:always
|
||||
(remove #(contains? remove-ids (:id %)))
|
||||
templates-a-b-test?
|
||||
(concat [{:id "wireframing-kit", :name "Wireframe library"}
|
||||
{:id "prototype-examples", :name "Prototype template"}
|
||||
{:id "plants-app", :name "UI mockup example"}
|
||||
{:id "penpot-design-system", :name "Design system example"}]))]
|
||||
(assoc state :builtin-templates libraries)))))
|
||||
|
||||
(defn fetch-builtin-templates
|
||||
[]
|
||||
@@ -405,12 +417,13 @@
|
||||
(dm/assert! (string? name))
|
||||
(ptk/reify ::create-team
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(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)]
|
||||
(->> (rp/cmd! :create-team {:name name :features features})
|
||||
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))))))
|
||||
@@ -421,7 +434,7 @@
|
||||
[{:keys [name emails role] :as params}]
|
||||
(ptk/reify ::create-team-with-invitations
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(watch [it state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
@@ -430,7 +443,7 @@
|
||||
:emails emails
|
||||
:role role
|
||||
:features features}]
|
||||
(->> (rp/cmd! :create-team-with-invitations params)
|
||||
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
|
||||
(rx/tap on-success)
|
||||
(rx/map team-created)
|
||||
(rx/catch on-error))))))
|
||||
@@ -553,12 +566,12 @@
|
||||
:resend resend?})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(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 params)
|
||||
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
@@ -569,10 +569,9 @@
|
||||
on-success identity}} (meta params)]
|
||||
(->> (rp/cmd! :delete-profile {})
|
||||
(rx/tap on-success)
|
||||
(rx/delay-at-least 300)
|
||||
(rx/catch (constantly (rx/of 1)))
|
||||
(rx/map logged-out)
|
||||
(rx/catch on-error))))))
|
||||
(rx/catch on-error)
|
||||
(rx/delay-at-least 300))))))
|
||||
|
||||
;; --- EVENT: request-profile-recovery
|
||||
|
||||
@@ -697,15 +696,20 @@
|
||||
(ptk/reify ::show-redirect-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(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")
|
||||
:else
|
||||
(tr "errors.generic"))]
|
||||
(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 (msg/warn hint))))))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as-alias ev]
|
||||
[app.util.http :as http]
|
||||
[app.util.sse :as sse]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -93,12 +94,12 @@
|
||||
(= query-params :all) :get
|
||||
(str/starts-with? nid "get-") :get
|
||||
:else :post)
|
||||
|
||||
request {:method method
|
||||
:uri (u/join cf/public-uri "api/rpc/command/" nid)
|
||||
:credentials "include"
|
||||
:headers {"accept" "application/transit+json,text/event-stream,*/*"
|
||||
"x-external-session-id" (cf/external-session-id)}
|
||||
"x-external-session-id" (cf/external-session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
:body (when (= method :post)
|
||||
(if form-data?
|
||||
(http/form-data params)
|
||||
@@ -137,7 +138,8 @@
|
||||
(->> (http/send! {:method :post
|
||||
:uri uri
|
||||
:credentials "include"
|
||||
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||
:headers {"x-external-session-id" (cf/external-session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
:query params})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response))))
|
||||
@@ -147,7 +149,8 @@
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join cf/public-uri "api/export")
|
||||
:body (http/transit-data (dissoc params :blob?))
|
||||
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||
:headers {"x-external-session-id" (cf/external-session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
:credentials "include"
|
||||
:response-type (if blob? :blob :text)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
@@ -167,7 +170,8 @@
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
|
||||
:credentials "include"
|
||||
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||
:headers {"x-external-session-id" (cf/external-session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
:body (http/form-data params)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
@@ -70,27 +70,28 @@
|
||||
(rx/subs!
|
||||
(fn [tdata]
|
||||
(handle-token tdata))
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(or (= :validation type)
|
||||
(= :invalid-token code)
|
||||
(= :token-expired (:reason error)))
|
||||
(reset! bad-token true)
|
||||
(fn [cause]
|
||||
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||
(cond
|
||||
(or (= :validation type)
|
||||
(= :invalid-token code)
|
||||
(= :token-expired (:reason error)))
|
||||
(reset! bad-token true)
|
||||
|
||||
(= :email-already-exists code)
|
||||
(let [msg (tr "errors.email-already-exists")]
|
||||
(ts/schedule 100 #(st/emit! (msg/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
(= :email-already-exists code)
|
||||
(let [msg (tr "errors.email-already-exists")]
|
||||
(ts/schedule 100 #(st/emit! (msg/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(= :email-already-validated code)
|
||||
(let [msg (tr "errors.email-already-validated")]
|
||||
(ts/schedule 100 #(st/emit! (msg/warn msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
(= :email-already-validated code)
|
||||
(let [msg (tr "errors.email-already-validated")]
|
||||
(ts/schedule 100 #(st/emit! (msg/warn msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
:else
|
||||
(let [msg (tr "errors.generic")]
|
||||
(ts/schedule 100 #(st/emit! (msg/error msg)))
|
||||
(st/emit! (rt/nav :auth-login))))))))
|
||||
:else
|
||||
(let [msg (tr "errors.generic")]
|
||||
(ts/schedule 100 #(st/emit! (msg/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))))))))
|
||||
|
||||
(if @bad-token
|
||||
[:> static/invalid-token {}]
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
(mf/defc radio-buttons
|
||||
{::mf/props :obj}
|
||||
[{:keys [children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
|
||||
[{:keys [name children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
|
||||
(let [encode-fn (d/nilv encode-fn identity)
|
||||
decode-fn (d/nilv decode-fn identity)
|
||||
nitems (if (array? children)
|
||||
@@ -94,5 +94,6 @@
|
||||
|
||||
[:& (mf/provider context) {:value context-value}
|
||||
[:div {:class (dm/str class " " (stl/css :radio-btn-wrapper))
|
||||
:style {:width width}}
|
||||
:style {:width width}
|
||||
:key (dm/str name "-" selected)}
|
||||
children]]))
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
(mf/defc loading-placeholder
|
||||
[]
|
||||
[:div {:class (stl/css :grid-empty-placeholder :loader)}
|
||||
[:div {:class (stl/css :icon)} i/loader]
|
||||
[:div {:class (stl/css :icon)} i/loader-pencil]
|
||||
[:div {:class (stl/css :text)} (tr "dashboard.loading-files")]])
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
svg {
|
||||
width: $s-64;
|
||||
height: $s-64;
|
||||
stroke: $df-secondary;
|
||||
fill: none;
|
||||
fill: $df-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
@@ -51,7 +52,8 @@
|
||||
(let [mdata {:on-success (partial on-create-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params {:name (get-in @form [:clean-data :name])}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata)))))
|
||||
(st/emit! (-> (dd/create-team (with-meta params mdata))
|
||||
(with-meta {::ev/origin :dashboard})))))
|
||||
|
||||
(defn- on-update-submit
|
||||
[form]
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}
|
||||
params {:name name}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))
|
||||
(st/emit! (-> (dd/create-team (with-meta params mdata))
|
||||
(with-meta {::ev/origin :onboarding-without-invitations}))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:create-team-and-invite-later"
|
||||
@@ -115,7 +116,8 @@
|
||||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
|
||||
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))
|
||||
(st/emit! (-> (dd/create-team-with-invitations (with-meta params mdata))
|
||||
(with-meta {::ev/origin :onboarding-with-invitations}))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
:label "team:create-team-and-invite"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[app.main.ui.releases.v1-8]
|
||||
[app.main.ui.releases.v1-9]
|
||||
[app.main.ui.releases.v2-0]
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -91,4 +92,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.0")))
|
||||
(rc/render-release-notes (assoc params :version "2.1")))
|
||||
|
||||
48
frontend/src/app/main/ui/releases/v2_1.cljs
Normal file
48
frontend/src/app/main/ui/releases/v2_1.cljs
Normal file
@@ -0,0 +1,48 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.releases.v2-1
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.1"
|
||||
[{:keys [slide klass finish version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-intro-image.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What's new in Penpot? "]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Penpot 2.1 brings improvements to the authentication system, path editing, real-time persistence, and comments system among other enhancements. We’ve improved the stability of the platform by fixing a bunch of bugs, a lot of them raised by our amazing community <3."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This minor release comes shortly after our amazing Penpot 2.0 and it shows the way to long-expected capabilities like the incoming new plugin system!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
" Ready to dive in? Let 's get started!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click finish} "Let's go"]]]]]])))
|
||||
|
||||
79
frontend/src/app/main/ui/releases/v2_1.scss
Normal file
79
frontend/src/app/main/ui/releases/v2_1.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: $s-324 1fr;
|
||||
height: $s-500;
|
||||
width: $s-888;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: $s-324;
|
||||
border-radius: $br-8 0 0 $br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: $s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr $s-32;
|
||||
gap: $s-24;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include flexCenter;
|
||||
@include headlineSmallTypography;
|
||||
height: $s-32;
|
||||
width: $s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
width: $s-440;
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: $s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -18,11 +18,12 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn on-error
|
||||
[{:keys [code] :as error}]
|
||||
(if (= :owner-teams-with-people code)
|
||||
(let [msg (tr "notifications.profile-deletion-not-allowed")]
|
||||
(rx/of (msg/error msg)))
|
||||
(rx/throw error)))
|
||||
[cause]
|
||||
(let [code (-> cause ex-data :code)]
|
||||
(if (= :owner-teams-with-people code)
|
||||
(let [msg (tr "notifications.profile-deletion-not-allowed")]
|
||||
(rx/of (msg/error msg)))
|
||||
(rx/throw cause))))
|
||||
|
||||
(mf/defc delete-account-modal
|
||||
{::mf/register modal/components
|
||||
|
||||
Reference in New Issue
Block a user