diff --git a/public/editor-client/package-lock.json b/public/editor-client/package-lock.json index ec22c52d2c..8bb2076b53 100644 --- a/public/editor-client/package-lock.json +++ b/public/editor-client/package-lock.json @@ -13,7 +13,8 @@ "fp-utilities": "^1.1.4", "franc": "^6.1.0", "js-base64": "^3.7.5", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "valtio": "^1.13.2" }, "devDependencies": { "@babel/parser": "^7.21.8", @@ -3367,6 +3368,14 @@ "node": ">=0.10.0" } }, + "node_modules/derive-valtio": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz", + "integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==", + "peerDependencies": { + "valtio": "*" + } + }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -6093,8 +6102,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -6348,6 +6356,18 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7477,6 +7497,11 @@ "node": ">= 6" } }, + "node_modules/proxy-compare": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", + "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==" + }, "node_modules/pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", @@ -7541,6 +7566,18 @@ } ] }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -9200,6 +9237,14 @@ "node": ">=0.10.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9248,6 +9293,31 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/valtio": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", + "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", + "dependencies": { + "derive-valtio": "0.1.0", + "proxy-compare": "2.6.0", + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", diff --git a/public/editor-client/package.json b/public/editor-client/package.json index 8a6cf8fb74..bc948f66c6 100644 --- a/public/editor-client/package.json +++ b/public/editor-client/package.json @@ -18,11 +18,12 @@ "author": "brizy", "license": "MIT", "dependencies": { + "@brizy/readers": "^1.0.1", "fp-utilities": "^1.1.4", + "franc": "^6.1.0", "js-base64": "^3.7.5", "lodash": "^4.17.21", - "franc": "^6.1.0", - "@brizy/readers": "^1.0.1" + "valtio": "^1.13.2" }, "devDependencies": { "@babel/parser": "^7.21.8", diff --git a/public/editor-client/src/api/index.ts b/public/editor-client/src/api/index.ts index b53628f339..c4be5eb03b 100644 --- a/public/editor-client/src/api/index.ts +++ b/public/editor-client/src/api/index.ts @@ -1,11 +1,10 @@ -import { Arr, Obj, Str } from "@brizy/readers"; import { Config, getConfig } from "@/config"; +import { ConfigDCItem } from "@/types/DynamicContent"; import { GlobalBlock } from "@/types/GlobalBlocks"; import { Page } from "@/types/Page"; import { Rule } from "@/types/PopupConditions"; import { Project } from "@/types/Project"; import { ResponseWithBody } from "@/types/Response"; -import { ConfigDCItem } from "@/types/DynamicContent"; import { CreateSavedBlock, CreateSavedLayout, @@ -15,8 +14,10 @@ import { SavedLayoutMeta } from "@/types/SavedBlocks"; import { ScreenshotData } from "@/types/Screenshots"; -import { Dictionary } from "../types/utils"; +import { CSSSymbol } from "@/types/Symbols"; import { t } from "@/utils/i18n"; +import { Arr, Obj, Str } from "@brizy/readers"; +import { Dictionary } from "../types/utils"; import { Literal } from "../utils/types"; import { GetCollections, @@ -1262,3 +1263,102 @@ export const updateGlobalBlocks = async ( }; //#endregion + +//#region Symbols + +///// TOOD: review return type of unknown +export const getSymbols = async (): Promise => { + const config = getConfig(); + + if (!config) { + throw new Error(t("Invalid __BRZ_PLUGIN_ENV__ at receiving symbols")); + } + + const { editorVersion, url: _url, hash, actions } = config; + + const url = makeUrl(_url, { + action: actions.symbolList, + version: editorVersion, + hash + }); + + return request(url, { + method: "GET", + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }) + .then((r) => r.json()) + .then((r) => (r.success ? r.data : [])); +}; + +export const createSymbol = async (data: CSSSymbol[]): Promise => { + const config = getConfig(); + + if (!config) { + throw new Error(t("Invalid __BRZ_PLUGIN_ENV__ at createSymbols")); + } + + const { editorVersion, url: _url, hash, actions } = config; + + const url = makeUrl(_url, { + action: actions.symbolCreate, + version: editorVersion, + hash + }); + + await request(url, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8" + }, + body: JSON.stringify(data) + }); +}; + +export const updateSymbol = async (data: CSSSymbol[]): Promise => { + const config = getConfig(); + + if (!config) { + throw new Error(t("Invalid __BRZ_PLUGIN_ENV__ at update symbol")); + } + + const { editorVersion, url: _url, hash, actions } = config; + + const url = makeUrl(_url, { + action: actions.symbolUpdate, + version: editorVersion, + hash + }); + + await request(url, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8" + }, + body: JSON.stringify(data) + }); +}; + +export const deleteSymbol = async (data: string[]): Promise => { + const config = getConfig(); + + if (!config) { + throw new Error(t("Invalid __BRZ_PLUGIN_ENV__ at delete symbol")); + } + + const { editorVersion, url: _url, hash, actions } = config; + + const url = makeUrl(_url, { + action: actions.symbolDelete, + version: editorVersion, + hash + }); + + await request(url, { + method: "DELETE", + body: JSON.stringify(data) + }); +}; + +//#endregion diff --git a/public/editor-client/src/config.ts b/public/editor-client/src/config.ts index 9176f937e6..db21cc5c2e 100644 --- a/public/editor-client/src/config.ts +++ b/public/editor-client/src/config.ts @@ -52,6 +52,11 @@ interface Actions { heartBeat: string; takeOver: string; getFonts: string; + + symbolCreate: string; + symbolDelete: string; + symbolList: string; + symbolUpdate: string; } interface ProjectStatus { @@ -282,6 +287,22 @@ const actionsReader = parseStrict({ getFonts: pipe( mPipe(Obj.readKey("getFonts"), Str.read), throwOnNullish("Invalid actions: getFonts") + ), + symbolCreate: pipe( + mPipe(Obj.readKey("symbolCreate"), Str.read), + throwOnNullish("Invalid actions: symbolCreate") + ), + symbolDelete: pipe( + mPipe(Obj.readKey("symbolDelete"), Str.read), + throwOnNullish("Invalid actions: symbolDelete") + ), + symbolList: pipe( + mPipe(Obj.readKey("symbolList"), Str.read), + throwOnNullish("Invalid actions: symbolList") + ), + symbolUpdate: pipe( + mPipe(Obj.readKey("symbolUpdate"), Str.read), + throwOnNullish("Invalid actions: symbolUpdate") ) }); diff --git a/public/editor-client/src/index.ts b/public/editor-client/src/index.ts index acb8f5ed65..f87f251b13 100644 --- a/public/editor-client/src/index.ts +++ b/public/editor-client/src/index.ts @@ -1,3 +1,4 @@ +import { symbols } from "@/symbols"; import set from "lodash/set"; import { doAiRequest } from "./aiText"; import { autoSave } from "./autoSave"; @@ -15,9 +16,9 @@ import { import { placeholderData, placeholders } from "./dynamicContent"; import { handler as posts } from "./Elements/Posts"; import { uploadedFonts } from "./fonts"; -import { heartBeat } from "./heartBeat"; -import {globalBlocks } from "./globalBlocks/blocks"; +import { globalBlocks } from "./globalBlocks/blocks"; import { globalPopups } from "./globalBlocks/popups"; +import { heartBeat } from "./heartBeat"; import { addMedia } from "./media/addMedia"; import { addMediaGallery } from "./media/addMediaGallery"; import { onChange } from "./onChange"; @@ -65,7 +66,8 @@ const api = { loadCollectionTypes }, screenshots: screenshots(), - heartBeat: heartBeat(config) + heartBeat: heartBeat(config), + symbols }; if (window.__VISUAL_CONFIG__) { diff --git a/public/editor-client/src/onChange/index.ts b/public/editor-client/src/onChange/index.ts index 5c6ca09c0b..a9c66d6e67 100644 --- a/public/editor-client/src/onChange/index.ts +++ b/public/editor-client/src/onChange/index.ts @@ -1,4 +1,5 @@ import { updateGlobalBlock, updatePage, updateProject } from "@/api"; +import { handleSymbols } from "@/onChange/symbols"; import { OnChange } from "@/types/OnChange"; export const onChange = (data: OnChange) => { @@ -13,4 +14,8 @@ export const onChange = (data: OnChange) => { if (data.globalBlock) { updateGlobalBlock(data.globalBlock, { is_autosave: 0 }); } + + if (data.symbol) { + handleSymbols(data.symbol); + } }; diff --git a/public/editor-client/src/onChange/symbols.ts b/public/editor-client/src/onChange/symbols.ts new file mode 100644 index 0000000000..69c6eace4f --- /dev/null +++ b/public/editor-client/src/onChange/symbols.ts @@ -0,0 +1,46 @@ +import { store } from "@/store"; +import { getIndex as getSymbolIndex } from "@/symbols/utils"; +import { SymbolAction, SymbolsAction } from "@/types/Symbols"; +import { remove } from "lodash"; + +export const handleSymbols = (action: SymbolsAction): void => { + const { type } = action; + + switch (type) { + case SymbolAction.Create: { + store.symbols.toCreate.push(action.payload); + break; + } + case SymbolAction.Update: { + const { uid } = action.payload; + const index = getSymbolIndex(uid, store.symbols.toCreate); + + if (index === -1) { + store.symbols.toUpdate.push(action.payload); + } else { + store.symbols.toCreate.splice(index, 1, action.payload); + } + + break; + } + case SymbolAction.DELETE: { + const uid = action.payload; + + const indexInToCreate = getSymbolIndex(uid, store.symbols.toCreate); + + if (indexInToCreate !== -1) { + remove(store.symbols.toCreate, { uid }); + break; + } + + const indexInToUpdate = getSymbolIndex(uid, store.symbols.toUpdate); + + if (indexInToUpdate !== -1) { + remove(store.symbols.toUpdate, { uid }); + } + + store.symbols.toDelete.push(uid); + break; + } + } +}; diff --git a/public/editor-client/src/publish/index.ts b/public/editor-client/src/publish/index.ts index 44c2c905bf..38ae8473db 100644 --- a/public/editor-client/src/publish/index.ts +++ b/public/editor-client/src/publish/index.ts @@ -1,6 +1,11 @@ import { updateGlobalBlocks, updatePage, updateProject } from "@/api"; +import { updateSymbols } from "@/publish/symbols"; +import { store } from "@/store"; +import { State } from "@/store/types"; +import { hasSomeSymbolToUpdate } from "@/store/utils"; import { Publish } from "@/types/Publish"; import { t } from "@/utils/i18n"; +import { snapshot } from "valtio/vanilla"; export const publish: Publish = { async handler(res, rej, args) { @@ -31,6 +36,16 @@ export const publish: Publish = { } } + const snap = snapshot(store) as Readonly; + + if (hasSomeSymbolToUpdate(snap)) { + await updateSymbols( + snap.symbols.toCreate, + snap.symbols.toUpdate, + snap.symbols.toDelete + ); + } + if (errors.length > 0) { rej(errors.join(";")); } else { diff --git a/public/editor-client/src/publish/symbols.ts b/public/editor-client/src/publish/symbols.ts new file mode 100644 index 0000000000..9cde775b21 --- /dev/null +++ b/public/editor-client/src/publish/symbols.ts @@ -0,0 +1,23 @@ +import { createSymbol, deleteSymbol, updateSymbol } from "@/api"; +import { clearSymbols } from "@/store/utils"; +import { CSSSymbol } from "@/types/Symbols"; + +export const updateSymbols = async ( + toCreate: CSSSymbol[], + toUpdate: CSSSymbol[], + toDelete: string[] +) => { + if (toCreate.length > 0) { + await createSymbol(toCreate); + } + + if (toUpdate.length > 0) { + await updateSymbol(toUpdate); + } + + if (toDelete.length > 0) { + await deleteSymbol(toDelete); + } + + clearSymbols(); +}; diff --git a/public/editor-client/src/store/index.ts b/public/editor-client/src/store/index.ts new file mode 100644 index 0000000000..b1c96ceb9d --- /dev/null +++ b/public/editor-client/src/store/index.ts @@ -0,0 +1,10 @@ +import { proxy } from "valtio/vanilla"; +import type { State } from "./types"; + +export const store = proxy({ + symbols: { + toCreate: [], + toUpdate: [], + toDelete: [] + } +}); diff --git a/public/editor-client/src/store/types.ts b/public/editor-client/src/store/types.ts new file mode 100644 index 0000000000..4392bc417e --- /dev/null +++ b/public/editor-client/src/store/types.ts @@ -0,0 +1,9 @@ +import { CSSSymbol } from "../types/Symbols"; + +export interface State { + symbols: { + toCreate: CSSSymbol[]; + toUpdate: CSSSymbol[]; + toDelete: string[]; + }; +} diff --git a/public/editor-client/src/store/utils.ts b/public/editor-client/src/store/utils.ts new file mode 100644 index 0000000000..0f64b0f6ce --- /dev/null +++ b/public/editor-client/src/store/utils.ts @@ -0,0 +1,13 @@ +import { store } from "./index"; +import { State } from "./types"; + +export const clearSymbols = (): void => { + store.symbols.toCreate = []; + store.symbols.toUpdate = []; + store.symbols.toDelete = []; +}; + +export const hasSomeSymbolToUpdate = ({ + symbols: { toCreate, toUpdate, toDelete } +}: Readonly): boolean => + toCreate.length > 0 || toUpdate.length > 0 || toDelete.length > 0; diff --git a/public/editor-client/src/symbols/index.ts b/public/editor-client/src/symbols/index.ts new file mode 100644 index 0000000000..9e44aacf8e --- /dev/null +++ b/public/editor-client/src/symbols/index.ts @@ -0,0 +1,43 @@ +import { createSymbol, deleteSymbol, getSymbols, updateSymbol } from "@/api"; +import { Symbols } from "@/types/Symbols"; +import { t } from "@/utils/i18n"; +import { Str } from "@brizy/readers"; + +export const symbols: Symbols = { + get: async (res, rej) => { + try { + const symbols = await getSymbols(); + res(symbols); + } catch (e) { + const msg = Str.read(e) ?? t("Can't receive symbols"); + rej(msg); + } + }, + add: async (res, rej, data) => { + try { + const response = await createSymbol(data); + res(response); + } catch (e) { + const msg = Str.read(e) ?? t("Can't create symbols"); + rej(msg); + } + }, + update: async (res, rej, data) => { + try { + const response = await updateSymbol(data); + res(response); + } catch (e) { + const msg = Str.read(e) ?? t("Can't update symbols"); + rej(msg); + } + }, + delete: async (res, rej, data) => { + try { + const response = await deleteSymbol(data); + res(response); + } catch (e) { + const msg = Str.read(e) ?? t("Can't delete symbols"); + rej(msg); + } + } +}; diff --git a/public/editor-client/src/symbols/utils.ts b/public/editor-client/src/symbols/utils.ts new file mode 100644 index 0000000000..0034a5c31d --- /dev/null +++ b/public/editor-client/src/symbols/utils.ts @@ -0,0 +1,4 @@ +import { CSSSymbol } from "@/types/Symbols"; + +export const getIndex = (id: string, symbols: CSSSymbol[]): number => + symbols.findIndex(({ uid }) => id === uid); diff --git a/public/editor-client/src/types/OnChange.ts b/public/editor-client/src/types/OnChange.ts index 0e91833cc8..1f4a1ca7d5 100644 --- a/public/editor-client/src/types/OnChange.ts +++ b/public/editor-client/src/types/OnChange.ts @@ -1,3 +1,4 @@ +import { SymbolsAction } from "@/types/Symbols"; import { GlobalBlock } from "./GlobalBlocks"; import { Page } from "./Page"; import { Project } from "./Project"; @@ -6,4 +7,5 @@ export interface OnChange { projectData?: Project; pageData?: Page; globalBlock?: GlobalBlock; + symbol?: SymbolsAction; } diff --git a/public/editor-client/src/types/Symbols.ts b/public/editor-client/src/types/Symbols.ts new file mode 100644 index 0000000000..06ddbf5b5b --- /dev/null +++ b/public/editor-client/src/types/Symbols.ts @@ -0,0 +1,60 @@ +import { Literal } from "@/utils/types"; +import { Response } from "./Response"; + +export interface CSSSymbol { + uid: string; + componentClassName: string; + childrenClassName?: string; + // INFO: the correct type should be `ElementTypes` from editor, but is not ok to import here from editor + type: string; + model: { + component: Record; + children?: Record; + }; +} + +export interface DBSymbol { + uid: string; + label: string; + version: Literal; + data: { + type: string; + className: string; + model: Record; + }; +} + +export enum SymbolAction { + Create = "CREATE", + Update = "UPDATE", + DELETE = "DELETE" +} + +export interface SymbolCreate { + type: SymbolAction.Create; + payload: CSSSymbol; +} + +export interface SymbolUpdate { + type: SymbolAction.Update; + payload: CSSSymbol; +} + +export interface SymbolDelete { + type: SymbolAction.DELETE; + payload: string; +} + +export type SymbolsAction = SymbolCreate | SymbolUpdate | SymbolDelete; + +///// TODO: review unknown +export interface Symbols { + get: (res: Response, rej: Response) => void; + add: (res: Response, rej: Response, data: CSSSymbol[]) => void; + update: ( + res: Response, + rej: Response, + data: CSSSymbol[] + ) => void; + delete: (res: Response, rej: Response, data: string[]) => void; +}