From 0824ce4374876c5f0997f21c966f656dddbbecf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Wed, 20 May 2026 15:35:22 +0200 Subject: [PATCH 1/3] chore: wip --- code/api/get-templates.ts | 4 +- code/api/pages/post-page.ts | 21 +- code/api/post-add-image-template-to-room.ts | 43 ++ code/api/post-add-template-to-room.ts | 14 +- code/api/post-template.ts | 6 + .../add-to-room.configuration.tsx | 367 ++++++++++++ .../add-to-room.confirmation.tsx | 360 +++++++++++ .../add-to-room.dialog.steps.tsx | 52 +- .../add-to-room.dialog.tsx | 28 +- .../add-to-room.render-template.tsx | 116 ++++ .../add-to-room.select-template.tsx | 111 ++-- .../add-to-room.template.tsx | 40 +- .../hooks/use-context-menu.tsx | 132 ++-- .../hooks/use-create-page-from-template.tsx | 135 +++++ .../hooks/use-json-template.tsx | 562 ++++++++++++++++-- .../hooks/use-update-page-thumbnail.tsx | 104 ++-- .../images-library/images-library.actions.tsx | 74 ++- .../images-library/images-library.tsx | 25 +- .../images-library/uploaded.image.tsx | 14 +- .../room-components/overlay/save-template.tsx | 308 +++++----- .../room-components/sidebar-selector.tsx | 4 +- .../templates-library/template.tsx | 46 +- .../templates-library.actions.tsx | 57 +- .../templates-library/templates-library.tsx | 118 +++- .../templates-library/types.ts | 2 + .../videos-library/videos-library.tsx | 2 +- code/components/room/room.header.tsx | 2 +- code/components/room/room.layout.tsx | 2 + code/components/room/room.left-sidebar.tsx | 2 +- code/components/room/room.right-layout.tsx | 11 +- .../add-to-room/add-to-room.confirmation.tsx | 201 ------- .../add-to-room/add-to-room.create-room.tsx | 61 -- .../add-to-room/add-to-room.select-page.tsx | 137 ----- .../add-to-room/add-to-room.select-room.tsx | 118 ---- .../templates/components/home/home.tsx | 56 -- .../templates/components/images/images.tsx | 309 ---------- .../templates/components/page/menu.tsx | 111 ---- .../templates/components/page/page-loader.tsx | 108 ---- .../templates/components/page/page.tsx | 297 --------- .../templates/components/page/user-form.tsx | 105 ---- .../components/templates/template.tsx | 43 -- .../components/templates/templates.tsx | 215 ------- .../templates/components/templates/types.ts | 30 - .../templates/components/upload-image.tsx | 91 --- .../hooks/use-amount-slots-template.tsx | 34 -- .../hooks/use-handle-route-params.tsx | 25 - .../use-cases/templates/store/add-to-room.ts | 45 -- .../use-cases/templates/store/store.ts | 81 --- .../use-cases/templates/utils/utils.ts | 27 - code/src/routeTree.gen.ts | 43 -- code/src/routes/globals.css | 12 + .../use-cases/templates/$instanceId.tsx | 45 -- code/src/routes/use-cases/templates/index.tsx | 43 -- code/store/add-template-to-room.ts | 100 ++++ code/store/templates.ts | 22 + code/vite.config.ts | 1 + 56 files changed, 2378 insertions(+), 2744 deletions(-) create mode 100644 code/api/post-add-image-template-to-room.ts create mode 100644 code/components/room-components/add-template-to-room/add-to-room.configuration.tsx create mode 100644 code/components/room-components/add-template-to-room/add-to-room.confirmation.tsx rename code/components/{use-cases/templates/components/add-to-room => room-components/add-template-to-room}/add-to-room.dialog.steps.tsx (61%) rename code/components/{use-cases/templates/components/add-to-room => room-components/add-template-to-room}/add-to-room.dialog.tsx (66%) create mode 100644 code/components/room-components/add-template-to-room/add-to-room.render-template.tsx rename code/components/{use-cases/templates/components/add-to-room => room-components/add-template-to-room}/add-to-room.select-template.tsx (53%) rename code/components/{use-cases/templates/components/add-to-room => room-components/add-template-to-room}/add-to-room.template.tsx (50%) create mode 100644 code/components/room-components/hooks/use-create-page-from-template.tsx delete mode 100644 code/components/use-cases/templates/components/add-to-room/add-to-room.confirmation.tsx delete mode 100644 code/components/use-cases/templates/components/add-to-room/add-to-room.create-room.tsx delete mode 100644 code/components/use-cases/templates/components/add-to-room/add-to-room.select-page.tsx delete mode 100644 code/components/use-cases/templates/components/add-to-room/add-to-room.select-room.tsx delete mode 100644 code/components/use-cases/templates/components/home/home.tsx delete mode 100644 code/components/use-cases/templates/components/images/images.tsx delete mode 100644 code/components/use-cases/templates/components/page/menu.tsx delete mode 100644 code/components/use-cases/templates/components/page/page-loader.tsx delete mode 100644 code/components/use-cases/templates/components/page/page.tsx delete mode 100644 code/components/use-cases/templates/components/page/user-form.tsx delete mode 100644 code/components/use-cases/templates/components/templates/template.tsx delete mode 100644 code/components/use-cases/templates/components/templates/templates.tsx delete mode 100644 code/components/use-cases/templates/components/templates/types.ts delete mode 100644 code/components/use-cases/templates/components/upload-image.tsx delete mode 100644 code/components/use-cases/templates/hooks/use-amount-slots-template.tsx delete mode 100644 code/components/use-cases/templates/hooks/use-handle-route-params.tsx delete mode 100644 code/components/use-cases/templates/store/add-to-room.ts delete mode 100644 code/components/use-cases/templates/store/store.ts delete mode 100644 code/components/use-cases/templates/utils/utils.ts delete mode 100644 code/src/routes/use-cases/templates/$instanceId.tsx delete mode 100644 code/src/routes/use-cases/templates/index.tsx create mode 100644 code/store/add-template-to-room.ts diff --git a/code/api/get-templates.ts b/code/api/get-templates.ts index 8ce0512a..73c62e9e 100644 --- a/code/api/get-templates.ts +++ b/code/api/get-templates.ts @@ -4,13 +4,15 @@ export const getTemplates = async ( roomId: string, + kind?: string, + imageSlots?: number, offset: number = 0, limit: number = 20, ) => { const apiEndpoint = import.meta.env.VITE_API_ENDPOINT; const hubName = import.meta.env.VITE_API_ENDPOINT_HUB_NAME; - const endpoint = `${apiEndpoint}/${hubName}/rooms/${roomId}/templates?offset=${offset}&limit=${limit}`; + const endpoint = `${apiEndpoint}/${hubName}/rooms/${roomId}/templates?offset=${offset}&limit=${limit}${kind ? `&kind=${kind}` : ""}${imageSlots ? `&imageSlots=${imageSlots}` : ""}`; const response = await fetch(endpoint); const data = await response.json(); diff --git a/code/api/pages/post-page.ts b/code/api/pages/post-page.ts index ea847a0a..86b5aae3 100644 --- a/code/api/pages/post-page.ts +++ b/code/api/pages/post-page.ts @@ -9,7 +9,22 @@ export const postPage = async ( name, thumbnail, position, - }: { pageId: string; name: string; thumbnail: string; position?: number }, + templateId, + target, + }: { + pageId: string; + name: string; + thumbnail: string; + position?: number; + templateId?: string; + target?: { + id: string; + position: { + x: number; + y: number; + }; + }; + }, ) => { const apiEndpoint = import.meta.env.VITE_API_ENDPOINT; const hubName = import.meta.env.VITE_API_ENDPOINT_HUB_NAME; @@ -26,11 +41,13 @@ export const postPage = async ( name, thumbnail, position, + templateId, + target, }), }); if (!response.ok) { - throw new Error(`Error creating chat: ${response.statusText}`); + throw new Error(`Error creating page: ${response.statusText}`); } const data = await response.json(); diff --git a/code/api/post-add-image-template-to-room.ts b/code/api/post-add-image-template-to-room.ts new file mode 100644 index 00000000..7b46b99e --- /dev/null +++ b/code/api/post-add-image-template-to-room.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +export type PostAddImageTemplateToRoomPayload = { + roomId: string; + pageId: string; + templateId: string; + target: { + id: string; + position: { + x: number; + y: number; + }; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record; +}; + +export const postAddImageTemplateToRoom = async ( + payload: PostAddImageTemplateToRoomPayload, +) => { + const apiEndpoint = import.meta.env.VITE_API_ENDPOINT; + const hubName = import.meta.env.VITE_API_ENDPOINT_HUB_NAME; + + const endpoint = `${apiEndpoint}/${hubName}/templates/add-image-template-to-room`; + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: payload ? JSON.stringify(payload) : undefined, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error("Failed to add image template to room: " + errorData.error); + } + + const data = await response.json(); + + return data; +}; diff --git a/code/api/post-add-template-to-room.ts b/code/api/post-add-template-to-room.ts index 07f665f3..da1ffa22 100644 --- a/code/api/post-add-template-to-room.ts +++ b/code/api/post-add-template-to-room.ts @@ -3,12 +3,16 @@ // SPDX-License-Identifier: Apache-2.0 export type PostAddTemplateToRoomPayload = { - roomId?: string; - roomName?: string; - frameName: string; - templateInstanceId: string; + roomId: string; + pageId: string; templateId: string; - imagesIds: string[]; + target: { + id: string; + position: { + x: number; + y: number; + }; + }; }; export const postAddTemplateToRoom = async ( diff --git a/code/api/post-template.ts b/code/api/post-template.ts index eb3d02cf..1d20d2ed 100644 --- a/code/api/post-template.ts +++ b/code/api/post-template.ts @@ -5,12 +5,16 @@ export const postTemplate = async ({ roomId, name, + kind, + imageSlots, linkedNodeType, templateImage, templateData, }: { roomId: string; name: string; + kind: string; + imageSlots?: number; linkedNodeType: string | null; templateImage: string; templateData: string; @@ -26,6 +30,8 @@ export const postTemplate = async ({ }, body: JSON.stringify({ name, + kind, + ...(imageSlots && { imageSlots }), linkedNodeType, templateImage, templateData, diff --git a/code/components/room-components/add-template-to-room/add-to-room.configuration.tsx b/code/components/room-components/add-template-to-room/add-to-room.configuration.tsx new file mode 100644 index 00000000..c21c07e6 --- /dev/null +++ b/code/components/room-components/add-template-to-room/add-to-room.configuration.tsx @@ -0,0 +1,367 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { Button } from "@/components/ui/button"; +import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import React from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { useJsonTemplate } from "../hooks/use-json-template"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { AddToRoomRenderTemplate } from "./add-to-room.render-template"; +import { Input } from "@/components/ui/input"; + +export function AddToRoomConfiguration() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [templateData, setTemplateData] = React.useState(null); + + const [text, setText] = React.useState(""); + + const initialized = useAddTemplateToRoom((state) => state.initialized); + const room = useAddTemplateToRoom((state) => state.room); + const page = useAddTemplateToRoom((state) => state.page); + const template = useAddTemplateToRoom((state) => state.template); + const selectedImages = useAddTemplateToRoom((state) => state.images); + const templateParameters = useAddTemplateToRoom((state) => state.parameters); + const selectedNode = useAddTemplateToRoom((state) => state.selectedNode); + const setTemplate = useAddTemplateToRoom((state) => state.setTemplate); + const setSelectedNode = useAddTemplateToRoom( + (state) => state.setSelectedNode, + ); + const setStep = useAddTemplateToRoom((state) => state.setStep); + const setTemplateParameters = useAddTemplateToRoom( + (state) => state.setTemplateParameters, + ); + const setInitialized = useAddTemplateToRoom((state) => state.setInitialized); + + const { getNodeMetadata, getTemplateNodesMetadata, setupTemplateDefaults } = + useJsonTemplate(); + + React.useEffect(() => { + if (!template) return; + + try { + const templateData = JSON.parse(template.templateData); + setTemplateData(templateData); + } catch { + setTemplateData(null); + } + }, [template]); + + React.useEffect(() => { + if (room && page && !initialized && templateData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const imageNodes: any[] = getTemplateNodesMetadata(templateData, [ + "image", + ]); + const sortedImageNodes = imageNodes.sort((a, b) => + a.id.localeCompare(b.id), + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const imageTemplateParameters: Record = { + ...templateParameters, + }; + + for (let i = 0; i < sortedImageNodes.length; i++) { + const imageNode = selectedImages?.[i]; + const node = sortedImageNodes[i]; + const nodeId = node.id; + if (imageNode) { + imageTemplateParameters[nodeId] = { + ...imageTemplateParameters[nodeId], + nodeId, + kind: "image", + properties: { + image: { + source: imageNode.url, + width: imageNode.size.width, + height: imageNode.size.height, + }, + fit: "cover", + }, + }; + } else { + delete imageTemplateParameters[nodeId]; + } + } + + const defaultTemplateParameters = setupTemplateDefaults( + templateData, + imageTemplateParameters, + ); + + setTemplateParameters(defaultTemplateParameters); + setInitialized(true); + } + }, [ + room, + page, + initialized, + selectedImages, + templateData, + setupTemplateDefaults, + setInitialized, + ]); + + const nodeMetadata = React.useMemo(() => { + if (!templateData || !selectedNode) return null; + + return getNodeMetadata(templateData, selectedNode); + }, [templateData, selectedNode, getNodeMetadata]); + + React.useEffect(() => { + if (selectedNode && nodeMetadata?.kind === "text") { + setText(templateParameters?.[selectedNode]?.properties?.text || ""); + } else { + setText(""); + } + }, [selectedNode, nodeMetadata, templateParameters]); + + if (!template || !room) { + return null; + } + + return ( + <> +
+ + Check the selection and confirm to add the images to the room. + +
+
+ +
+
+
+
+ +
+ {selectedImages.map((image) => { + return ( +
{ + const newParameters = { + ...templateParameters, + [selectedNode]: { + nodeId: selectedNode, + kind: "image", + properties: { + image: { + source: image.url, + width: image.size.width, + height: image.size.height, + }, + fit: "cover", + }, + }, + }; + + setTemplateParameters(newParameters); + } + : undefined + } + > + {`image +
+ ); + })} +
+
+
+
+
+
+
+
+ {nodeMetadata && selectedNode && ( + <> +
+
Element
+ +
+
+
Id / Kind
+
+ {nodeMetadata.id} /{" "} + {nodeMetadata.kind} +
+
+ + )} + {nodeMetadata && selectedNode && nodeMetadata.kind === "image" && ( + <> +
+
+
Image format
+ +
+ + )} + {nodeMetadata && selectedNode && nodeMetadata.kind === "text" && ( + <> +
+
+
Text
+
+ { + window.weaveOnFieldFocus = true; + }} + onBlurCapture={() => { + window.weaveOnFieldFocus = false; + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + const newParameters = { + ...templateParameters, + [selectedNode]: { + ...templateParameters[selectedNode], + nodeId: selectedNode, + kind: "text", + properties: { + ...templateParameters[selectedNode].properties, + text, + }, + }, + }; + + setTemplateParameters(newParameters); + } + }} + onChange={(e) => setText(e.target.value)} + /> +
+
+ + )} + {!nodeMetadata && !selectedNode && ( +
+
+ Select an element, if the element is an: +
+
+ + For image elements, click on the element and + then select one of the images below to set it as content of + the element. + +
+
+ + For text elements, click on the element and + then define the text. + +
+
+ )} +
+
+ + + + +
+ + ); +} diff --git a/code/components/room-components/add-template-to-room/add-to-room.confirmation.tsx b/code/components/room-components/add-template-to-room/add-to-room.confirmation.tsx new file mode 100644 index 00000000..cc77197d --- /dev/null +++ b/code/components/room-components/add-template-to-room/add-to-room.confirmation.tsx @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import { Button } from "@/components/ui/button"; +import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { toast } from "sonner"; +import React from "react"; +import { Spinner } from "@/components/ui/spinner"; +import { useMutation } from "@tanstack/react-query"; +import { + postAddImageTemplateToRoom, + PostAddImageTemplateToRoomPayload, +} from "@/api/post-add-image-template-to-room"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { AddToRoomRenderTemplate } from "./add-to-room.render-template"; +import { useCollaborationRoom } from "@/store/store"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { useWeave } from "@inditextech/weave-react"; +import Konva from "konva"; + +export function AddToRoomConfirmation() { + const [targetContainer, setTargetContainer] = + React.useState("mainLayer"); + const [positionKind, setPositionKind] = React.useState< + "at-origin" | "centered-at-origin" | "custom" + >("at-origin"); + const [position, setPosition] = React.useState<{ x: number; y: number }>({ + x: 0, + y: 0, + }); + + const instance = useWeave((state) => state.instance); + + const actualRoom = useCollaborationRoom((state) => state.roomInfo.data); + const actualPage = useCollaborationRoom( + (state) => state.pages.actualPageElement, + ); + + const room = useAddTemplateToRoom((state) => state.room); + const page = useAddTemplateToRoom((state) => state.page); + const template = useAddTemplateToRoom((state) => state.template); + const renderSize = useAddTemplateToRoom((state) => state.render.size); + const templateParameters = useAddTemplateToRoom((state) => state.parameters); + const setStep = useAddTemplateToRoom((state) => state.setStep); + const setVisible = useAddTemplateToRoom((state) => state.setVisible); + const setTemplate = useAddTemplateToRoom((state) => state.setTemplate); + const setTemplateParameters = useAddTemplateToRoom( + (state) => state.setTemplateParameters, + ); + const setImages = useAddTemplateToRoom((state) => state.setImages); + const [processing, setProcessing] = React.useState(false); + + const mutationAddToRoom = useMutation({ + mutationFn: async (payload: PostAddImageTemplateToRoomPayload) => { + return await postAddImageTemplateToRoom(payload); + }, + onSettled() { + setProcessing(false); + }, + onSuccess() { + if (!room) { + return; + } + + setTemplate(undefined); + setTemplateParameters({}); + setImages([]); + setVisible(false); + }, + onError() { + toast.error("Failed to create template, please try again"); + }, + }); + + const targetContainers = React.useMemo(() => { + if (!instance) { + return []; + } + + const stage = instance.getStage(); + const containers = stage.find( + (n: Konva.Node) => + n.getAttrs().name?.indexOf("node") !== -1 && + n.getAttrs().nodeType === "frame", + ); + + return containers.map((c) => ({ + id: c.id(), + name: c.getAttrs().nodeName ?? c.getAttrs().id, + })); + }, [instance]); + + if (!template || !room) { + return null; + } + + return ( + <> +
+ + Check the selection and confirm to add the template. The template will + be added on the center of the page. + +
+
+ +
+
+
+
+
Destination
+
+
Room
+
{actualRoom.room.name}
+
+
+
+
Page
+
{actualPage.name}
+
+
+
+
Target container
+ +
+
+
+
Position
+ +
+ {positionKind === "custom" && ( + <> +
+
+
X
+
+ { + window.weaveOnFieldFocus = true; + }} + onBlurCapture={() => { + window.weaveOnFieldFocus = false; + }} + onChange={(e) => { + const newPosition = { + ...position, + x: Number(e.target.value), + }; + + setPosition(newPosition); + }} + /> +
+
Y
+
+ { + window.weaveOnFieldFocus = true; + }} + onBlurCapture={() => { + window.weaveOnFieldFocus = false; + }} + onChange={(e) => { + const newPosition = { + ...position, + y: Number(e.target.value), + }; + + setPosition(newPosition); + }} + /> +
+
+ + )} +
+
+ + + + +
+ + ); +} diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.steps.tsx b/code/components/room-components/add-template-to-room/add-to-room.dialog.steps.tsx similarity index 61% rename from code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.steps.tsx rename to code/components/room-components/add-template-to-room/add-to-room.dialog.steps.tsx index 33673e09..8530a5f3 100644 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.steps.tsx +++ b/code/components/room-components/add-template-to-room/add-to-room.dialog.steps.tsx @@ -3,20 +3,20 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import { useAddToRoom } from "../../store/add-to-room"; import { cn } from "@/lib/utils"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; export function AddToRoomDialogSteps() { - const step = useAddToRoom((state) => state.step); + const step = useAddTemplateToRoom((state) => state.step); return (
@@ -24,51 +24,31 @@ export function AddToRoomDialogSteps() {
- ROOM -
-
-
- 2 -
-
- PAGE + TEMPLATE
- 3 + 2
- TEMPLATE + SETUP
- 4 + 3
- CONFIRM + RESUME
); diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.tsx b/code/components/room-components/add-template-to-room/add-to-room.dialog.tsx similarity index 66% rename from code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.tsx rename to code/components/room-components/add-template-to-room/add-to-room.dialog.tsx index 13f08052..dbf2e23a 100644 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.dialog.tsx +++ b/code/components/room-components/add-template-to-room/add-to-room.dialog.tsx @@ -9,42 +9,33 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import React from "react"; import { X } from "lucide-react"; -import { useTemplatesUseCase } from "../../store/store"; -import { useAddToRoom } from "../../store/add-to-room"; -import { AddToRoomSelectRoom } from "./add-to-room.select-room"; import { AddToRoomSelectTemplate } from "./add-to-room.select-template"; import { AddToRoomConfirmation } from "./add-to-room.confirmation"; import { AddToRoomDialogSteps } from "./add-to-room.dialog.steps"; -import { AddToRoomSelectPage } from "./add-to-room.select-page"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { AddToRoomConfiguration } from "./add-to-room.configuration"; export function AddToRoomDialog() { - const step = useAddToRoom((state) => state.step); - - const addToRoomOpen = useTemplatesUseCase((state) => state.addToRoom.open); - const setAddToRoomOpen = useTemplatesUseCase( - (state) => state.setAddToRoomOpen, - ); + const step = useAddTemplateToRoom((state) => state.step); + const visible = useAddTemplateToRoom((state) => state.visible); + const setVisible = useAddTemplateToRoom((state) => state.setVisible); return ( - setAddToRoomOpen(open)} - > + setVisible(open)}>
- Add to Room + Add Images from Template
- {step === "select-room" && } - {step === "select-page" && } {step === "select-template" && } + {step === "configuration" && } {step === "confirm" && }
diff --git a/code/components/room-components/add-template-to-room/add-to-room.render-template.tsx b/code/components/room-components/add-template-to-room/add-to-room.render-template.tsx new file mode 100644 index 00000000..367794c4 --- /dev/null +++ b/code/components/room-components/add-template-to-room/add-to-room.render-template.tsx @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import React from "react"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { useJsonTemplate } from "../hooks/use-json-template"; + +type AddToRoomRenderTemplateProps = { + width: number; + height: number; + readOnly: boolean; +}; + +export function AddToRoomRenderTemplate( + params: Readonly, +) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [templateData, setTemplateData] = React.useState(null); + + const room = useAddTemplateToRoom((state) => state.room); + const template = useAddTemplateToRoom((state) => state.template); + const templateParameters = useAddTemplateToRoom((state) => state.parameters); + const selectedNode = useAddTemplateToRoom((state) => state.selectedNode); + const renderSize = useAddTemplateToRoom((state) => state.render.size); + const renderScale = useAddTemplateToRoom((state) => state.render.scale); + const setSelectedNode = useAddTemplateToRoom( + (state) => state.setSelectedNode, + ); + const setTemplateRender = useAddTemplateToRoom( + (state) => state.setTemplateRender, + ); + + const { getTemplateToHTML } = useJsonTemplate(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [nodesHTML, setNodesHTML] = React.useState(null); + + React.useEffect(() => { + if (!template) return; + + try { + const templateData = JSON.parse(template.templateData); + setTemplateData(templateData); + } catch { + setTemplateData(null); + } + }, [template]); + + React.useEffect(() => { + if (!templateData) return; + + try { + const { + structure: model, + scale, + size, + } = getTemplateToHTML( + templateData, + { + width: params.width, + height: params.height, + }, + templateParameters, + selectedNode, + setSelectedNode, + { + readOnly: params.readOnly, + }, + ); + + setTemplateRender(size, scale); + setNodesHTML(model); + } catch (ex) { + console.error(ex); + setNodesHTML(null); + } + }, [templateData, templateParameters, selectedNode, setSelectedNode]); + + const diffX = React.useMemo( + () => params.width - renderSize.width * renderScale, + [renderScale, renderSize], + ); + const diffY = React.useMemo( + () => params.height - renderSize.height * renderScale, + [renderScale, renderSize], + ); + + if (!template || !room) { + return null; + } + + return ( +
+
+ {nodesHTML} +
+
+ ); +} diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-template.tsx b/code/components/room-components/add-template-to-room/add-to-room.select-template.tsx similarity index 53% rename from code/components/use-cases/templates/components/add-to-room/add-to-room.select-template.tsx rename to code/components/room-components/add-template-to-room/add-to-room.select-template.tsx index 3917e4de..b296e79c 100644 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-template.tsx +++ b/code/components/room-components/add-template-to-room/add-to-room.select-template.tsx @@ -2,46 +2,40 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Button } from "@/components/ui/button"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useInView } from "react-intersection-observer"; -import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { DialogDescription } from "@/components/ui/dialog"; import React from "react"; -import { useAddToRoom } from "../../store/add-to-room"; -import { useTemplatesUseCase } from "../../store/store"; -import { TemplateEntity } from "../templates/types"; import { getTemplates } from "@/api/get-templates"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { LayoutTemplateIcon } from "lucide-react"; +import { LayoutTemplateIcon, LoaderCircle } from "lucide-react"; import { AddToRoomTemplate } from "./add-to-room.template"; import { useWeave } from "@inditextech/weave-react"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { TemplateEntity } from "../templates-library/types"; const TEMPLATES_LIMIT = 20; export function AddToRoomSelectTemplate() { const instance = useWeave((state) => state.instance); - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const addToRoomOpen = useTemplatesUseCase((state) => state.addToRoom.open); - - const frameName = useAddToRoom((state) => state.frameName); - const setFrameName = useAddToRoom((state) => state.setFrameName); - const template = useAddToRoom((state) => state.template); - const setStep = useAddToRoom((state) => state.setStep); + const selectedImages = useAddTemplateToRoom((state) => state.images); + const visible = useAddTemplateToRoom((state) => state.visible); + const room = useAddTemplateToRoom((state) => state.room); const [templates, setTemplates] = React.useState([]); const query = useInfiniteQuery({ - queryKey: ["getTemplates", instanceId], + queryKey: ["getTemplates", room?.id ?? "", "imageTemplate"], queryFn: async ({ pageParam }) => { - if (!instanceId) { + if (!room) { return []; } return await getTemplates( - instanceId ?? "", + room.id, + "imageTemplate", + selectedImages.length, pageParam as number, TEMPLATES_LIMIT, ); @@ -59,7 +53,7 @@ export function AddToRoomSelectTemplate() { } return undefined; // no more pages }, - enabled: addToRoomOpen, + enabled: visible, }); React.useEffect(() => { @@ -90,21 +84,38 @@ export function AddToRoomSelectTemplate() { Select the template to use. -
- {templates.length === 0 && ( -
-
- -
- No templates defined +
+ {!query.isLoading && + !query.isFetchingNextPage && + templates.length === 0 && ( +
+
+ +
+ No templates defined +
+ )} + {query.isLoading && !query.isFetchingNextPage && ( +
+ +
+

+ LOADING IMAGE TEMPLATES +

+

Please wait...

+
)} - {templates.length > 0 && ( + {query.isFetched && templates.length > 0 && (
{ if (!instance) { return; @@ -137,50 +148,6 @@ export function AddToRoomSelectTemplate() { )}
- - Define a name for the frame - -
-
- - { - setFrameName(e.target.value); - }} - onFocus={() => { - window.weaveOnFieldFocus = true; - }} - onBlurCapture={() => { - window.weaveOnFieldFocus = false; - }} - /> -
-
-
- - - -
); diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.template.tsx b/code/components/room-components/add-template-to-room/add-to-room.template.tsx similarity index 50% rename from code/components/use-cases/templates/components/add-to-room/add-to-room.template.tsx rename to code/components/room-components/add-template-to-room/add-to-room.template.tsx index 6aa13fa4..171849d4 100644 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.template.tsx +++ b/code/components/room-components/add-template-to-room/add-to-room.template.tsx @@ -3,39 +3,40 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import { TemplateEntity } from "../templates/types"; -import { useAddToRoom } from "../../store/add-to-room"; import { CheckIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -import { useTemplatesUseCase } from "../../store/store"; -import { useAmountSlotsTemplate } from "../../hooks/use-amount-slots-template"; +import { TemplateEntity } from "../templates-library/types"; +import { useAddTemplateToRoom } from "@/store/add-template-to-room"; +import { Badge } from "@/components/ui/badge"; type TemplateProps = { template: TemplateEntity; }; export const AddToRoomTemplate = ({ template }: Readonly) => { - const selectedImages = useTemplatesUseCase((state) => state.images.selected); - - const selectedTemplate = useAddToRoom((state) => state.template); - const setTemplate = useAddToRoom((state) => state.setTemplate); + const selectedImages = useAddTemplateToRoom((state) => state.images); + const selectedTemplate = useAddTemplateToRoom((state) => state.template); + const setStep = useAddTemplateToRoom((state) => state.setStep); + const setTemplate = useAddTemplateToRoom((state) => state.setTemplate); const isSelected = React.useMemo(() => { return selectedTemplate?.templateId === template.templateId; }, [selectedTemplate, template]); - const amountOfImageTemplates = useAmountSlotsTemplate({ template }); + if (!template) { + return null; + } return (
amountOfImageTemplates, + selectedImages.length > template.imageSlots, }, )} onClick={() => { @@ -43,7 +44,8 @@ export const AddToRoomTemplate = ({ template }: Readonly) => { setTemplate(undefined); return; } - if (selectedImages.length <= amountOfImageTemplates) { + if (selectedImages.length <= template.imageSlots) { + setStep("configuration"); setTemplate(template); } }} @@ -52,17 +54,23 @@ export const AddToRoomTemplate = ({ template }: Readonly) => { className={cn( "bg-[#d6d6d6] w-full aspect-video block object-contain relative", { - ["opacity-60"]: selectedImages.length > amountOfImageTemplates, + ["opacity-60"]: selectedImages.length > template.imageSlots, ["transition-transform duration-500 group-hover:opacity-60"]: - selectedImages.length <= amountOfImageTemplates, + selectedImages.length <= template.imageSlots, }, )} src={template.templateImage} alt="A template" data-template-data={template.templateData} /> +
+ {template.imageSlots} image slot(s) +
+
+ {template.name} +
{isSelected && ( -
+
)} diff --git a/code/components/room-components/hooks/use-context-menu.tsx b/code/components/room-components/hooks/use-context-menu.tsx index 583e389e..fb679a26 100644 --- a/code/components/room-components/hooks/use-context-menu.tsx +++ b/code/components/room-components/hooks/use-context-menu.tsx @@ -33,7 +33,7 @@ import { Lock, EyeOff, Link, - // PackagePlus, + PackagePlus, PackageOpen, Paperclip, PanelLeftRightDashed, @@ -47,7 +47,7 @@ import { useIAChat } from "@/store/ia-chat"; import Konva from "konva"; import { useHandleGuides } from "./use-handle-guides"; import { formatForDisplay } from "@tanstack/react-hotkeys"; -// import { useJsonTemplate } from "./use-json-template"; +import { useJsonTemplate } from "./use-json-template"; function useContextMenu() { const instance = useWeave((state) => state.instance); @@ -75,12 +75,16 @@ function useContextMenu() { const aiChatEnabled = useIAChat((state) => state.enabled); - const setSaveDialogVisible = useTemplates( + const setTemplateData = useTemplates((state) => state.setData); + const setTemplateSaveDialogVisible = useTemplates( (state) => state.setSaveDialogVisible, ); + const setTemplateSaveDialogKind = useTemplates( + (state) => state.setSaveDialogKind, + ); const { isExporting } = useExportPageToImageServerSide(); - // const { generateJsonTemplate } = useJsonTemplate(); + const { generateTemplate, generateImageTemplate } = useJsonTemplate(); const promptInputAttachmentsController = usePromptInputAttachments(); @@ -455,57 +459,72 @@ function useContextMenu() { }); } - // if (!singleLocked && nodes.length > 0) { - // options.push({ - // id: "div-templates-1", - // type: "divider", - // }); - // // SAVE AS TEMPLATE - // options.push({ - // id: "save-as-template", - // type: "button", - // label: ( - //
- //
Save as template
- //
- // ), - // icon: , - // disabled: !["selectionTool"].includes(actActionActive ?? ""), - // onClick: () => { - // setSaveDialogVisible(true); - // setContextMenuShow(false); - // }, - // }); - // // SAVE AS TEMPLATE - // options.push({ - // id: "save-as-json-template", - // type: "button", - // label: ( - //
- //
Save as JSON template
- //
- // ), - // icon: , - // disabled: !["selectionTool"].includes(actActionActive ?? ""), - // onClick: async () => { - // try { - // const template = generateJsonTemplate(nodes); - // await navigator.clipboard.writeText(JSON.stringify(template)); - // toast.success("JSON template copied to clipboard."); - // } catch (error) { - // console.error(error); - // if (error instanceof Error && error.cause === "NoInstance") { - // toast.error("Weave instance is not available."); - // } - // if (error instanceof Error && error.cause === "NoNodesSelected") { - // toast.error("No nodes selected to generate JSON template."); - // } - // } - - // setContextMenuShow(false); - // }, - // }); - // } + if (!singleLocked && nodes.length > 0) { + options.push({ + id: "div-templates-1", + type: "divider", + }); + // SAVE AS TEMPLATE + options.push({ + id: "save-as-template", + type: "button", + label: ( +
+
Save as template
+
+ ), + icon: , + disabled: !["selectionTool"].includes(actActionActive ?? ""), + onClick: async () => { + try { + const template = generateTemplate(nodes); + + setTemplateData(template); + setTemplateSaveDialogKind("template"); + setTemplateSaveDialogVisible(true); + setContextMenuShow(false); + } catch (error) { + console.error(error); + if (error instanceof Error && error.cause === "NoInstance") { + toast.error("Weave instance is not available."); + } + if (error instanceof Error && error.cause === "NoNodesSelected") { + toast.error("No nodes selected to generate JSON template."); + } + } + }, + }); + // SAVE AS IMAGE TEMPLATE + options.push({ + id: "save-as-image-template", + type: "button", + label: ( +
+
Save as images template
+
+ ), + icon: , + disabled: !["selectionTool"].includes(actActionActive ?? ""), + onClick: async () => { + try { + const template = generateImageTemplate(nodes); + + setTemplateData(template); + setTemplateSaveDialogKind("imageTemplate"); + setTemplateSaveDialogVisible(true); + setContextMenuShow(false); + } catch (error) { + console.error(error); + if (error instanceof Error && error.cause === "NoInstance") { + toast.error("Weave instance is not available."); + } + if (error instanceof Error && error.cause === "NoNodesSelected") { + toast.error("No nodes selected to generate JSON template."); + } + } + }, + }); + } if (!singleLocked && nodes.length > 0) { // SEPARATOR @@ -729,7 +748,8 @@ function useContextMenu() { aiChatEnabled, promptInputAttachmentsController, setLinkedNode, - setSaveDialogVisible, + setTemplateSaveDialogKind, + setTemplateSaveDialogVisible, setExportConfigVisible, setExportNodes, isExporting, diff --git a/code/components/room-components/hooks/use-create-page-from-template.tsx b/code/components/room-components/hooks/use-create-page-from-template.tsx new file mode 100644 index 00000000..afc14c2d --- /dev/null +++ b/code/components/room-components/hooks/use-create-page-from-template.tsx @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +// +// SPDX-License-Identifier: Apache-2.0 + +import React from "react"; +import { v4 as uuidv4 } from "uuid"; +import { WeaveStateManipulation } from "@inditextech/weave-sdk"; +import { useWeave } from "@inditextech/weave-react"; +import { toast } from "sonner"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postPage } from "@/api/pages/post-page"; +import { WeaveStoreAzureWebPubsub } from "@inditextech/weave-store-azure-web-pubsub/client"; +import { useCollaborationRoom } from "@/store/store"; +import { getRoom } from "@/api/get-room"; + +type UseCreatePageFromTemplateProps = { + roomId: string; +}; + +export const useCreatePageFromTemplate = ( + hookParams: Readonly, +) => { + const createToastRef = React.useRef(null); + + const instance = useWeave((state) => state.instance); + + const pagesAmount = useCollaborationRoom((state) => state.pages.amount); + const setPagesActualPage = useCollaborationRoom( + (state) => state.setPagesActualPage, + ); + const setPagesActualPageId = useCollaborationRoom( + (state) => state.setPagesActualPageId, + ); + + const queryClient = useQueryClient(); + + const handleSwitchPage = React.useCallback( + async (pageIndex: number, pageId: string) => { + if (!instance) return; + + const queryKeyPages = ["getPages", hookParams.roomId]; + queryClient.invalidateQueries({ queryKey: queryKeyPages }); + + const queryKey = ["getPagesInfinite", hookParams.roomId]; + queryClient.invalidateQueries({ queryKey }); + + const store = instance.getStore() as WeaveStoreAzureWebPubsub; + + try { + const data = await queryClient.fetchQuery({ + queryKey: ["roomData", pageId], + queryFn: () => getRoom(pageId), + }); + + store.switchToRoom(pageId, data); + // eslint-disable-next-line no-empty + } catch { + store.switchToRoom(pageId, undefined); + } + + setPagesActualPage(pageIndex); + setPagesActualPageId(pageId); + }, + [ + instance, + queryClient, + hookParams.roomId, + setPagesActualPage, + setPagesActualPageId, + ], + ); + + const createPage = useMutation({ + mutationFn: async (params: { + pageId: string; + name: string; + thumbnail: string; + templateId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + templateData: any; + }) => { + const boundingBox = WeaveStateManipulation.getNodesBoundingBox( + params.templateData, + ); + + const payloadParams = { + pageId: params.pageId, + name: params.name, + thumbnail: params.thumbnail, + templateId: params.templateId, + target: { + id: "mainLayer", + position: { + x: -1 * (boundingBox.width / 2), + y: -1 * (boundingBox.height / 2), + }, + }, + }; + return await postPage(hookParams.roomId, payloadParams); + }, + onSettled: () => { + if (createToastRef.current) { + toast.dismiss(createToastRef.current); + } + }, + onSuccess: async (_, { pageId }) => { + await handleSwitchPage(pagesAmount + 1, pageId); + toast.success("Template materialized on the page successfully"); + }, + onError: () => { + toast.error("Error creating page"); + }, + }); + + const createPageFromTemplate = React.useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (templateId: string, templateData: any) => { + createToastRef.current = toast.loading("Creating page...", { + duration: Infinity, + }); + createPage.mutate({ + pageId: `${hookParams.roomId}-${uuidv4()}`, + name: "New page", + thumbnail: "", + templateId, + templateData, + }); + }, + [], + ); + + return { + createPageFromTemplate, + }; +}; diff --git a/code/components/room-components/hooks/use-json-template.tsx b/code/components/room-components/hooks/use-json-template.tsx index 76a5f6f3..93286d74 100644 --- a/code/components/room-components/hooks/use-json-template.tsx +++ b/code/components/room-components/hooks/use-json-template.tsx @@ -7,6 +7,7 @@ import { Weave } from "@inditextech/weave-sdk"; import { BoundingBox, WeaveSelection } from "@inditextech/weave-types"; import { useWeave } from "@inditextech/weave-react"; import Konva from "konva"; +import { cn } from "@/lib/utils"; function getGroupTopLeft(instance: Weave, nodes: WeaveSelection[]) { if (!nodes.length) return null; @@ -25,51 +26,333 @@ function getGroupTopLeft(instance: Weave, nodes: WeaveSelection[]) { return { x: minX, y: minY }; } -export const useJsonTemplate = () => { - const instance = useWeave((state) => state.instance); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractNodesMetadata(template: any, filter: string[] = []): any { + const templateNodesMetadata = []; - const generateJsonTemplate = React.useCallback( - (nodes: WeaveSelection[]) => { - if (!instance) - throw new Error("Instance is required to generate JSON template", { - cause: "NoInstance", - }); + for (const node of template.nodes) { + const nodeMetadata = { + id: node.id, + kind: node.kind, + editable: node.editable, + }; - if (nodes.length === 0) { - throw new Error("No nodes selected to generate JSON template", { - cause: "NoNodesSelected", - }); - } + if (node.children) { + templateNodesMetadata.push( + ...extractNodesMetadata({ nodes: node.children }, filter), + ); + } - const topLeft = getGroupTopLeft(instance, nodes); + if (filter.length === 0 || filter.includes(node.kind)) { + templateNodesMetadata.push(nodeMetadata); + } + } - const mappedNodes = []; - for (const node of nodes) { - mappedNodes.push( - mapNode( - instance, - node.instance, - topLeft ?? { x: 0, y: 0 }, - instance.getStage() as Konva.Container, - ), + return templateNodesMetadata; +} + +function nodeDefaults( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + defaults: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record, +) { + defaults[node.id] = { + ...defaults[node.id], + nodeId: node.id, + kind: node.kind, + properties: { + ...node.defaultProperties, + ...(parameters[node.id] && { ...parameters[node.id]?.properties }), + }, + }; + + if ( + Object.keys(defaults[node.id].properties).length === 0 && + ["image", "text"].includes(node.kind) + ) { + delete defaults[node.id]; + } + + if (node.children) { + for (const childNode of node.children) { + nodeDefaults(defaults, childNode, parameters); + } + } +} + +function templateSetDefaults( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const defaults: Record = {}; + + for (const node of template.nodes) { + nodeDefaults(defaults, node, parameters); + } + + return defaults; +} + +function defineScale( + nodeSize: { width: number; height: number }, + parentSize: { width: number; height: number }, +) { + const padding = 20; + const scaleX = (parentSize.width - 2 * padding) / nodeSize.width; + const scaleY = (parentSize.height - 2 * padding) / nodeSize.height; + return Math.min(scaleX, scaleY); +} + +function nodeToHTML( + scale: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record, + selectedNode: string | null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: (params: any) => void, + params: { + readOnly: boolean; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + switch (node.kind) { + case "frame": { + return ( +
+ { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + node.children.map((childNode: any) => + nodeToHTML( + scale, + childNode, + parameters, + selectedNode, + action, + params, + ), + ) + } +
+ ); + } + + case "image": { + if ( + parameters[node.id] && + parameters[node.id]?.properties?.image?.source + ) { + return ( + { + action(node.id); + } + : undefined + } + className={cn({ + ["cursor-default"]: params.readOnly, + ["cursor-pointer hover:ring-4 hover:ring-blue-500"]: + !params.readOnly, + ["ring-4 ring-red-500"]: + selectedNode === node.id && !params.readOnly, + })} + style={{ + width: node.width, + height: node.height, + position: "absolute", + top: node.y, + left: node.x, + background: !params.readOnly ? "#333333" : "transparent", + objectFit: parameters[node.id].properties.fit, + }} + /> ); } - return { - version: "1.0", - name: "test", - nodes: mappedNodes, - }; - }, - [instance], - ); + if (params.readOnly) { + return null; + } + + return ( +
{ + action(node.id); + }} + className={cn("cursor-pointer hover:ring-4 hover:ring-blue-500", { + ["ring-4 ring-red-500"]: selectedNode === node.id, + })} + style={{ + width: node.width, + height: node.height, + position: "absolute", + top: node.y, + left: node.x, + background: "#333333", + cursor: "pointer", + }} + /> + ); + } + + case "text": { + let textToRender = node.defaultProperties.text; + if (parameters[node.id]) { + textToRender = parameters[node.id].properties.text || textToRender; + } + + return ( +
{ + action(node.id); + }} + className={cn("hover:ring-4 hover:ring-blue-500", { + ["ring-4 ring-red-500"]: selectedNode === node.id, + })} + style={{ + width: node.width, + height: node.height, + position: "absolute", + fontFamily: node.defaultProperties.fontFamily, + fontSize: node.defaultProperties.fontSize, + textAlign: node.defaultProperties.align, + verticalAlign: node.defaultProperties.verticalAlign, + color: node.defaultProperties.fill, + top: node.y, + left: node.x, + lineHeight: 1, + cursor: "pointer", + }} + > + {textToRender} +
+ ); + } + + default: { + return
{`Unknown node kind: ${node.kind}`}
; + } + } +} + +function templateToHTMLRepresentation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: any, + parentSize: { width: number; height: number }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record, + selectedNode: string | null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: (params: any) => void, + params: { + readOnly: boolean; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const structure = []; + + const maxSize = { width: -Infinity, height: -Infinity }; + + if (template.nodes.length === 0) { + return { structure: [], size: { width: 0, height: 0 } }; + } + + for (const node of template.nodes) { + maxSize.width = Math.max(maxSize.width, node.x + node.width); + maxSize.height = Math.max(maxSize.height, node.y + node.height); + } + + const scale = defineScale(maxSize, parentSize); + + for (const node of template.nodes) { + structure.push( + nodeToHTML(scale, node, parameters, selectedNode, action, params), + ); + maxSize.width = Math.max(maxSize.width, node.x + node.width); + maxSize.height = Math.max(maxSize.height, node.y + node.height); + } + + return { structure, scale, size: maxSize }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function findNodeById(template: any, nodeId: string): any | null { + for (const node of template.nodes) { + if (node.id === nodeId) { + return node; + } + + if (node.children) { + const foundInChildren = findNodeById({ nodes: node.children }, nodeId); + if (foundInChildren) { + return foundInChildren; + } + } + } + + return null; +} + +const mapNode = ( + instance: Weave, + node: Konva.Node, + topLeft: Konva.Vector2d, + relativeTo: Konva.Container, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any => { + const nodeType = node.getAttrs().nodeType; + + const boundingBox: BoundingBox = node.getClientRect({ + skipStroke: true, + relativeTo, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeHandler = instance.getNodeHandler(nodeType); + + if (!nodeHandler) { + throw new Error(`No node handler found for node type ${nodeType}`); + } + + const nodeSerialized = nodeHandler.serialize(node); return { - generateJsonTemplate, + ...nodeSerialized, + props: { + ...nodeSerialized.props, + x: Number((boundingBox.x - topLeft.x).toFixed(6)), + y: Number((boundingBox.y - topLeft.y).toFixed(6)), + }, }; }; -const mapNode = ( +const mapImageNode = ( instance: Weave, node: Konva.Node, topLeft: Konva.Vector2d, @@ -78,7 +361,7 @@ const mapNode = ( ): any => { const nodeType = node.getAttrs().nodeType; - const boundigBox: BoundingBox = node.getClientRect({ + const boundingBox: BoundingBox = node.getClientRect({ skipStroke: true, relativeTo, }); @@ -98,34 +381,221 @@ const mapNode = ( return { id: node.id(), - x: Number((boundigBox.x - topLeft.x).toFixed(6)), - y: Number((boundigBox.y - topLeft.y).toFixed(6)), + x: Number((boundingBox.x - topLeft.x).toFixed(6)), + y: Number((boundingBox.y - topLeft.y).toFixed(6)), width: - Number(boundigBox.width.toFixed(6)) - node.getAttrs().strokeWidth * 2, + Number(boundingBox.width.toFixed(6)) - + node.getAttrs().borderWidth * 2, height: - Number(boundigBox.height.toFixed(6)) - - node.getAttrs().strokeWidth * 2, + Number(boundingBox.height.toFixed(6)) - + node.getAttrs().borderWidth * 2, kind: "frame", children: nodes.map((childNode) => // eslint-disable-next-line @typescript-eslint/no-explicit-any - mapNode(instance, childNode, { x: 0, y: 0 }, frameContainer as any), + mapImageNode( + instance, + childNode, + { x: 0, y: 0 }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + frameContainer as any, + ), ), + defaultProperties: { + name: "", + width: + Number(boundingBox.width.toFixed(6)) - + node.getAttrs().borderWidth * 2, + height: + Number(boundingBox.height.toFixed(6)) - + node.getAttrs().borderWidth * 2, + }, + editable: false, + optional: false, + }; + } + + case "image": { + return { + id: node.id(), + x: Number((boundingBox.x - topLeft.x).toFixed(6)), + y: Number((boundingBox.y - topLeft.y).toFixed(6)), + width: + Number(boundingBox.width.toFixed(6)) - + node.getAttrs().strokeWidth * 2, + height: + Number(boundingBox.height.toFixed(6)) - + node.getAttrs().strokeWidth * 2, + kind: "image", editable: true, optional: true, }; } - default: { + case "text": { return { id: node.id(), - x: Number((boundigBox.x - topLeft.x).toFixed(6)), - y: Number((boundigBox.y - topLeft.y).toFixed(6)), - width: Number(boundigBox.width.toFixed(6)), - height: Number(boundigBox.height.toFixed(6)), - kind: nodeType === "rectangle" ? "image" : nodeType, + x: Number((boundingBox.x - topLeft.x).toFixed(6)), + y: Number((boundingBox.y - topLeft.y).toFixed(6)), + width: + Number(boundingBox.width.toFixed(6)) - + node.getAttrs().strokeWidth * 2, + height: + Number(boundingBox.height.toFixed(6)) - + node.getAttrs().strokeWidth * 2, + kind: "text", + defaultProperties: { + fontFamily: node.getAttrs().fontFamily, + fontSize: node.getAttrs().fontSize, + align: node.getAttrs().align, + verticalAlign: node.getAttrs().verticalAlign, + fill: node.getAttrs().fill, + layout: "smart", + text: node.getAttrs().text, + }, editable: true, optional: true, }; } + + default: { + return { + id: node.id(), + x: Number((boundingBox.x - topLeft.x).toFixed(6)), + y: Number((boundingBox.y - topLeft.y).toFixed(6)), + width: Number(boundingBox.width.toFixed(6)), + height: Number(boundingBox.height.toFixed(6)), + kind: nodeType, + editable: false, + optional: false, + }; + } } }; + +export const useJsonTemplate = () => { + const instance = useWeave((state) => state.instance); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getNodeMetadata = React.useCallback((template: any, nodeId: string) => { + return findNodeById(template, nodeId); + }, []); + + const getTemplateToHTML = React.useCallback( + ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: any, + parentSize: { width: number; height: number }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record, + selectedNode: string | null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: (params: any) => void, + params: { + readOnly: boolean; + }, + ) => { + return templateToHTMLRepresentation( + template, + parentSize, + parameters, + selectedNode, + action, + params, + ); + }, + [], + ); + + const setupTemplateDefaults = React.useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (template: any, parameters: Record) => { + return templateSetDefaults(template, parameters); + }, + [], + ); + + const getTemplateNodesMetadata = React.useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (template: any, filter: string[] = []) => { + return extractNodesMetadata(template, filter); + }, + [], + ); + + const generateTemplate = React.useCallback( + (nodes: WeaveSelection[]) => { + if (!instance) + throw new Error("Instance is required to generate JSON template", { + cause: "NoInstance", + }); + + if (nodes.length === 0) { + throw new Error("No nodes selected to generate JSON template", { + cause: "NoNodesSelected", + }); + } + + const topLeft = getGroupTopLeft(instance, nodes); + + const mappedNodes = []; + for (const node of nodes) { + mappedNodes.push( + mapNode( + instance, + node.instance, + topLeft ?? { x: 0, y: 0 }, + instance.getStage() as Konva.Container, + ), + ); + } + + return mappedNodes; + }, + [instance], + ); + + const generateImageTemplate = React.useCallback( + (nodes: WeaveSelection[]) => { + if (!instance) + throw new Error("Instance is required to generate JSON template", { + cause: "NoInstance", + }); + + if (nodes.length === 0) { + throw new Error("No nodes selected to generate JSON template", { + cause: "NoNodesSelected", + }); + } + + const topLeft = getGroupTopLeft(instance, nodes); + + const mappedNodes = []; + for (const node of nodes) { + mappedNodes.push( + mapImageNode( + instance, + node.instance, + topLeft ?? { x: 0, y: 0 }, + instance.getStage() as Konva.Container, + ), + ); + } + + return { + version: "1.0", + name: "test", + nodes: mappedNodes, + }; + }, + [instance], + ); + + return { + setupTemplateDefaults, + getNodeMetadata, + getTemplateToHTML, + getTemplateNodesMetadata, + generateTemplate, + generateImageTemplate, + }; +}; diff --git a/code/components/room-components/hooks/use-update-page-thumbnail.tsx b/code/components/room-components/hooks/use-update-page-thumbnail.tsx index 03cbc888..510e65d9 100644 --- a/code/components/room-components/hooks/use-update-page-thumbnail.tsx +++ b/code/components/room-components/hooks/use-update-page-thumbnail.tsx @@ -12,6 +12,7 @@ import { putPageThumbnail } from "@/api/pages/put-page-thumbnail"; import { sleep } from "@/lib/utils"; import { getEmmiter } from "./use-tasks-events"; import Konva from "konva"; +import { debounce } from "lodash"; export const useUpdatePageThumbnail = () => { const [roomState, setRoomState] = React.useState< @@ -21,6 +22,13 @@ export const useUpdatePageThumbnail = () => { const instance = useWeave((state) => state.instance); const isRoomSwitching = useWeave((state) => state.room.switching); + const [lastUpdate, setLastUpdate] = React.useState(0); + const [doUpdatePageThumbnail, setDoUpdatePageThumbnail] = + React.useState(false); + const [actualLeaderId, setActualLeaderId] = React.useState( + null, + ); + const leaderId = useCollaborationRoom((state) => state.leaderId); const clientId = useCollaborationRoom((state) => state.clientId); const roomId = useCollaborationRoom((state) => state.room); @@ -36,6 +44,15 @@ export const useUpdatePageThumbnail = () => { setRoomState("idle"); }, [actualPageId]); + React.useEffect(() => { + if (actualLeaderId !== leaderId) { + setActualLeaderId(leaderId); + if (leaderId === clientId) { + setDoUpdatePageThumbnail(true); + } + } + }, [clientId, leaderId, actualLeaderId]); + React.useEffect(() => { if (!instance) { return; @@ -96,7 +113,7 @@ export const useUpdatePageThumbnail = () => { return; } - const takePageThumbnail = async (isLoaded: boolean = false) => { + const takePageThumbnail = async () => { if (!instance) { return; } @@ -114,10 +131,6 @@ export const useUpdatePageThumbnail = () => { return; } - if (!isLoaded && roomState !== "loaded") { - return; - } - if (!actualPageElement) { return; } @@ -138,49 +151,48 @@ export const useUpdatePageThumbnail = () => { return; } - clonedStage.toBlob({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const blob: any = await clonedStage.toBlob({ x: exportRect.x, y: exportRect.y, width: exportRect.width, height: exportRect.height, pixelRatio: (1 / stage.scaleX()) * 0.3, mimeType: "image/png", - callback: (blob: Blob | null) => { - if (!blob) { - return; - } - - const reader = new FileReader(); - - reader.onloadend = () => { - getEmmiter().emit("pageThumbnailUpdated", { - roomId: roomId ?? "", - pageId: actualPageId ?? "", - dataURL: reader.result as string, - }); - }; - - reader.onerror = () => { - console.error(); - }; - - reader.readAsDataURL(blob); - - if (leaderId === clientId) { - updatePageThumbnail.mutate({ - roomId: roomId ?? "", - pageId: actualPageId ?? "", - thumbnail: blob, - }); - } - }, }); - }; - const onAsyncElementsLoadedHandler = () => { - takePageThumbnail(true); + if (!blob) { + return; + } + + const reader = new FileReader(); + + reader.onloadend = () => { + getEmmiter().emit("pageThumbnailUpdated", { + roomId: roomId ?? "", + pageId: actualPageId ?? "", + dataURL: reader.result as string, + }); + }; + + reader.onerror = () => { + console.error(); + }; + + reader.readAsDataURL(blob); + + if (leaderId === clientId && Date.now() > lastUpdate) { + setLastUpdate(Date.now()); + updatePageThumbnail.mutate({ + roomId: roomId ?? "", + pageId: actualPageId ?? "", + thumbnail: blob, + }); + } }; + const debouncedTakePageThumbnail = debounce(takePageThumbnail, 500); + const onRenderHandler = async () => { if (!instance) { return; @@ -190,26 +202,24 @@ export const useUpdatePageThumbnail = () => { await sleep(100); } - takePageThumbnail(); + debouncedTakePageThumbnail(); }; instance.addEventListener("onRender", onRenderHandler); - instance.addEventListener( - "onAsyncElementsLoaded", - onAsyncElementsLoadedHandler, - ); + + if (doUpdatePageThumbnail) { + setDoUpdatePageThumbnail(false); + debouncedTakePageThumbnail(); + } return () => { instance.removeEventListener("onRender", onRenderHandler); - instance.removeEventListener( - "onAsyncElementsLoaded", - onAsyncElementsLoadedHandler, - ); }; }, [ instance, roomId, updatePageThumbnail, + doUpdatePageThumbnail, leaderId, clientId, roomState, diff --git a/code/components/room-components/images-library/images-library.actions.tsx b/code/components/room-components/images-library/images-library.actions.tsx index 4478cd6c..8323c260 100644 --- a/code/components/room-components/images-library/images-library.actions.tsx +++ b/code/components/room-components/images-library/images-library.actions.tsx @@ -6,7 +6,7 @@ import React from "react"; import { v4 as uuidv4 } from "uuid"; import { toast } from "sonner"; import { useMutation } from "@tanstack/react-query"; -import { BrushCleaning, Trash } from "lucide-react"; +import { LayoutPanelTop, Trash } from "lucide-react"; import { WeaveStateElement } from "@inditextech/weave-types"; import { delImage } from "@/api/v2/del-image"; import { useWeave } from "@inditextech/weave-react"; @@ -15,6 +15,7 @@ import { postRemoveBackground as postRemoveBackgroundV2 } from "@/api/v2/post-re import { ImageEntity } from "./types"; import { cn } from "@/lib/utils"; import { useGetSession } from "../hooks/use-get-session"; +import { ImageInfo, useAddTemplateToRoom } from "@/store/add-template-to-room"; type ImagesLibraryActions = { images: ImageEntity[]; @@ -35,10 +36,41 @@ export const ImagesLibraryActions = ({ }: Readonly) => { const instance = useWeave((state) => state.instance); + const apiEndpoint = import.meta.env.VITE_API_V2_ENDPOINT; + const hubName = import.meta.env.VITE_API_ENDPOINT_HUB_NAME; + const { session } = useGetSession(); const clientId = useCollaborationRoom((state) => state.clientId); const room = useCollaborationRoom((state) => state.room); + const actualPageId = useCollaborationRoom( + (state) => state.pages.actualPageId, + ); + + const setRoomAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setRoom, + ); + const setPageAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setPage, + ); + const setStepAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setStep, + ); + const setTemplateAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setTemplate, + ); + const setTemplateParametersAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setTemplateParameters, + ); + const setImagesAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setImages, + ); + const setVisibleAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setVisible, + ); + const setInitializedAddTemplateToPageDialog = useAddTemplateToRoom( + (state) => state.setInitialized, + ); const mutationUploadV2 = useMutation({ mutationFn: async ({ @@ -206,15 +238,43 @@ export const ImagesLibraryActions = ({ key="remove-background-selected" className="cursor-pointer flex gap-2 justify-center items-center h-[40px] font-inter text-xs text-center bg-transparent hover:text-[#c9c9c9]" onClick={() => { - for (const image of realSelectedImages) { - handleRemoveBackground(image); - } - setShowSelection(false); + if (!room) return; - setSelectedImages([]); + if (!actualPageId) return; + + setRoomAddTemplateToPageDialog({ + id: room, + name: "", + create: false, + }); + setPageAddTemplateToPageDialog({ + id: actualPageId, + name: "", + }); + setStepAddTemplateToPageDialog("select-template"); + setTemplateAddTemplateToPageDialog(undefined); + setTemplateParametersAddTemplateToPageDialog({}); + + const selectedImagesInfo: ImageInfo[] = selectedImages.map( + (image) => ({ + id: image.imageId, + url: `${apiEndpoint}/${hubName}/rooms/${room}/images/${image.imageId}`, + size: { + width: image.width ?? 0, + height: image.height ?? 0, + }, + }), + ); + const sortedSelectedImagesInfo = selectedImagesInfo.sort((a, b) => + a.id.localeCompare(b.id), + ); + + setImagesAddTemplateToPageDialog(sortedSelectedImagesInfo); + setVisibleAddTemplateToPageDialog(true); + setInitializedAddTemplateToPageDialog(false); }} > - + , ); } diff --git a/code/components/room-components/images-library/images-library.tsx b/code/components/room-components/images-library/images-library.tsx index 71977534..8a58f157 100644 --- a/code/components/room-components/images-library/images-library.tsx +++ b/code/components/room-components/images-library/images-library.tsx @@ -360,15 +360,7 @@ export const ImagesLibrary = () => { const newSelectedImages = []; for (const image of imagesToRender) { - const appImage = appImages.find( - (appImage) => appImage.props.imageId === image.imageId, - ); - - if ( - typeof appImage === "undefined" && - ["completed"].includes(image.status) && - image.removalJobId === null - ) { + if (["completed"].includes(image.status) && image.removalJobId === null) { newSelectedImages.push(image); } } @@ -625,17 +617,8 @@ export const ImagesLibrary = () => {
{imageComponent} {showSelection && - typeof appImage === "undefined" && - !( - ["pending", "working"].includes( - image.status, - ) || - (image.removalJobId !== null && - image.removalStatus !== null && - ["pending", "working"].includes( - image.removalStatus, - )) - ) && ( + ["completed"].includes(image.status) && + image.removalJobId === null && (
{ )}
- + {typeof appImage !== "undefined" && ( <> { const getDownscaledImage = async () => { - const dataURL = await downscaleImageFromURL(imageUrl, { - maxWidth: 200, - maxHeight: 200, - }); - setDownscaledImageDataUrl(dataURL); + try { + const dataURL = await downscaleImageFromURL(imageUrl, { + maxWidth: 200, + maxHeight: 200, + }); + setDownscaledImageDataUrl(dataURL); + } catch (ex) { + console.error("Error downscaling image", ex); + } }; if (imageUrl) { diff --git a/code/components/room-components/overlay/save-template.tsx b/code/components/room-components/overlay/save-template.tsx index 5875e189..47f1b61a 100644 --- a/code/components/room-components/overlay/save-template.tsx +++ b/code/components/room-components/overlay/save-template.tsx @@ -13,14 +13,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import React from "react"; @@ -32,28 +24,33 @@ import { WEAVE_EXPORT_RETURN_FORMAT } from "@inditextech/weave-types"; import { WeaveNodesSelectionPlugin } from "@inditextech/weave-sdk"; import { postTemplate } from "@/api/post-template"; import { SidebarActive, useCollaborationRoom } from "@/store/store"; -import { getSelectionAsTemplate } from "@/components/utils/templates"; import { SIDEBAR_ELEMENTS } from "@/lib/constants"; import Konva from "konva"; +import { cn, sleep } from "@/lib/utils"; +import { useJsonTemplate } from "../hooks/use-json-template"; export function SaveTemplateDialog() { const inputRef = React.useRef(null); const room = useCollaborationRoom((state) => state.room); + const viewType = useCollaborationRoom((state) => state.viewType); const setSidebarActive = useCollaborationRoom( (state) => state.setSidebarActive, ); - const [templateData, setTemplateData] = React.useState(""); + const [templateData, setTemplateData] = React.useState(null); const [templateImage, setTemplateImage] = React.useState( undefined, ); + const [initialized, setInitialized] = React.useState(false); const [generatingImagePreview, setGeneratingImagePreview] = React.useState(false); const [linkedNodeType, setLinkedNodeType] = React.useState("none"); const [name, setName] = React.useState(""); const [saving, setSaving] = React.useState(false); + const templateJsonData = useTemplates((state) => state.data); + const saveDialogKind = useTemplates((state) => state.saveDialog.kind); const saveDialogVisible = useTemplates((state) => state.saveDialog.visible); const setSaveDialogVisible = useTemplates( (state) => state.setSaveDialogVisible, @@ -61,6 +58,8 @@ export function SaveTemplateDialog() { const instance = useWeave((state) => state.instance); + const { getTemplateNodesMetadata } = useJsonTemplate(); + const sidebarToggle = React.useCallback( (element: SidebarActive) => { setSidebarActive(element); @@ -100,20 +99,32 @@ export function SaveTemplateDialog() { WEAVE_EXPORT_RETURN_FORMAT.DATA_URL, )) as string; + await sleep(1000); + setGeneratingImagePreview(false); setTemplateImage(selectionPreviewURL); - - const template = getSelectionAsTemplate(instance); - setTemplateData(JSON.stringify(template)); } - setTemplateImage(undefined); - getSelectionPreviewImage(); if (saveDialogVisible) { + setTemplateImage(undefined); + getSelectionPreviewImage(); setLinkedNodeType("none"); + setInitialized(true); } + + return () => { + setInitialized(false); + }; }, [saveDialogVisible, instance]); + React.useEffect(() => { + try { + setTemplateData(JSON.stringify(templateJsonData)); + } catch { + setTemplateData(null); + } + }, [templateJsonData]); + React.useEffect(() => { if (saveDialogVisible) { setTimeout(() => { @@ -140,10 +151,16 @@ export function SaveTemplateDialog() { if (!room) { throw new Error("Room is not defined"); } + if (!saveDialogKind) { + throw new Error("Template kind is not defined"); + } + setSaving(true); return await postTemplate({ roomId: room ?? "", name, + kind: saveDialogKind, + imageSlots: saveDialogKind === "imageTemplate" ? editableNodes : 0, linkedNodeType: linkedNodeType, templateImage, templateData, @@ -179,6 +196,8 @@ export function SaveTemplateDialog() { (e: any) => { if (!saveDialogVisible) return; + if (!templateData) return; + if (!templateImage) return; if (e.key === "Enter") { @@ -201,132 +220,149 @@ export function SaveTemplateDialog() { }; }, [onKeyDown]); + const editableNodes = React.useMemo(() => { + if (!templateJsonData) return 0; + + if (saveDialogKind === "template") { + return 0; + } + + const metadata = getTemplateNodesMetadata(templateJsonData); + return metadata.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (m: any) => m.editable && ["image", "text"].includes(m.kind), + ).length; + }, [saveDialogKind, templateJsonData]); + return ( - setSaveDialogVisible(open)} - > - - - -
- - Save as Template - - - + +
+ + Save the selection as a template for future use. + +
+
+
+
+
+ {generatingImagePreview && ( +
+ generating selection thumbnail... +
+ )} + {!generatingImagePreview && ( + Template preview + )} +
+
+ + { + window.weaveOnFieldFocus = true; }} - > - - - -
- - Save the selection as a template for future use. - -
- -
-
-
- {generatingImagePreview && ( -
- generating selection thumbnail... -
- )} - {!generatingImagePreview && ( - Template preview + onBlurCapture={() => { + window.weaveOnFieldFocus = false; + }} + onChange={(e) => setName(e.target.value)} + className="w-full py-0 h-[40px] rounded-none !text-[14px] !border-black font-normal text-black text-left focus:outline-none bg-transparent shadow-none" + /> + {saveDialogKind === "imageTemplate" && ( + <> +
+
+
Editable nodes
+
{editableNodes}
+
+ )}
- - { - window.weaveOnFieldFocus = true; - }} - onBlurCapture={() => { - window.weaveOnFieldFocus = false; +
+
+ + +
-
-
- - - - -
- -
+ SAVE + + + + +
+ {generatingImagePreview && ( +
+
GENERATING TEMPLATE PREVIEW
+
please wait...
+
+ )} + ); } diff --git a/code/components/room-components/sidebar-selector.tsx b/code/components/room-components/sidebar-selector.tsx index ad91afd3..668c505d 100644 --- a/code/components/room-components/sidebar-selector.tsx +++ b/code/components/room-components/sidebar-selector.tsx @@ -143,7 +143,7 @@ export const SidebarSelector = ({ title }: Readonly) => { {formatForDisplay("Shift+G")} - {/* { e.stopPropagation(); @@ -155,7 +155,7 @@ export const SidebarSelector = ({ title }: Readonly) => { {formatForDisplay("Shift+T")} - */} + { diff --git a/code/components/room-components/templates-library/template.tsx b/code/components/room-components/templates-library/template.tsx index c0dbc1cc..39835ace 100644 --- a/code/components/room-components/templates-library/template.tsx +++ b/code/components/room-components/templates-library/template.tsx @@ -7,6 +7,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { useWeave } from "@inditextech/weave-react"; import { TemplateEntity } from "./types"; import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; type TemplateProps = { template: TemplateEntity; @@ -15,6 +16,11 @@ type TemplateProps = { onChange: (checked: string | boolean) => void; }; +const KINDS_MAP: Record = { + imageTemplate: "image template", + template: "template", +}; + export const Template = ({ template, selected, @@ -31,22 +37,24 @@ export const Template = ({
- A template -
-
{template.name}
+
+
+ {template.name} +
{showSelection && ( )}
+ A template +
+ {KINDS_MAP[template.kind]} + {template.kind === "imageTemplate" && ( + {template.imageSlots} image slot(s) + )} +
{template.removalJobId !== null && template.removalStatus !== null && ["pending", "working"].includes(template.removalStatus) && ( diff --git a/code/components/room-components/templates-library/templates-library.actions.tsx b/code/components/room-components/templates-library/templates-library.actions.tsx index 8b24371d..b0222084 100644 --- a/code/components/room-components/templates-library/templates-library.actions.tsx +++ b/code/components/room-components/templates-library/templates-library.actions.tsx @@ -12,13 +12,25 @@ import { TemplateEntity } from "./types"; import { cn } from "@/lib/utils"; import { delTemplate } from "@/api/del-template"; import { useGetSession } from "../hooks/use-get-session"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; type TemplatesLibraryActions = { selectedTemplates: TemplateEntity[]; + kind: "template" | "imageTemplate" | "all"; + setKind: (kind: "template" | "imageTemplate" | "all") => void; }; export const TemplatesLibraryActions = ({ selectedTemplates, + kind, + setKind, }: Readonly) => { const instance = useWeave((state) => state.instance); @@ -90,18 +102,49 @@ export const TemplatesLibraryActions = ({ } return ( -
+
0, - ["w-full justify-center"]: actions.length === 0, + ["w-full justify-start"]: actions.length === 0, })} > - {actions.length > 0 ? ( - "SELECTION ACTIONS" - ) : ( - select a template - )} +
{actions}
diff --git a/code/components/room-components/templates-library/templates-library.tsx b/code/components/room-components/templates-library/templates-library.tsx index 6707679a..cae1bbc2 100644 --- a/code/components/room-components/templates-library/templates-library.tsx +++ b/code/components/room-components/templates-library/templates-library.tsx @@ -9,7 +9,13 @@ import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useMutation, useInfiniteQuery } from "@tanstack/react-query"; -import { SquareCheck, SquareX, Trash2 } from "lucide-react"; +import { + LayoutPanelTop, + LoaderCircle, + SquareCheck, + SquareX, + Trash2, +} from "lucide-react"; import { useWeave } from "@inditextech/weave-react"; import { useCollaborationRoom } from "@/store/store"; import { SIDEBAR_ELEMENTS } from "@/lib/constants"; @@ -19,6 +25,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { cn } from "@/lib/utils"; @@ -31,13 +38,17 @@ import Konva from "konva"; import { setTemplateOnPosition } from "@/components/utils/templates"; import { SidebarHeader } from "../sidebar-header"; import { useGetSession } from "../hooks/use-get-session"; -// import { eventBus } from "@/components/utils/events-bus"; +import { useCreatePageFromTemplate } from "../hooks/use-create-page-from-template"; const TEMPLATES_LIMIT = 20; export const TemplatesLibrary = () => { const instance = useWeave((state) => state.instance); + const [kind, setKind] = React.useState<"template" | "imageTemplate" | "all">( + "template", + ); + const [selectedTemplates, setSelectedTemplates] = React.useState< TemplateEntity[] >([]); @@ -74,6 +85,10 @@ export const TemplatesLibrary = () => { }, }); + const { createPageFromTemplate } = useCreatePageFromTemplate({ + roomId: room ?? "", + }); + const handleDeleteTemplate = React.useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any (template: any) => { @@ -134,7 +149,7 @@ export const TemplatesLibrary = () => { }, [instance]); const query = useInfiniteQuery({ - queryKey: ["getTemplates", room], + queryKey: ["getTemplates", room, kind], queryFn: async ({ pageParam }) => { if (!room) { return []; @@ -142,6 +157,8 @@ export const TemplatesLibrary = () => { return await getTemplates( room ?? "", + kind === "all" ? undefined : kind, + undefined, pageParam as number, TEMPLATES_LIMIT, ); @@ -222,10 +239,6 @@ export const TemplatesLibrary = () => { return null; } - // if (sidebarActive !== SIDEBAR_ELEMENTS.templates) { - // return null; - // } - return (
{
)} - - {templates.length === 0 && ( -
- No templates - - Save a node or a selection of nodes -
- as a template. -
+ {!query.isLoading && + !query.isFetchingNextPage && + templates.length === 0 && ( +
+ No templates defined + + Save a node or a selection of nodes as a template. +
+
+ Use it to start new pages with predefined content and structure. +
+
+ )} + {query.isLoading && !query.isFetchingNextPage && ( +
+ +
+

+ LOADING TEMPLATES +

+

Please wait...

+
)} - {templates.length > 0 && ( + {query.isFetched && templates.length > 0 && (
{ {templateComponent}
- + + {template.kind === "template" && ( + <> + { + try { + const templateData = JSON.parse( + template.templateData, + ); + createPageFromTemplate( + template.templateId, + templateData, + ); + } catch { + toast.error("Error parsing template data"); + } + }} + > + + New page from template + + + + )} { @@ -374,9 +444,11 @@ export const TemplatesLibrary = () => {
)} - {showSelection && ( - - )} +
); }; diff --git a/code/components/room-components/templates-library/types.ts b/code/components/room-components/templates-library/types.ts index 3896b684..2462428a 100644 --- a/code/components/room-components/templates-library/types.ts +++ b/code/components/room-components/templates-library/types.ts @@ -9,6 +9,8 @@ export type TemplateEntity = { templateId: string; status: TemplateStatus; name: string; + kind: string; + imageSlots: number; linkedNodeType: string | null; templateImage: string; templateData: string; diff --git a/code/components/room-components/videos-library/videos-library.tsx b/code/components/room-components/videos-library/videos-library.tsx index 72dcb0fd..eef2b9ba 100644 --- a/code/components/room-components/videos-library/videos-library.tsx +++ b/code/components/room-components/videos-library/videos-library.tsx @@ -411,7 +411,7 @@ export const VideosLibrary = () => { )}
- + {typeof appVideo !== "undefined" && ( <>
) => { + )} {/*
diff --git a/code/components/room/room.left-sidebar.tsx b/code/components/room/room.left-sidebar.tsx index 9a2dcdc7..fa30882e 100644 --- a/code/components/room/room.left-sidebar.tsx +++ b/code/components/room/room.left-sidebar.tsx @@ -50,7 +50,7 @@ export const RoomLeftSidebar = ({ viewType === "fixed", ["top-[62px] left-[62px] w-[400px] h-[calc(100%-54px-16px-40px)] drop-shadow"]: viewType === "floating" && showLeftSidebarFloating, - ["fixed pointer-events-auto bg-white border-r border-r-[#c9c9c9]"]: + ["fixed pointer-events-auto bg-white border-r-[0.5px] border-r-[#c9c9c9]"]: !imageCroppingEnabled && ((viewType === "floating" && showLeftSidebarFloating) || viewType === "fixed"), diff --git a/code/components/room/room.right-layout.tsx b/code/components/room/room.right-layout.tsx index 1bf7cf78..2c62083b 100644 --- a/code/components/room/room.right-layout.tsx +++ b/code/components/room/room.right-layout.tsx @@ -14,7 +14,7 @@ import { NodeProperties } from "../room-components/overlay/node-properties"; import { SIDEBAR_ELEMENTS } from "@/lib/constants"; import { Comments } from "../room-components/comment/comments"; import { VideosLibrary } from "../room-components/videos-library/videos-library"; -// import { TemplatesLibrary } from "../room-components/templates-library/templates-library"; +import { TemplatesLibrary } from "../room-components/templates-library/templates-library"; import { ChatBot } from "../room-components/ai-components/chatbot"; import { useIAChat } from "@/store/ia-chat"; import { useWeave } from "@inditextech/weave-react"; @@ -44,7 +44,7 @@ export const RoomRightSidebar = () => { className={cn( "z-0 top-0 right-0 w-[400px] flex flex-col justify-between", { - ["fixed top-0 right-0 bottom-0 h-[calc(100%-65px-40px)] w-[400px]"]: + ["fixed top-0 right-0 bottom-0 h-[calc(100%-40px)] w-[400px] border-l-[0.5px] border-l-[#c9c9c9]"]: viewType === "fixed", ["absolute top-[62px] right-[8px] w-[400px] h-[calc(100%-54px-16px-40px)]"]: viewType === "floating", @@ -74,9 +74,10 @@ export const RoomRightSidebar = () => { {WEAVE_STORE_CONNECTION_STATUS.CONNECTED === weaveConnectionStatus && !isRoomSwitching && (
{ - {/* */} + diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.confirmation.tsx b/code/components/use-cases/templates/components/add-to-room/add-to-room.confirmation.tsx deleted file mode 100644 index 9afbccc4..00000000 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.confirmation.tsx +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/components/ui/button"; -import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; -import { toast } from "sonner"; -import React from "react"; -import Masonry from "react-responsive-masonry"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Spinner } from "@/components/ui/spinner"; -import { useTemplatesUseCase } from "../../store/store"; -import { useAddToRoom } from "../../store/add-to-room"; -import { cn } from "@/lib/utils"; -import { useMutation } from "@tanstack/react-query"; -import { - postAddTemplateToRoom, - PostAddTemplateToRoomPayload, -} from "@/api/post-add-template-to-room"; - -export function AddToRoomConfirmation() { - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const selectedImages = useTemplatesUseCase((state) => state.images.selected); - const setAddToRoomOpen = useTemplatesUseCase( - (state) => state.setAddToRoomOpen, - ); - const setSelectedImages = useTemplatesUseCase( - (state) => state.setSelectedImages, - ); - - const room = useAddToRoom((state) => state.room); - const page = useAddToRoom((state) => state.page); - const template = useAddToRoom((state) => state.template); - const frameName = useAddToRoom((state) => state.frameName); - const setStep = useAddToRoom((state) => state.setStep); - - const [processing, setProcessing] = React.useState(false); - - const mutationAddToRoom = useMutation({ - mutationFn: async (payload: PostAddTemplateToRoomPayload) => { - return await postAddTemplateToRoom(payload); - }, - onSettled() { - setProcessing(false); - }, - onSuccess() { - if (!room) { - return; - } - - if (room.create) { - toast.success( - `Room ${room.id} created and template added successfully`, - { - action: { - label: "Go to room", - onClick: () => { - const host = - window.location.protocol + "//" + window.location.host; - window.open( - `${host}/rooms/${room.id}`, - "_blank", - "noopener,noreferrer", - ); - }, - }, - }, - ); - } else { - toast.success(`Template added to room ${room.id} successfully`); - } - - setSelectedImages([]); - setAddToRoomOpen(false); - }, - onError() { - toast.error("Failed to create template, please try again"); - }, - }); - - const apiEndpoint = import.meta.env.VITE_API_ENDPOINT; - - if (!template || !room) { - return null; - } - - return ( - <> -
- - Check the selection and confirm to add the images to the room. - -
-
-
- - - {selectedImages.map((image) => { - return ( - {`image - ); - })} - - -
-
-
-
-
-
-
- Room -
-
- {room?.name || ""} -
-
-
-
- Page -
-
- {page?.name || ""} -
-
-
-
- Frame name -
-
- {frameName || ""} -
-
-
-
-
- Template to use -
-
-
- A template -
{template.name}
-
-
-
-
-
- - - - -
- - ); -} diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.create-room.tsx b/code/components/use-cases/templates/components/add-to-room/add-to-room.create-room.tsx deleted file mode 100644 index c4df2934..00000000 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.create-room.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/components/ui/button"; -import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import React from "react"; -import { useAddToRoom } from "../../store/add-to-room"; - -export function AddToRoomCreateRoom() { - const room = useAddToRoom((state) => state.room); - const setStep = useAddToRoom((state) => state.setStep); - const setRoom = useAddToRoom((state) => state.setRoom); - - const roomName = React.useMemo(() => { - return room ? room.id : ""; - }, [room]); - - return ( - <> -
- - Define the name of the room to create. - -
-
- - { - setRoom({ id: e.target.value, name: "", create: true }); - }} - onFocus={() => { - window.weaveOnFieldFocus = true; - }} - onBlurCapture={() => { - window.weaveOnFieldFocus = false; - }} - /> -
-
-
- - - -
- - ); -} diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-page.tsx b/code/components/use-cases/templates/components/add-to-room/add-to-room.select-page.tsx deleted file mode 100644 index 8e9394b2..00000000 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/components/ui/button"; -import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; -import React from "react"; -import { useInView } from "react-intersection-observer"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useTemplatesUseCase } from "../../store/store"; -import { cn } from "@/lib/utils"; -import { useAddToRoom } from "../../store/add-to-room"; -import { CheckIcon } from "lucide-react"; -import { getPages } from "@/api/pages/get-pages"; - -const PAGES_LIMIT = 100; - -export function AddToRoomSelectPage() { - const addToRoomOpen = useTemplatesUseCase((state) => state.addToRoom.open); - - const room = useAddToRoom((state) => state.room); - const page = useAddToRoom((state) => state.page); - const setPage = useAddToRoom((state) => state.setPage); - const setStep = useAddToRoom((state) => state.setStep); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [pages, setPages] = React.useState([]); - - const query = useInfiniteQuery({ - queryKey: ["getPagesInfiniteList", room?.id ?? ""], - queryFn: async ({ pageParam }) => { - if (!room?.id) return { items: [], total: 0 }; - - return (await getPages( - room?.id, - "active", - pageParam, - PAGES_LIMIT, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - )) as any; - }, - select: (newData) => newData, // keep shape stable - structuralSharing: true, - initialPageParam: undefined, - getNextPageParam: (lastPage, allPages) => { - const loadedSoFar = allPages.reduce( - (sum, page) => sum + page.items.length, - 0, - ); - if (loadedSoFar < lastPage.total) { - return loadedSoFar; // next offset - } - return undefined; // no more pages - }, - enabled: addToRoomOpen, - }); - - React.useEffect(() => { - if (!query.data) return; - setPages(query.data?.pages.flatMap((page) => page.items) ?? []); - }, [query.data]); - - const { ref, inView } = useInView({ threshold: 1 }); - - React.useEffect(() => { - if (inView && query.hasNextPage && !query.isFetchingNextPage) { - query.fetchNextPage(); - } - }, [inView, query]); - - return ( - <> -
- - Select the page where you want to add the template. - -
- - {pages.length > 0 && - pages.map((pageInfo, index) => { - return ( -
{ - setPage({ id: pageInfo.pageId, name: pageInfo.name }); - }} - > -
{pageInfo?.name}
- {page?.id === pageInfo?.pageId && ( -
- -
- )} -
- ); - })} -
- {query.isFetchingNextPage && ( -

- loading more... -

- )} - -
-
-
- - - - - - ); -} diff --git a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-room.tsx b/code/components/use-cases/templates/components/add-to-room/add-to-room.select-room.tsx deleted file mode 100644 index cbb46162..00000000 --- a/code/components/use-cases/templates/components/add-to-room/add-to-room.select-room.tsx +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/components/ui/button"; -import { DialogDescription, DialogFooter } from "@/components/ui/dialog"; -import React from "react"; -import { useInView } from "react-intersection-observer"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useTemplatesUseCase } from "../../store/store"; -import { getRooms } from "@/api/rooms/get-rooms"; -import { cn } from "@/lib/utils"; -import { useAddToRoom } from "../../store/add-to-room"; -import { CheckIcon } from "lucide-react"; - -const ROOMS_LIMIT = 100; - -export function AddToRoomSelectRoom() { - const addToRoomOpen = useTemplatesUseCase((state) => state.addToRoom.open); - - const room = useAddToRoom((state) => state.room); - const setRoom = useAddToRoom((state) => state.setRoom); - const setStep = useAddToRoom((state) => state.setStep); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [rooms, setRooms] = React.useState([]); - - const query = useInfiniteQuery({ - queryKey: ["getRooms"], - queryFn: async ({ pageParam }) => { - return await getRooms(pageParam, ROOMS_LIMIT); - }, - select: (newData) => newData, // keep shape stable - structuralSharing: true, - initialPageParam: undefined, - getNextPageParam: (lastPage) => { - return lastPage.continuationToken === "" - ? undefined - : lastPage.continuationToken; - }, - enabled: addToRoomOpen, - }); - - React.useEffect(() => { - if (!query.data) return; - setRooms(query.data?.pages.flatMap((page) => page.items) ?? []); - }, [query.data]); - - const { ref, inView } = useInView({ threshold: 1 }); - - React.useEffect(() => { - if (inView && query.hasNextPage && !query.isFetchingNextPage) { - query.fetchNextPage(); - } - }, [inView, query]); - - return ( - <> -
- - Select the room where you want to add the template. - -
- - {rooms.length > 0 && - rooms.map((roomInfo, index) => { - return ( -
{ - setRoom({ - id: roomInfo.roomId, - name: roomInfo.name, - create: false, - }); - }} - > -
{roomInfo?.name}
- {room?.id === roomInfo?.roomId && ( -
- -
- )} -
- ); - })} -
- {query.isFetchingNextPage && ( -

- loading more... -

- )} - -
-
-
- - - - - ); -} diff --git a/code/components/use-cases/templates/components/home/home.tsx b/code/components/use-cases/templates/components/home/home.tsx deleted file mode 100644 index d99b708c..00000000 --- a/code/components/use-cases/templates/components/home/home.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { Toaster } from "@/components/ui/sonner"; -import { ShowcaseLeftSidebar } from "@/components/showcase/showcase.left-sidebar"; -import { SessionLogin } from "@/components/session/session.login"; -import { ShowcaseDependencies } from "@/components/showcase/showcase.dependencies"; -import { useGetSession } from "@/components/room-components/hooks/use-get-session"; -import { Rooms } from "@/components/room-components/rooms/rooms"; -import { CreateRoomDialog } from "@/components/room-components/overlay/create-room"; -import { JoinRoomDialog } from "@/components/room-components/overlay/join-room"; -import { EditRoomDialog } from "@/components/room-components/overlay/edit-room"; -import { DeleteRoomDialog } from "@/components/room-components/overlay/delete-room"; -import { RoomAccessLinkDialog } from "@/components/room-components/overlay/room-acces-link"; -import { SignOverlay } from "@/components/sign-overlay/sign-overlay"; - -export const TemplatesHomePage = () => { - const { session } = useGetSession(); - - return ( - <> -
- -
- {!session && } - {session && } -
- -
- - - - - - - - - ); -}; diff --git a/code/components/use-cases/templates/components/images/images.tsx b/code/components/use-cases/templates/components/images/images.tsx deleted file mode 100644 index ce9b1a36..00000000 --- a/code/components/use-cases/templates/components/images/images.tsx +++ /dev/null @@ -1,309 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { toast } from "sonner"; -import { - useInfiniteQuery, - useMutation, - useQueryClient, -} from "@tanstack/react-query"; -import Masonry from "react-responsive-masonry"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Checkbox } from "@/components/ui/checkbox"; -import { useTemplatesUseCase } from "../../store/store"; -import { - ImagesIcon, - ImageUpIcon, - LayoutPanelTop, - ListChecks, - PlusIcon, - TrashIcon, -} from "lucide-react"; -import { getTemplatesImages } from "@/api/templates/get-templates-images"; -import { UploadImage } from "../upload-image"; -import { useAddToRoom } from "../../store/add-to-room"; -import { delTemplatesImage } from "@/api/templates/del-templates-image"; -import { Badge } from "@/components/ui/badge"; -import { ToolbarButton } from "@/components/room-components/toolbar/toolbar-button"; -import { Divider } from "@/components/room-components/overlay/divider"; - -export const Images = () => { - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const selectedImages = useTemplatesUseCase((state) => state.images.selected); - const templatesManage = useTemplatesUseCase( - (state) => state.templates.manage, - ); - const setShowSelectFileImage = useTemplatesUseCase( - (state) => state.setShowSelectFileImage, - ); - const setSelectedImages = useTemplatesUseCase( - (state) => state.setSelectedImages, - ); - const setAddToRoomOpen = useTemplatesUseCase( - (state) => state.setAddToRoomOpen, - ); - const setTemplatesManage = useTemplatesUseCase( - (state) => state.setTemplatesManage, - ); - - const setRoom = useAddToRoom((state) => state.setRoom); - const setTemplate = useAddToRoom((state) => state.setTemplate); - const setFrameName = useAddToRoom((state) => state.setFrameName); - const setStep = useAddToRoom((state) => state.setStep); - - const queryClient = useQueryClient(); - - const { data, isFetching, isFetched } = useInfiniteQuery({ - queryKey: ["getTemplatesImages", instanceId], - queryFn: async ({ pageParam }) => { - return await getTemplatesImages( - instanceId, - 20, - pageParam as unknown as string, - ); - }, - select: (newData) => newData, // keep shape stable - structuralSharing: true, - initialPageParam: 0, - getNextPageParam: (lastPage) => { - return lastPage.continuationToken; - }, - }); - - const mutationDelete = useMutation({ - mutationFn: async (imageId: string) => { - return await delTemplatesImage(instanceId ?? "", imageId); - }, - onMutate: () => { - const toastId = toast.loading("Requesting images deletion..."); - return { toastId }; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onSettled: (_, __, ___, context: any) => { - const queryKey = ["getTemplatesImages", instanceId]; - queryClient.invalidateQueries({ queryKey }); - - if (context?.toastId) { - toast.dismiss(context.toastId); - } - }, - onError() { - toast.error("Error requesting images deletion."); - }, - }); - - const handleDeleteImage = React.useCallback( - (image: string) => { - mutationDelete.mutate(image); - }, - [mutationDelete], - ); - - const apiEndpoint = import.meta.env.VITE_API_ENDPOINT; - - const images = React.useMemo(() => { - if (!data) { - return []; - } - return data.pages.flatMap((page) => page.images); - }, [data]); - - return ( - <> -
-
- {isFetching && ( -
-
-
loading
-
-
- )} - {isFetched && !isFetching && images.length > 0 && ( - <> - - - {images.map((image) => ( -
- {`image { - if (selectedImages.includes(image)) { - setSelectedImages( - selectedImages.filter((img) => img !== image), - ); - } else { - setSelectedImages([...selectedImages, image]); - } - }} - /> -
- { - if (selectedImages.includes(image)) { - setSelectedImages( - selectedImages.filter((img) => img !== image), - ); - } else { - setSelectedImages([...selectedImages, image]); - } - }} - /> -
-
- ))} -
-
-
-
- - actions - - - } - onClick={() => { - setTemplatesManage(!templatesManage); - }} - label={ -
-

Manage templates

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="bottom" - tooltipAlign="center" - /> - } - onClick={() => { - setShowSelectFileImage(true); - }} - label={ -
-

Upload image

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="bottom" - tooltipAlign="center" - /> - - } - onClick={() => { - if (selectedImages.length > 0) { - setSelectedImages([]); - } else { - const allImages = data?.pages.flatMap( - (page) => page.images, - ); - if (allImages) { - setSelectedImages(allImages); - } - } - }} - label={ -
-

Toggle all

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="top" - tooltipAlign="center" - /> -
-
- {selectedImages.length > 0 && ( - <> - - {`${selectedImages.length}`} IMAGE(S) SELECTED - - - - )} - {selectedImages.length > 0 && ( - <> - } - onClick={() => { - for (const image of selectedImages) { - handleDeleteImage(image); - } - - setSelectedImages([]); - }} - label={ -
-

Delete selected

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="top" - tooltipAlign="end" - /> - } - onClick={() => { - setRoom(undefined); - setTemplate(undefined); - setFrameName(""); - setStep("select-room"); - setAddToRoomOpen(true); - }} - label={ -
-

Add to room

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="top" - tooltipAlign="end" - /> - - )} -
-
- - )} - {isFetched && !isFetching && images.length === 0 && ( -
-
-
- -
- No images loaded -
-
- -
-
- )} -
-
- - - ); -}; diff --git a/code/components/use-cases/templates/components/page/menu.tsx b/code/components/use-cases/templates/components/page/menu.tsx deleted file mode 100644 index bdc09da4..00000000 --- a/code/components/use-cases/templates/components/page/menu.tsx +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { cn } from "@/lib/utils"; -import { useNavigate } from "@tanstack/react-router"; -import { useWeave } from "@inditextech/weave-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Logo } from "@/components/utils/logo"; -import { ChevronDown, ChevronUp } from "lucide-react"; -import { useTemplatesUseCase } from "../../store/store"; -import { useCollaborationRoom } from "@/store/store"; - -export function Menu() { - const navigate = useNavigate(); - - const instance = useWeave((state) => state.instance); - - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const setUser = useTemplatesUseCase((state) => state.setUser); - const setInstanceId = useTemplatesUseCase((state) => state.setInstanceId); - - const room = useCollaborationRoom((state) => state.room); - const roomInfo = useCollaborationRoom((state) => state.roomInfo.data); - const setRoomsRoomId = useCollaborationRoom((state) => state.setRoomsRoomId); - const setRoomsDeleteVisible = useCollaborationRoom( - (state) => state.setRoomsDeleteVisible, - ); - - const [menuOpen, setMenuOpen] = React.useState(false); - - const handleArchiveRoom = React.useCallback(async () => { - setMenuOpen(false); - setRoomsRoomId(room ?? ""); - setRoomsDeleteVisible(true); - }, [room, setRoomsRoomId, setRoomsDeleteVisible]); - - const handleExitRoom = React.useCallback(async () => { - sessionStorage.removeItem(`weave.js_standalone_templates_${instanceId}`); - await instance?.getStore().disconnect(); - setMenuOpen(false); - setUser(undefined); - setInstanceId("undefined"); - navigate({ to: "/use-cases/templates" }); - }, [instance, instanceId, navigate, setInstanceId, setUser]); - - return ( - <> - { - setMenuOpen(open); - }} - > - -
-
- -
- {menuOpen ? ( - - ) : ( - - )} -
-
- { - e.preventDefault(); - }} - align="start" - side="bottom" - alignOffset={-8} - sideOffset={8} - className="font-inter rounded-none !shadow-none !drop-shadow" - > - {roomInfo?.roomUser?.role === "owner" && - roomInfo?.room.status !== "archived" && ( - <> - - Archive - - - - )} - - Exit - - -
- - ); -} diff --git a/code/components/use-cases/templates/components/page/page-loader.tsx b/code/components/use-cases/templates/components/page/page-loader.tsx deleted file mode 100644 index fbf2e765..00000000 --- a/code/components/use-cases/templates/components/page/page-loader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import { AnimatePresence, motion } from "framer-motion"; -import { Logo } from "@/components/utils/logo"; -import React from "react"; -import ScrollVelocity from "@/components/ui/reactbits/TextAnimations/ScrollVelocity/ScrollVelocity"; - -type PageLoaderProps = { - instanceId?: string; - content: React.ReactNode; - description?: React.ReactNode; -}; - -const containerVariants = { - hidden: { - opacity: 1, - filter: "blur(10px)", - transition: { duration: 2, ease: [0.25, 0.1, 0.25, 1], staggerChildren: 0 }, - }, - visible: { - opacity: 1, - filter: "blur(0)", - transition: { - duration: 1, - ease: [0.25, 0.1, 0.25, 1], - staggerChildren: 0.3, - }, - }, -}; - -const childVariants = { - hidden: { - filter: "blur(10px)", - opacity: 0, - transition: { duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }, - }, - visible: { - filter: "blur(0)", - opacity: 1, - transition: { duration: 1, ease: [0.25, 0.1, 0.25, 1] }, - }, -}; - -export function PageLoader({ - instanceId, - content, - description, -}: Readonly) { - return ( - -
- -
-
-
- - - - -
-
- {content} -
- - {instanceId && ( -
- {instanceId} -
- )} - - {description && ( -
- - {description} - -
- )} -
-
-
-
-
- ); -} diff --git a/code/components/use-cases/templates/components/page/page.tsx b/code/components/use-cases/templates/components/page/page.tsx deleted file mode 100644 index 1aaa2a5d..00000000 --- a/code/components/use-cases/templates/components/page/page.tsx +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { Toaster } from "@/components/ui/sonner"; -import useHandleRouteParams from "../../hooks/use-handle-route-params"; -import { useTemplatesUseCase } from "../../store/store"; -import { Templates } from "../templates/templates"; -import { Images } from "../images/images"; -import { Ban, LoaderCircle, X } from "lucide-react"; -import { Menu } from "./menu"; -import { AddToRoomDialog } from "../add-to-room/add-to-room.dialog"; -import { Divider } from "@/components/room-components/overlay/divider"; -import { ToolbarButton } from "@/components/room-components/toolbar/toolbar-button"; -import { useGetSession } from "@/components/room-components/hooks/use-get-session"; -import { useLoadRoom } from "@/components/room-components/hooks/use-load-room"; -import { useCollaborationRoom } from "@/store/store"; -import { Logo } from "@/components/utils/logo"; -import { SessionLogin } from "@/components/session/session.login"; -import { Button } from "@/components/ui/button"; -import { useNavigate } from "@tanstack/react-router"; -import { Badge } from "@/components/ui/badge"; -import { RoomUser } from "@/components/room/room.user"; -import { SignOverlay } from "@/components/sign-overlay/sign-overlay"; - -export const TemplatesPage = () => { - const navigate = useNavigate(); - - useHandleRouteParams(); - - useLoadRoom(); - - const { session, isPending } = useGetSession(); - - const roomInfo = useCollaborationRoom((state) => state.roomInfo.data); - const roomInfoLoading = useCollaborationRoom( - (state) => state.roomInfo.loading, - ); - const roomInfoLoaded = useCollaborationRoom((state) => state.roomInfo.loaded); - const roomInfoError = useCollaborationRoom((state) => state.roomInfo.error); - - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const templatesManage = useTemplatesUseCase( - (state) => state.templates.manage, - ); - const setUser = useTemplatesUseCase((state) => state.setUser); - const setTemplatesManage = useTemplatesUseCase( - (state) => state.setTemplatesManage, - ); - const setAddToRoomOpen = useTemplatesUseCase( - (state) => state.setAddToRoomOpen, - ); - - React.useEffect(() => { - if (roomInfo) { - document.title = `${roomInfo.room.name} | Templates | Weave.js`; - } else { - document.title = `Room | Templates | Weave.js`; - } - }, [roomInfo]); - - React.useEffect(() => { - setTemplatesManage(false); - setAddToRoomOpen(false); - }, [setTemplatesManage, setAddToRoomOpen]); - - React.useEffect(() => { - if (instanceId !== "undefined" && !session) { - const userStorage = sessionStorage.getItem( - `weave.js_standalone_templates_${instanceId}`, - ); - try { - const userMapped = JSON.parse(userStorage ?? ""); - if (userMapped) { - setUser(userMapped); - } - // eslint-disable-next-line no-empty - } catch {} - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [instanceId, session]); - - if (!isPending && session && roomInfoLoading) { - return ( -
-
-
- -

- TEMPLATES -

-
-
-
- -
- LOADING ROOM -
-
-
-
-
- ); - } - if (!isPending && session && roomInfoLoaded && roomInfoError) { - return ( -
-
-
- -

- STANDALONE -

-
-
- {roomInfoError && roomInfoError.cause === 404 && ( - <> - - -
-

- The specified room does not exist. -
- It may have been deleted or the URL may be incorrect. -

-
- - )} - {roomInfoError && roomInfoError.cause === 403 && ( - <> - - -
-

- You don't have permissions to access this room, -
- ask the room owner or any participant for an invite. -

-
- - )} - - -
-
-
- ); - } - - if (!isPending && !session) { - return ( - <> -
-
-
- -

- STANDALONE -

-
-
-
-

YOU NEED TO SIGN IN

-
- -
-
-
- - - ); - } - - if (!isPending && !session) { - return ( - <> -
-
-
- -

- STANDALONE -

-
-
-
-

YOU NEED TO SIGN IN

-
- -
-
-
- - - ); - } - - return ( - <> -
-
-
- -
- - ROOM - -
- {roomInfo?.room?.name ?? ""} -
-
- {templatesManage && ( - <> - -
Templates
- - )} -
-
- {roomInfo?.room?.status === "archived" && ( - <> -
- {roomInfo?.room?.status === "archived" && ( - archived - )} -
- - )} - - {templatesManage && ( - <> - - } - onClick={() => { - setTemplatesManage(!templatesManage); - }} - label={ -
-

Close

-
- } - size="small" - variant="squared" - tooltipSideOffset={14} - tooltipSide="bottom" - tooltipAlign="end" - /> - - )} -
-
-
- {!templatesManage && ( -
- -
- )} - {templatesManage && ( -
- -
- )} -
-
- - - - ); -}; diff --git a/code/components/use-cases/templates/components/page/user-form.tsx b/code/components/use-cases/templates/components/page/user-form.tsx deleted file mode 100644 index 4a185bfa..00000000 --- a/code/components/use-cases/templates/components/page/user-form.tsx +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { v4 as uuidv4 } from "uuid"; -import { motion } from "motion/react"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useTemplatesUseCase } from "../../store/store"; - -const formSchema = z - .object({ - username: z - .string() - .trim() - .min(1, { message: "The username is required" }) - .max(50, { message: "The username must be maximum 50 characters long" }), - }) - .required(); - -function UserForm() { - const inputRef = React.useRef(null); - - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const setUser = useTemplatesUseCase((state) => state.setUser); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - username: "", - }, - }); - - React.useEffect(() => { - setTimeout(() => { - inputRef.current?.focus(); - }, 0); - }, []); - - function onSubmit(values: z.infer) { - const userMapped = { - id: `${values.username}-${uuidv4()}`, - name: values.username, - email: `${values.username}@weavejs.com`, - }; - setUser(userMapped); - sessionStorage.setItem( - `weave.js_standalone_templates_${instanceId}`, - JSON.stringify(userMapped), - ); - } - - return ( - -
- - ( - - - - - - - )} - /> -
- -
- - -
- ); -} - -export default UserForm; diff --git a/code/components/use-cases/templates/components/templates/template.tsx b/code/components/use-cases/templates/components/templates/template.tsx deleted file mode 100644 index 8827aa80..00000000 --- a/code/components/use-cases/templates/components/templates/template.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { TemplateEntity } from "./types"; -import { useAmountSlotsTemplate } from "../../hooks/use-amount-slots-template"; -import { Badge } from "@/components/ui/badge"; - -type TemplateProps = { - template: TemplateEntity; -}; - -export const Template = ({ template }: Readonly) => { - const amountOfImageTemplates = useAmountSlotsTemplate({ template }); - - return ( -
- A template -
-
{template.name}
-
-
- - {amountOfImageTemplates} slot(s) available - -
- {template.removalJobId !== null && - template.removalStatus !== null && - ["pending", "working"].includes(template.removalStatus) && ( -
- )} -
- ); -}; diff --git a/code/components/use-cases/templates/components/templates/templates.tsx b/code/components/use-cases/templates/components/templates/templates.tsx deleted file mode 100644 index 4d803b3d..00000000 --- a/code/components/use-cases/templates/components/templates/templates.tsx +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) -// -// SPDX-License-Identifier: Apache-2.0 - -import React from "react"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { useInView } from "react-intersection-observer"; -import { useTemplatesUseCase } from "../../store/store"; -import { getTemplates } from "@/api/get-templates"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { TemplateEntity } from "./types"; -import { Template } from "./template"; -import { LayoutTemplateIcon } from "lucide-react"; -import { useWeave } from "@inditextech/weave-react"; -import { Badge } from "@/components/ui/badge"; -import { Divider } from "@/components/room-components/overlay/divider"; -import { ToolbarButton } from "@/components/room-components/toolbar/toolbar-button"; - -const TEMPLATES_LIMIT = 20; - -export const Templates = () => { - const instance = useWeave((state) => state.instance); - - const instanceId = useTemplatesUseCase((state) => state.instanceId); - const templatesManage = useTemplatesUseCase( - (state) => state.templates.manage, - ); - - const [selectedTemplates, setSelectedTemplates] = React.useState< - TemplateEntity[] - >([]); - const [templates, setTemplates] = React.useState([]); - - const query = useInfiniteQuery({ - queryKey: ["getTemplates", instanceId], - queryFn: async ({ pageParam }) => { - if (!instanceId) { - return []; - } - - return await getTemplates( - instanceId ?? "", - pageParam as number, - TEMPLATES_LIMIT, - ); - }, - select: (newData) => newData, // keep shape stable - structuralSharing: true, - initialPageParam: 0, - getNextPageParam: (lastPage, allPages) => { - const loadedSoFar = allPages.reduce( - (sum, page) => sum + page.items.length, - 0, - ); - if (loadedSoFar < lastPage.total) { - return loadedSoFar; // next offset - } - return undefined; // no more pages - }, - enabled: templatesManage, - }); - - React.useEffect(() => { - if (!query.data) return; - setTemplates((prev: TemplateEntity[]) => - (query.data?.pages.flatMap((page) => page.items) ?? []).map( - (newItem: TemplateEntity) => - prev.find( - (oldItem) => - oldItem.templateId === newItem.templateId && - oldItem.updatedAt === newItem.updatedAt, - ) || newItem, - ), - ); - }, [query.data]); - - const { ref, inView } = useInView({ threshold: 1 }); - - React.useEffect(() => { - if (inView && query.hasNextPage && !query.isFetchingNextPage) { - query.fetchNextPage(); - } - }, [inView, query]); - - const handleCheckboxChange = React.useCallback( - (checked: boolean, template: TemplateEntity) => { - let newSelectedTemplates = [...selectedTemplates]; - if (checked) { - newSelectedTemplates.push(template); - } else { - newSelectedTemplates = newSelectedTemplates.filter( - (actTemplate) => actTemplate !== template, - ); - } - const unique = [...new Set(newSelectedTemplates)]; - setSelectedTemplates(unique); - }, - [selectedTemplates], - ); - - if (!templatesManage) { - return null; - } - - return ( -
- {templates.length === 0 && ( -
-
-
- -
- No templates defined -
-
- -
-
- )} - {templates.length > 0 && ( - <> - -
{ - if (!instance) { - return; - } - - if (e.target instanceof HTMLImageElement) { - instance.startDrag("add-template-to-room"); - instance.setDragProperties<{ templateData: string }>({ - templateData: e.target.dataset.templateData ?? "", - }); - } - }} - > - {templates.length > 0 && - templates.map((template) => { - const isChecked = selectedTemplates.includes(template); - - const templateComponent = ( -