diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 22b9b08a1a..f6b262eecc 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -26,6 +26,7 @@ [app.db.sql :as-alias sql] [app.features.fdata :as feat.fdata] [app.features.logical-deletion :as ldel] + [app.http.sse :as sse] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.msgbus :as mbus] @@ -38,6 +39,7 @@ [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] [app.util.blob :as blob] + [app.util.events :as events] [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.worker :as wrk] @@ -353,9 +355,8 @@ ::sm/params schema:get-project-files ::sm/result schema:files} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}] - (dm/with-open [conn (db/open pool)] - (projects/check-read-permissions! conn profile-id project-id) - (get-project-files conn project-id))) + (projects/check-read-permissions! pool profile-id project-id) + (get-project-files pool project-id)) ;; --- COMMAND QUERY: has-file-libraries @@ -424,7 +425,6 @@ ;; --- QUERY COMMAND: get-page - (defn- prune-objects "Given the page data and the object-id returns the page data with all other not needed objects removed from the `:objects` data @@ -765,6 +765,54 @@ (teams/check-read-permissions! conn profile-id team-id) (get-team-recent-files conn team-id))) + +;; --- COMMAND QUERY: get-team-deleted-files + +(def sql:team-deleted-files + "WITH deleted_files AS ( + SELECT f.id, + f.revn, + f.vern, + f.project_id, + f.created_at, + f.modified_at, + f.name, + f.is_shared, + f.deleted_at AS will_be_deleted_at, + ft.media_id AS thumbnail_id, + row_number() OVER w AS row_num, + p.team_id + FROM file AS f + INNER JOIN project AS p ON (p.id = f.project_id) + LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id + AND ft.revn = f.revn + AND ft.deleted_at is null) + WHERE p.team_id = ? + AND (p.deleted_at > ?::timestamptz OR + f.deleted_at > ?::timestamptz) + WINDOW w AS (PARTITION BY f.project_id + ORDER BY f.modified_at DESC) + ORDER BY f.modified_at DESC + ) + SELECT * FROM deleted_files") + +(defn get-team-deleted-files + [conn team-id] + (let [now (ct/now)] + (db/exec! conn [sql:team-deleted-files team-id now now]))) + +(def ^:private schema:get-team-deleted-files + [:map {:title "get-team-deleted-files"} + [:team-id ::sm/uuid]]) + +(sv/defmethod ::get-team-deleted-files + {::doc/added "2.12" + ::sm/params schema:get-team-deleted-files} + [cfg {:keys [::rpc/profile-id team-id]}] + (db/run! cfg (fn [{:keys [::db/conn]}] + (teams/check-read-permissions! conn profile-id team-id) + (get-team-deleted-files conn team-id)))) + ;; --- COMMAND QUERY: get-file-info @@ -1113,3 +1161,118 @@ (check-edition-permissions! conn profile-id file-id) (-> (ignore-sync conn params) (update :features db/decode-pgarray #{}))) + +;; --- MUTATION COMMAND: delete-files-immediatelly + +(def ^:private sql:delete-team-files + "UPDATE file AS uf SET deleted_at = ?::timestamptz + FROM ( + SELECT f.id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + JOIN team AS t ON (t.id = p.team_id) + WHERE t.deleted_at IS NULL + AND t.id = ? + AND f.id = ANY(?::uuid[]) + ) AS subquery + WHERE uf.id = subquery.id + RETURNING uf.id, uf.deleted_at;") + +(def ^:private schema:permanently-delete-team-files + [:map {:title "permanently-delete-team-files"} + [:team-id ::sm/uuid] + [:ids [::sm/set ::sm/uuid]]]) + +(sv/defmethod ::permanently-delete-team-files + "Mark the specified files to be deleted immediatelly on the + specified team. The team-id on params will be used to filter and + check writable permissons on team." + + {::doc/added "2.12" + ::sm/params schema:permanently-delete-team-files + ::db/transaction true} + + [{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}] + (teams/check-edition-permissions! conn profile-id team-id) + + (reduce (fn [acc {:keys [id deleted-at]}] + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :file + :deleted-at deleted-at + :id id}}) + (conj acc id)) + #{} + (db/plan conn [sql:delete-team-files request-at team-id + (db/create-array conn "uuid" ids)]))) + +;; --- MUTATION COMMAND: restore-files-immediatelly + +(def ^:private sql:resolve-editable-files + "SELECT f.id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + JOIN team AS t ON (t.id = p.team_id) + WHERE t.deleted_at IS NULL + AND t.id = ? + AND f.id = ANY(?::uuid[])") + +(defn- restore-file + [conn file-id] + (db/update! conn :file + {:deleted-at nil + :has-media-trimmed false} + {:id file-id} + {::db/return-keys false}) + + (db/update! conn :file-media-object + {:deleted-at nil} + {:file-id file-id} + {::db/return-keys false}) + + (db/update! conn :file-change + {:deleted-at nil} + {:file-id file-id} + {::db/return-keys false}) + + (db/update! conn :file-data + {:deleted-at nil} + {:file-id file-id} + {::db/return-keys false}) + + (db/update! conn :file-thumbnail + {:deleted-at nil} + {:file-id file-id} + {::db/return-keys false}) + + (db/update! conn :file-tagged-object-thumbnail + {:deleted-at nil} + {:file-id file-id} + {::db/return-keys false})) + +(defn- restore-deleted-team-files + [{:keys [::db/conn]} {:keys [::rpc/profile-id team-id ids]}] + (teams/check-edition-permissions! conn profile-id team-id) + + (reduce (fn [affected {:keys [id]}] + (let [index (inc (count affected))] + (events/tap :progress {:file-id id :index index :total (count ids)}) + (restore-file conn id) + (conj affected id))) + #{} + (db/plan conn [sql:resolve-editable-files team-id + (db/create-array conn "uuid" ids)]))) + +(def ^:private schema:restore-deleted-team-files + [:map {:title "restore-deleted-team-files"} + [:team-id ::sm/uuid] + [:ids [::sm/set ::sm/uuid]]]) + +(sv/defmethod ::restore-deleted-team-files + "Removes the deletion mark from the specified files (and respective projects)." + + {::doc/added "2.12" + ::sse/stream? true + ::sm/params schema:restore-deleted-team-files} + [cfg params] + (sse/response #(db/tx-run! cfg restore-deleted-team-files params))) diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index 3684824bc5..c99c4f27d7 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -70,7 +70,27 @@ ;; --- QUERY: Get projects -(declare get-projects) +(def ^:private sql:projects + "SELECT p.*, + coalesce(tpp.is_pinned, false) as is_pinned, + (SELECT count(*) FROM file AS f + WHERE f.project_id = p.id + AND f.deleted_at is null) AS count, + (SELECT count(*) FROM file AS f + WHERE f.project_id = p.id) AS total_count + FROM project AS p + INNER JOIN team AS t ON (t.id = p.team_id) + LEFT JOIN team_project_profile_rel AS tpp + ON (tpp.project_id = p.id AND + tpp.team_id = p.team_id AND + tpp.profile_id = ?) + WHERE p.team_id = ? + AND t.deleted_at is null + ORDER BY p.modified_at DESC") + +(defn get-projects + [conn profile-id team-id] + (db/exec! conn [sql:projects profile-id team-id])) (def ^:private schema:get-projects [:map {:title "get-projects"} @@ -78,32 +98,11 @@ (sv/defmethod ::get-projects {::doc/added "1.18" + ::doc/changes [["2.12" "This endpoint now return deleted but recoverable projects"]] ::sm/params schema:get-projects} - [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] - (dm/with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (get-projects conn profile-id team-id))) - -(def sql:projects - "select p.*, - coalesce(tpp.is_pinned, false) as is_pinned, - (select count(*) from file as f - where f.project_id = p.id - and deleted_at is null) as count - from project as p - inner join team as t on (t.id = p.team_id) - left join team_project_profile_rel as tpp - on (tpp.project_id = p.id and - tpp.team_id = p.team_id and - tpp.profile_id = ?) - where p.team_id = ? - and p.deleted_at is null - and t.deleted_at is null - order by p.modified_at desc") - -(defn get-projects - [conn profile-id team-id] - (db/exec! conn [sql:projects profile-id team-id])) + [cfg {:keys [::rpc/profile-id team-id]}] + (teams/check-read-permissions! cfg profile-id team-id) + (get-projects cfg profile-id team-id)) ;; --- QUERY: Get all projects diff --git a/backend/src/app/setup/clock.clj b/backend/src/app/setup/clock.clj index 09ab991856..f73f6f8501 100644 --- a/backend/src/app/setup/clock.clj +++ b/backend/src/app/setup/clock.clj @@ -14,7 +14,9 @@ [integrant.core :as ig]) (:import java.time.Clock - java.time.Duration)) + java.time.Duration + java.time.Instant + java.time.ZoneId)) (defonce current (atom {:clock (Clock/systemDefaultZone) @@ -36,6 +38,12 @@ [_ _] (remove-watch current ::common)) +(defn fixed + "Get fixed clock, mainly used in tests" + [instant] + (Clock/fixed ^Instant (ct/inst instant) + ^ZoneId (ZoneId/of "Z"))) + (defn set-offset! [duration] (swap! current assoc :offset (some-> duration ct/duration))) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index f2286013aa..51832cefac 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -567,48 +567,12 @@ :id file-id}))) :deleted)) -(defn- restore-file* - [{:keys [::db/conn]} file-id] - (db/update! conn :file - {:deleted-at nil - :has-media-trimmed false} - {:id file-id} - {::db/return-keys false}) - - (db/update! conn :file-media-object - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-change - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-data - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - ;; Mark thumbnails to be deleted - (db/update! conn :file-thumbnail - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-tagged-object-thumbnail - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - :restored) - (defn restore-file! "Mark a file and all related objects as not deleted" [file-id] (let [file-id (h/parse-uuid file-id)] (db/tx-run! main/system - (fn [system] + (fn [{:keys [::db/conn] :as system}] (when-let [file (db/get* system :file {:id file-id} {::db/remove-deleted false @@ -622,7 +586,9 @@ :cause "explicit call to restore-file!"} ::audit/tracked-at (ct/now)}) - (restore-file* system file-id)))))) + + (#'files/restore-file conn file-id)) + :restored)))) (defn delete-project! "Mark a project for deletion" @@ -655,7 +621,7 @@ (doseq [{:keys [id]} (db/query conn :file {:project-id project-id} {::sql/columns [:id]})] - (restore-file* cfg id)) + (#'files/restore-file conn id)) :restored) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index f8701a0c1c..d34077bc2d 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -19,6 +19,7 @@ [app.http :as http] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.setup.clock :as clock] [app.storage :as sto] [backend-tests.helpers :as th] [clojure.test :as t] @@ -142,126 +143,112 @@ (t/is (= 0 (count result)))))))) (t/deftest file-gc-with-fragments - (letfn [(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] - (let [params {::th/type :update-file - ::rpc/profile-id profile-id - :id file-id - :session-id (uuid/random) - :revn revn - :vern 0 - :features cfeat/supported-features - :changes changes} - out (th/command! params)] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (:result out)))] + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) - (let [profile (th/create-profile* 1) - file (th/create-file* 1 {:profile-id (:id profile) - :project-id (:default-project-id profile) - :is-shared false}) + page-id (uuid/random) + shape-id (uuid/random)] - page-id (uuid/random) - shape-id (uuid/random)] + ;; Preventive file-gc + (t/is (true? (th/run-task! :file-gc {:file-id (:id file) :revn (:revn file)}))) - ;; Preventive file-gc - (t/is (true? (th/run-task! :file-gc {:file-id (:id file) :revn (:revn file)}))) + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 2 (count rows)))) - ;; Check the number of fragments before adding the page - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 2 (count rows)))) + ;; Add page + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :vern 0 + :changes + [{:type :add-page + :name "test" + :id page-id}]) - ;; Add page - (update-file! - :file-id (:id file) - :profile-id (:id profile) - :revn 0 - :vern 0 - :changes - [{:type :add-page - :name "test" - :id page-id}]) + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 3 (count rows)))) - ;; Check the number of fragments before adding the page - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 3 (count rows)))) + ;; The file-gc should mark for remove unused fragments + (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) - ;; The file-gc should mark for remove unused fragments - (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) + ;; Check the number of fragments + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 5 (count rows))) + (t/is (= 3 (count (filterv :deleted-at rows))))) - ;; Check the number of fragments - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 5 (count rows))) - (t/is (= 3 (count (filterv :deleted-at rows))))) + ;; The objects-gc should remove unused fragments + (let [res (th/run-task! :objects-gc {})] + (t/is (= 3 (:processed res)))) - ;; The objects-gc should remove unused fragments - (let [res (th/run-task! :objects-gc {})] - (t/is (= 3 (:processed res)))) + ;; Check the number of fragments + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 2 (count rows)))) - ;; Check the number of fragments - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 2 (count rows)))) + ;; Add shape to page that should add a new fragment + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :vern 0 + :changes + [{:type :add-obj + :page-id page-id + :id shape-id + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id shape-id + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :rect})}]) - ;; Add shape to page that should add a new fragment - (update-file! - :file-id (:id file) - :profile-id (:id profile) - :revn 0 - :vern 0 - :changes - [{:type :add-obj - :page-id page-id - :id shape-id - :parent-id uuid/zero - :frame-id uuid/zero - :components-v2 true - :obj (cts/setup-shape - {:id shape-id - :name "image" - :frame-id uuid/zero - :parent-id uuid/zero - :type :rect})}]) + ;; Check the number of fragments + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 3 (count rows)))) - ;; Check the number of fragments - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 3 (count rows)))) + ;; The file-gc should mark for remove unused fragments + (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) - ;; The file-gc should mark for remove unused fragments - (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) + ;; The objects-gc should remove unused fragments + (let [res (th/run-task! :objects-gc {})] + (t/is (= 3 (:processed res)))) - ;; The objects-gc should remove unused fragments - (let [res (th/run-task! :objects-gc {})] - (t/is (= 3 (:processed res)))) + ;; Check the number of fragments; + (let [rows (th/db-query :file-data {:file-id (:id file) + :type "fragment" + :deleted-at nil})] + (t/is (= 2 (count rows)))) - ;; Check the number of fragments; - (let [rows (th/db-query :file-data {:file-id (:id file) - :type "fragment" - :deleted-at nil})] - (t/is (= 2 (count rows)))) + ;; Lets proceed to delete all changes + (th/db-delete! :file-change {:file-id (:id file)}) + (th/db-delete! :file-data {:file-id (:id file) :type "snapshot"}) - ;; Lets proceed to delete all changes - (th/db-delete! :file-change {:file-id (:id file)}) - (th/db-delete! :file-data {:file-id (:id file) :type "snapshot"}) + (th/db-update! :file + {:has-media-trimmed false} + {:id (:id file)}) - (th/db-update! :file - {:has-media-trimmed false} - {:id (:id file)}) + ;; The file-gc should remove fragments related to changes + ;; snapshots previously deleted. + (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) - ;; The file-gc should remove fragments related to changes - ;; snapshots previously deleted. - (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))) + ;; Check the number of fragments; + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + ;; (pp/pprint rows) + (t/is (= 4 (count rows))) + (t/is (= 2 (count (remove :deleted-at rows))))) - ;; Check the number of fragments; - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - ;; (pp/pprint rows) - (t/is (= 4 (count rows))) - (t/is (= 2 (count (remove :deleted-at rows))))) + (let [res (th/run-task! :objects-gc {})] + (t/is (= 2 (:processed res)))) - (let [res (th/run-task! :objects-gc {})] - (t/is (= 2 (:processed res)))) - - (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] - (t/is (= 2 (count rows))))))) + (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})] + (t/is (= 2 (count rows)))))) (t/deftest file-gc-with-thumbnails (letfn [(add-file-media-object [& {:keys [profile-id file-id]}] @@ -279,20 +266,6 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) - (:result out))) - - (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] - (let [params {::th/type :update-file - ::rpc/profile-id profile-id - :id file-id - :session-id (uuid/random) - :revn revn - :vern 0 - :features cfeat/supported-features - :changes changes} - out (th/command! params)] - ;; (th/print-result! out) - (t/is (nil? (:error out))) (:result out)))] (let [storage (:app.storage/storage th/*system*) @@ -1893,3 +1866,125 @@ (t/is (= (:id file-2) (:file-id (get rows 0)))) (t/is (nil? (:deleted-at (get rows 0))))))) + +(t/deftest deleted-files-permanently-delete + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + file-id (uuid/next) + now (ct/inst "2025-10-31T00:00:00Z")] + + (binding [ct/*clock* (clock/fixed now)] + (let [data {::th/type :create-file + ::rpc/profile-id (:id prof) + :project-id proj-id + :id file-id + :name "foobar" + :is-shared false + :components-v2 true} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:name data) (:name result))) + (t/is (= proj-id (:project-id result))))) + + (let [data {::th/type :delete-file + :id file-id + ::rpc/profile-id (:id prof)} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; get deleted files + (let [data {::th/type :get-team-deleted-files + ::rpc/profile-id (:id prof) + :team-id team-id} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [[row1 :as result] (:result out)] + (t/is (= 1 (count result))) + (t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z")) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z")))) + + (let [data {::th/type :permanently-delete-team-files + ::rpc/profile-id (:id prof) + :team-id team-id + :ids #{file-id}} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= (:ids data) result))) + + (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] + (t/is (= (:deleted-at row) now))))))) + +(t/deftest deleted-files-restore + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + file-id (uuid/next) + now (ct/inst "2025-10-31T00:00:00Z")] + + (binding [ct/*clock* (clock/fixed now)] + (let [data {::th/type :create-file + ::rpc/profile-id (:id prof) + :project-id proj-id + :id file-id + :name "foobar" + :is-shared false + :components-v2 true} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:name data) (:name result))) + (t/is (= proj-id (:project-id result))))) + + (let [data {::th/type :delete-file + :id file-id + ::rpc/profile-id (:id prof)} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; get deleted files + (let [data {::th/type :get-team-deleted-files + ::rpc/profile-id (:id prof) + :team-id team-id} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [[row1 :as result] (:result out)] + (t/is (= 1 (count result))) + (t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z")) + (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) + (t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z")))) + + (let [data {::th/type :restore-deleted-team-files + ::rpc/profile-id (:id prof) + :team-id team-id + :ids #{file-id}} + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (fn? result)) + + (let [events (th/consume-sse result)] + ;; (pp/pprint events) + (t/is (= 2 (count events))) + (t/is (= :end (first (last events)))) + (t/is (= (:ids data) (last (last events))))))) + + (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] + (t/is (nil? (:deleted-at row))))))) diff --git a/backend/test/backend_tests/rpc_project_test.clj b/backend/test/backend_tests/rpc_project_test.clj index 652894aa47..6f4704e973 100644 --- a/backend/test/backend_tests/rpc_project_test.clj +++ b/backend/test/backend_tests/rpc_project_test.clj @@ -104,7 +104,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 1 (count result))))))) + (t/is (= 1 (count (remove :deleted-at result)))) + (t/is (= 2 (count result))))))) (t/deftest permissions-checks-create-project (let [profile1 (th/create-profile* 1) @@ -207,7 +208,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 1 (count result))))) + (t/is (= 2 (count result))) + (t/is (= 1 (count (remove :deleted-at result)))))) ;; run permanent deletion (should be noop) (let [result (th/run-task! :objects-gc {})] diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index fd6470893f..472ed76ff8 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -245,7 +245,10 @@ [:div {:class (stl/css-case :project-actions true :pinned-project (:is-pinned project))} (when-not (:is-default project) - [:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}]) + [:> pin-button* {:class (stl/css :pin-button) + :is-pinned (:is-pinned project) + :on-click toggle-pin + :tab-index 0}]) (when ^boolean can-edit [:button {:class (stl/css :add-file-btn) @@ -315,6 +318,7 @@ (let [projects (mf/with-memo [projects] (->> projects + (remove :deleted-at) (sort-by :modified-at) (reverse))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index d73ea8faae..8e7ccae588 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -688,6 +688,7 @@ pinned-projects (mf/with-memo [projects] (->> projects + (remove :deleted-at) (remove :is-default) (filter :is-pinned) (sort-by :name)