diff --git a/CHANGES.md b/CHANGES.md index 05587d16ff..d1324adeb9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -123,12 +123,13 @@ - Fix plugin parse-point returning plain map instead of Point record (by @FairyPigDev) [Github #9129](https://github.com/penpot/penpot/pull/9129) - Fix `:heigth` typo in clipboard frame-same-size? (by @iot2edge) [Github #9250](https://github.com/penpot/penpot/pull/9250) - Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090) +- Fix library updates reappear after being applied and the file is reloaded [Taiga #14040](https://tree.taiga.io/project/penpot/issue/14040) ## 2.15.0 (Unreleased) ### :sparkles: New features & Enhancements -- Add MCP server integration [Github #9174](https://github.com/penpot/penpot/issues/9174) +- Add MCP server integration [Github #9174](https://github.com/penpot/penpot/issues/9174) - Add chunked upload API for large media and binary files (removes previous upload size limits) [Github #8909](https://github.com/penpot/penpot/pull/8909) - Improve team name validation [Github #9176](https://github.com/penpot/penpot/pull/9176) @@ -166,7 +167,6 @@ - Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122) - Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927) - ## 2.14.3 ### :sparkles: New features & Enhancements @@ -196,7 +196,6 @@ - Fix typo `:podition` in swap-shapes grid cell - Fix multiple selection on shapes with token applied to stroke color - ## 2.14.2 ### :sparkles: New features & Enhancements diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index f57b8775df..f2e582c633 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -440,11 +440,28 @@ (db/run! cfg (fn [{:keys [::db/conn]}] (let [ids (db/create-array conn "uuid" ids) - sql (str "SELECT flr.* FROM file_library_rel AS flr " - " JOIN file AS l ON (flr.library_file_id = l.id) " - " WHERE flr.file_id = ANY(?) AND l.deleted_at IS NULL")] + sql (str "SELECT flr.*," + " fls.synced_at" + " FROM file_library_rel AS flr" + " JOIN file AS l" + " ON flr.library_file_id = l.id" + " LEFT JOIN file_library_sync AS fls" + " ON fls.file_id = flr.file_id" + " AND fls.library_file_id = flr.library_file_id" + " WHERE flr.file_id = ANY(?)" + " AND l.deleted_at IS NULL;")] (db/exec! conn [sql ids]))))) +(def ^:private sql:upsert-file-library-sync + "INSERT INTO file_library_sync (file_id, library_file_id, synced_at) + VALUES (?::uuid, ?::uuid, ?::timestamptz) + ON CONFLICT (file_id, library_file_id) + DO UPDATE SET synced_at = EXCLUDED.synced_at;") + +(defn upsert-file-library-sync! + [conn {:keys [file-id library-file-id synced-at]}] + (db/exec-one! conn [sql:upsert-file-library-sync file-id library-file-id synced-at])) + (def ^:private sql:get-libraries "WITH RECURSIVE libs AS ( SELECT fl.id @@ -799,32 +816,41 @@ (def ^:private sql:get-file-libraries "WITH RECURSIVE libs AS ( - SELECT fl.*, flr.synced_at - FROM file AS fl - JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) - WHERE flr.file_id = ?::uuid - UNION - SELECT fl.*, flr.synced_at - FROM file AS fl - JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) - JOIN libs AS l ON (flr.file_id = l.id) - ) - SELECT l.id, - l.features, - l.project_id, - p.team_id, - l.created_at, - l.modified_at, - l.deleted_at, - l.name, - l.revn, - l.vern, - l.synced_at, - l.is_shared, - l.version - FROM libs AS l - INNER JOIN project AS p ON (p.id = l.project_id) - WHERE l.deleted_at IS NULL;") + SELECT fl.* + FROM file AS fl + JOIN file_library_rel AS flr + ON flr.library_file_id = fl.id + WHERE flr.file_id = ?::uuid + + UNION + + SELECT fl.* + FROM file AS fl + JOIN file_library_rel AS flr + ON flr.library_file_id = fl.id + JOIN libs AS l + ON flr.file_id = l.id + ) + SELECT l.id, + l.features, + l.project_id, + p.team_id, + l.created_at, + l.modified_at, + l.deleted_at, + l.name, + l.revn, + l.vern, + l.is_shared, + l.version, + fls.synced_at + FROM libs AS l + JOIN project AS p + ON p.id = l.project_id + LEFT JOIN file_library_sync AS fls + ON fls.file_id = ?::uuid + AND fls.library_file_id = l.id + WHERE l.deleted_at IS NULL;") (defn get-file-libraries [conn file-id] @@ -834,7 +860,7 @@ ;; completly useless (map #(assoc % :is-indirect false)) (map decode-row-features)) - (db/exec! conn [sql:get-file-libraries file-id]))) + (db/exec! conn [sql:get-file-libraries file-id file-id]))) (defn get-resolved-file-libraries "Get all file libraries including itself. Returns an instance of diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 75f6f36994..ae2fc04e0c 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -573,7 +573,6 @@ ;; Insert all file relations (doseq [{:keys [library-file-id] :as rel} rels] (let [rel (-> rel - (assoc :synced-at timestamp) (update :file-id bfc/lookup-index) (update :library-file-id bfc/lookup-index))] @@ -583,7 +582,12 @@ :file-id (:file-id rel) :lib-id (:library-file-id rel) ::l/sync? true) - (db/insert! conn :file-library-rel rel)) + (let [rel-params (dissoc rel :synced-at)] + (db/insert! conn :file-library-rel rel-params) + (bfc/upsert-file-library-sync! conn {:file-id (:file-id rel-params) + :library-file-id (:library-file-id rel-params) + :synced-at (or (:synced-at rel) + timestamp)}))) (l/warn :hint "ignoring file library link" :file-id (:file-id rel) diff --git a/backend/src/app/binfile/v2.clj b/backend/src/app/binfile/v2.clj index c8acf2dc99..5adf25bd34 100644 --- a/backend/src/app/binfile/v2.clj +++ b/backend/src/app/binfile/v2.clj @@ -314,10 +314,10 @@ (doseq [rel (read-obj cfg :file-rels file-id)] (let [rel (-> rel (update :file-id bfc/lookup-index) - (update :library-file-id bfc/lookup-index) - (assoc :synced-at timestamp))] + (update :library-file-id bfc/lookup-index))] (db/insert! conn :file-library-rel rel - ::db/return-keys false))) + ::db/return-keys false) + (bfc/upsert-file-library-sync! conn (assoc rel :synced-at timestamp)))) (doseq [media (read-seq cfg :file-media-object file-id)] (let [media (-> media diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index b4ed065317..c6d3a7d6bd 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -824,10 +824,10 @@ :file-id (str file-id) :lib-id (str libr-id) ::l/sync? true) - (db/insert! conn :file-library-rel - {:synced-at timestamp - :file-id file-id - :library-file-id libr-id}))))) + (let [rel-params {:file-id file-id + :library-file-id libr-id}] + (db/insert! conn :file-library-rel rel-params) + (bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))))) (defn- import-storage-objects [{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}] diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index bd661f22cf..5ea79ff50c 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -481,7 +481,10 @@ :fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")} {:name "0148-add-variant-name-team-font-variant" - :fn (mg/resource "app/migrations/sql/0148-add-variant-name-team-font-variant.sql")}]) + :fn (mg/resource "app/migrations/sql/0148-add-variant-name-team-font-variant.sql")} + + {:name "0149-mod-file-library-rel-synced-at" + :fn (mg/resource "app/migrations/sql/0149-mod-file-library-rel-synced-at.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0149-mod-file-library-rel-synced-at.sql b/backend/src/app/migrations/sql/0149-mod-file-library-rel-synced-at.sql new file mode 100644 index 0000000000..7929cfc8de --- /dev/null +++ b/backend/src/app/migrations/sql/0149-mod-file-library-rel-synced-at.sql @@ -0,0 +1,19 @@ +CREATE TABLE file_library_sync ( + file_id uuid NOT NULL, + library_file_id uuid NOT NULL, + synced_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + PRIMARY KEY (file_id, library_file_id) +); + +INSERT INTO file_library_sync (file_id, library_file_id, synced_at) +SELECT file_id, library_file_id, synced_at + FROM file_library_rel; + +-- DEPRECATED: the `synced_at` column on `file_library_rel` is deprecated +-- and will be removed in a future migration. It's kept temporarily +-- for backward compatibility while data is migrated to `file_library_sync`. +COMMENT ON COLUMN file_library_rel.synced_at IS + 'DEPRECATED: will be removed in a future migration; kept temporarily for backward compatibility'; + + diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 346ff8b0fc..d53e1bca7a 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1064,7 +1064,10 @@ (defn link-file-to-library [conn {:keys [file-id library-id] :as params}] - (db/exec-one! conn [sql:link-file-to-library file-id library-id])) + (db/exec-one! conn [sql:link-file-to-library file-id library-id]) + (bfc/upsert-file-library-sync! conn {:file-id file-id + :library-file-id library-id + :synced-at (ct/now)})) (def ^:private schema:link-file-to-library @@ -1118,11 +1121,9 @@ (defn update-sync [conn {:keys [file-id library-id] :as params}] - (db/update! conn :file-library-rel - {:synced-at (ct/now)} - {:file-id file-id - :library-file-id library-id} - {::db/return-keys true})) + (bfc/upsert-file-library-sync! conn {:file-id file-id + :library-file-id library-id + :synced-at (ct/now)})) (def ^:private schema:update-file-library-sync-status [:map {:title "update-file-library-sync-status"} diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index ead821f79a..f8c6c11144 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -72,10 +72,14 @@ (doseq [params (sequence (comp (map #(bfc/remap-id % :file-id)) (map #(bfc/remap-id % :library-file-id)) - (map #(assoc % :synced-at timestamp)) (map #(assoc % :created-at timestamp))) flibs)] - (db/insert! conn :file-library-rel params ::db/return-keys false)) + (let [rel-params (dissoc params :synced-at)] + (db/insert! conn :file-library-rel rel-params ::db/return-keys false) + (bfc/upsert-file-library-sync! conn {:file-id (:file-id rel-params) + :library-file-id (:library-file-id rel-params) + :synced-at (or (:synced-at params) + timestamp)}))) (doseq [params (sequence (comp (map #(bfc/remap-id % :id)) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index d45dec0453..5faa31481b 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -918,6 +918,72 @@ (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :not-found))))) +(t/deftest link-file-to-library-creates-sync-row + (let [profile (th/create-profile* 1) + file1 (th/create-file* 1 {:project-id (:default-project-id profile) + :profile-id (:id profile) + :is-shared true}) + file2 (th/create-file* 2 {:project-id (:default-project-id profile) + :profile-id (:id profile)}) + data {::th/type :link-file-to-library + ::rpc/profile-id (:id profile) + :file-id (:id file2) + :library-id (:id file1)} + out (th/command! data) + rel (th/db-get :file-library-rel {:file-id (:id file2) + :library-file-id (:id file1)}) + sync (th/db-get :file-library-sync {:file-id (:id file2) + :library-file-id (:id file1)})] + + (t/is (nil? (:error out))) + (t/is (some? rel)) + (t/is (some? sync)) + (t/is (some? (:synced-at sync))))) + +(t/deftest update-file-library-sync-status-updates-sync-row + (let [profile (th/create-profile* 1) + file1 (th/create-file* 1 {:project-id (:default-project-id profile) + :profile-id (:id profile) + :is-shared true}) + file2 (th/create-file* 2 {:project-id (:default-project-id profile) + :profile-id (:id profile)}) + _ (th/link-file-to-library* {:file-id (:id file2) + :library-id (:id file1)}) + before (th/db-get :file-library-sync {:file-id (:id file2) + :library-file-id (:id file1)}) + _ (th/sleep 10) + data {::th/type :update-file-library-sync-status + ::rpc/profile-id (:id profile) + :file-id (:id file2) + :library-id (:id file1)} + out (th/command! data) + after (th/db-get :file-library-sync {:file-id (:id file2) + :library-file-id (:id file1)})] + + (t/is (nil? (:error out))) + (t/is (some? before)) + (t/is (some? after)) + (t/is (pos? (compare (:synced-at after) (:synced-at before)))))) + +(t/deftest update-file-library-sync-status-without-link-creates-sync-row + (let [profile (th/create-profile* 1) + file1 (th/create-file* 1 {:project-id (:default-project-id profile) + :profile-id (:id profile) + :is-shared true}) + file2 (th/create-file* 2 {:project-id (:default-project-id profile) + :profile-id (:id profile)}) + data {::th/type :update-file-library-sync-status + ::rpc/profile-id (:id profile) + :file-id (:id file2) + :library-id (:id file1)} + out (th/command! data) + sync (th/db-get :file-library-sync {:file-id (:id file2) + :library-file-id (:id file1)})] + + (t/is (nil? (:error out))) + (t/is (some? sync)) + (t/is (some? (:synced-at sync))))) + (t/deftest deletion (let [profile1 (th/create-profile* 1)