diff --git a/src/visualBuilder/components/FieldLocationAppList.tsx b/src/visualBuilder/components/FieldLocationAppList.tsx new file mode 100644 index 00000000..c9630717 --- /dev/null +++ b/src/visualBuilder/components/FieldLocationAppList.tsx @@ -0,0 +1,209 @@ +import React, { useState, useEffect, useMemo } from "preact/compat"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; +import { visualBuilderStyles } from "../visualBuilder.style"; +import classNames from "classnames"; +import { CslpData } from "../../utils/cslpdata"; + +interface App { + app_installation_uid: string; + app_uid: string; + contentTypeUid: string; + entryUid: string; + fieldDataType: string; + fieldDisplayName: string; + fieldPath: string; + icon?: string; + locale: string; + manifest: { + uid: string; + name: string; + description: string; + icon: string; + visibility: string; + }; + title: string; + uid: string; +} + +interface FieldLocationAppListProps { + apps: App[]; + position: "left" | "right"; + toolbarRef: React.RefObject; + domEditStack:CslpData[] + setDisplayAllApps: (displayAllApps: boolean) => void; + displayAllApps: boolean; +} + +const normalize = (text: string) => + text + .toLowerCase() + .replace(/[^a-z0-9 ]/gi, "") + .trim(); + +export const FieldLocationAppList = ({ + apps, + position, + toolbarRef, + domEditStack, + setDisplayAllApps, +}: FieldLocationAppListProps) => { + const remainingApps = apps.filter((app, index) => index !== 0); + const [search, setSearch] = useState(""); + + const filteredApps = useMemo(() => { + if (!search.trim()) return remainingApps; + + const normalizedSearch = normalize(search); + return remainingApps.filter((app) => { + return ( + normalize(app.title).includes(normalizedSearch) + ); + }); + }, [search, remainingApps]); + + const handleAppClick = (app: App) => { + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, + { + app: app, + position: toolbarRef.current?.getBoundingClientRect(), + DomEditStack:domEditStack + } + ); + setDisplayAllApps(false); + }; + + return ( +
+
+ + + + + setSearch((e.target as HTMLInputElement).value) + } + placeholder="Search for Apps" + className={ + visualBuilderStyles()[ + "visual-builder__field-location-app-list__search-input" + ] + } + /> +
+ +
+ {filteredApps.length === 0 && ( +
+ + No matching results found! + +
+ )} + {filteredApps.map((app) => ( +
handleAppClick(app)} + > +
+ {app.icon ? ( + {app.title} + ) : ( + + )} +
+ + {app.title} + +
+ ))} +
+
+ ); +}; diff --git a/src/visualBuilder/components/FieldLocationIcon.tsx b/src/visualBuilder/components/FieldLocationIcon.tsx new file mode 100644 index 00000000..a9396de9 --- /dev/null +++ b/src/visualBuilder/components/FieldLocationIcon.tsx @@ -0,0 +1,95 @@ +import classNames from "classnames"; +import { visualBuilderStyles } from "../visualBuilder.style"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { MoreIcon } from "./icons"; +import React, { useRef } from "preact/compat"; +import { LoadingIcon } from "./icons/loading"; +import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; +import { CslpData } from "../../utils/cslpdata"; + +export const FieldLocationIcon = ({ + fieldLocationData, + multipleFieldToolbarButtonClasses, + handleMoreIconClick, + moreButtonRef, + toolbarRef, + domEditStack +}: { + fieldLocationData: any; + multipleFieldToolbarButtonClasses: any; + handleMoreIconClick: () => void; + moreButtonRef: any; + toolbarRef: any; + domEditStack:CslpData[] +}) => { + + + + if (!fieldLocationData?.apps || fieldLocationData?.apps?.length === 0) { + return null; + } + + const handleAppClick = (app: any) => { + if(!toolbarRef.current) return + visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.FIELD_LOCATION_SELECTED_APP, { + app, + position: toolbarRef.current?.getBoundingClientRect(), + DomEditStack:domEditStack + }); + }; + + return ( +
+
+ + + + { + fieldLocationData.apps.length > 1 && ( + + ) + } +
+ ); +}; diff --git a/src/visualBuilder/components/FieldToolbar.tsx b/src/visualBuilder/components/FieldToolbar.tsx index c2ab9088..9009ae4e 100644 --- a/src/visualBuilder/components/FieldToolbar.tsx +++ b/src/visualBuilder/components/FieldToolbar.tsx @@ -1,4 +1,5 @@ import { CslpData } from "../../cslp/types/cslp.types"; +import { CslpData as CslpDataUtil } from "../../utils/cslpdata"; import getChildrenDirection from "../utils/getChildrenDirection"; import { ALLOWED_MODAL_EDITABLE_FIELD, @@ -19,12 +20,13 @@ import { MoveLeftIcon, MoveRightIcon, ReplaceAssetIcon, + MoreIcon, } from "./icons"; import { fieldIcons } from "./icons/fields"; import classNames from "classnames"; import { visualBuilderStyles } from "../visualBuilder.style"; import CommentIcon from "./CommentIcon"; -import React, { useEffect, useState } from "preact/compat"; +import React, { useEffect, useState, useRef } from "preact/compat"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; import { isFieldDisabled } from "../utils/isFieldDisabled"; import { IReferenceContentTypeSchema } from "../../cms/types/contentTypeSchema.types"; @@ -40,6 +42,10 @@ import { } from "./FieldRevert/FieldRevertComponent"; import { LoadingIcon } from "./icons/loading"; import { EntryPermissions } from "../utils/getEntryPermissions"; +import { EmptyAppIcon } from "./icons/EmptyAppIcon"; +import { FieldLocationAppList } from "./FieldLocationAppList"; +import { FieldLocationIcon } from "./FieldLocationIcon"; + export type FieldDetails = Pick< VisualBuilderCslpEventDetails, @@ -115,6 +121,13 @@ function FieldToolbarComponent( } = props; const { fieldMetadata, editableElement: targetElement } = eventDetails; const [isFormLoading, setIsFormLoading] = useState(false); + const [fieldLocationData, setFieldLocationData] = useState(null); + const [displayAllApps, setDisplayAllApps] = useState(false); + const moreButtonRef = useRef(null); + const toolbarRef = useRef(null); + const [appListPosition, setAppListPosition] = useState<"left" | "right">( + "right" + ); const parentPath = fieldMetadata?.multipleFieldMetadata?.parentDetails?.parentCslpValue || @@ -135,6 +148,7 @@ function FieldToolbarComponent( let Icon = null; let fieldType = null; let isWholeMultipleField = false; + const APP_LIST_MIN_WIDTH = 230; let disableFieldActions = false; if (fieldSchema) { @@ -149,7 +163,7 @@ function FieldToolbarComponent( disableFieldActions = isDisabled; fieldType = getFieldType(fieldSchema); - isModalEditable = ALLOWED_MODAL_EDITABLE_FIELD.includes(fieldType); + Icon = fieldIcons[fieldType]; @@ -171,20 +185,45 @@ function FieldToolbarComponent( fieldMetadata.instance.fieldPathWithIndex || fieldMetadata.multipleFieldMetadata?.index === -1); + isModalEditable = ALLOWED_MODAL_EDITABLE_FIELD.includes(fieldType) && !isWholeMultipleField; + isReplaceAllowed = ALLOWED_REPLACE_FIELDS.includes(fieldType) && !isWholeMultipleField; // if ( // DEFAULT_MULTIPLE_FIELDS.includes(fieldType) && // isWholeMultipleField && - // !isVariant + // !isVariant // ) { // return null; // } } + const domEditStack=getDOMEditStack(eventDetails.editableElement) as CslpDataUtil[] + + const invertTooltipPosition = targetElement.getBoundingClientRect().top <= TOOLTIP_TOP_EDGE_BUFFER; + const handleMoreIconClick = () => { + if (toolbarRef.current) { + const rect = toolbarRef.current.getBoundingClientRect(); + const spaceRight = window.innerWidth - rect.right; + const spaceLeft = rect.left; + let position = ""; + + if (spaceRight < APP_LIST_MIN_WIDTH) { + position = "left"; + } else if (spaceRight > APP_LIST_MIN_WIDTH) { + position = "right"; + } else { + position = spaceRight > spaceLeft ? "right" : "left"; + } + setAppListPosition(position as "left" | "right"); + } + + setDisplayAllApps(!displayAllApps); + }; + const editButton = Icon ? ( ); diff --git a/src/visualBuilder/components/icons/EmptyAppIcon.tsx b/src/visualBuilder/components/icons/EmptyAppIcon.tsx new file mode 100644 index 00000000..f0ba2d2b --- /dev/null +++ b/src/visualBuilder/components/icons/EmptyAppIcon.tsx @@ -0,0 +1,40 @@ +export const EmptyAppIcon = ({ + id = "", + ...props +}: React.SVGProps & { id?: string }) => { + return ( + + + + + ); +}; +export const sumOfAscii = (str: string): number => + [...str].reduce((a, b) => a + b.charCodeAt(0), 0); + +export const getColorFromString = (str: string = ""): string => { + const colorList: string[] = [ + "#99D8CE", + "#6BC3FE", + "#5060C1", + "#835EC3", + "#B16DBD", + "#FF85BC", + "#FF7E83", + "#A2D959", + "#59BA5E", + ]; + return colorList[sumOfAscii(str) % colorList.length]; +}; diff --git a/src/visualBuilder/components/icons/index.tsx b/src/visualBuilder/components/icons/index.tsx index d209df86..e8be7c7d 100644 --- a/src/visualBuilder/components/icons/index.tsx +++ b/src/visualBuilder/components/icons/index.tsx @@ -311,6 +311,22 @@ export function WarningOctagonIcon(): JSX.Element { ); } +export function MoreIcon(): JSX.Element { + return ( + + + + + + ); +} + export function ContentTypeIcon(): JSX.Element { return (
{ //@ts-expect-error - We are accessing private method here, but it is necessary to clean up the event listeners. diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 6b6d005a..117f505f 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -371,6 +371,22 @@ export class VisualBuilder { VisualBuilderPostMessageEvents.SEND_VARIANT_AND_LOCALE ); + visualBuilderPostMessage?.on<{ + scroll: boolean + }>( + VisualBuilderPostMessageEvents.TOGGLE_SCROLL, + (event) => { + if (!event.data.scroll) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'auto' + } + } + ); + + + + useHideFocusOverlayPostMessageEvent({ overlayWrapper: this.overlayWrapper, visualBuilderContainer: this.visualBuilderContainer, diff --git a/src/visualBuilder/listeners/mouseHover.ts b/src/visualBuilder/listeners/mouseHover.ts index e1cdc0e9..38adc4d8 100644 --- a/src/visualBuilder/listeners/mouseHover.ts +++ b/src/visualBuilder/listeners/mouseHover.ts @@ -200,6 +200,10 @@ function isFieldPathDropdown(target: HTMLElement): boolean { return target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper") || target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper__current-field"); } +function isFieldPathParent(target: HTMLElement): boolean { + return target.classList.contains("visual-builder__focused-toolbar__field-label-wrapper__parent-field"); +} + const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => { const eventDetails = getCsDataOfElement(params.event); const eventTarget = params.event.target as HTMLElement | null; @@ -221,7 +225,7 @@ const throttledMouseHover = throttle(async (params: HandleMouseHoverParams) => { } if( eventTarget && - isFieldPathDropdown(eventTarget) + (isFieldPathDropdown(eventTarget) || isFieldPathParent(eventTarget)) ) { params.customCursor && hideCustomCursor(params.customCursor); showOutline(); diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index e24bd4a5..ec960e3e 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -26,6 +26,8 @@ export enum VisualBuilderPostMessageEvents { COLLAB_RESOLVE_THREAD = "collab-resolve-thread", COLLAB_DELETE_THREAD = "collab-delete-thread", COLLAB_MISSING_THREADS = "collab-missing-threads", + FIELD_LOCATION_DATA = "field-location-data", + FIELD_LOCATION_SELECTED_APP = "field-location-selected-app", // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", @@ -50,4 +52,5 @@ export enum VisualBuilderPostMessageEvents { COLLAB_THREADS_REMOVE = "collab-threads-remove", COLLAB_THREAD_REOPEN = "collab-thread-reopen", COLLAB_THREAD_HIGHLIGHT = "collab-thread-highlight", -} \ No newline at end of file + TOGGLE_SCROLL = "toggle-scroll", +} diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index dc4ea46a..19603c87 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -121,6 +121,13 @@ export function visualBuilderStyles() { visibility: visible; } `, + "visual-builder__empty-block-plus-icon": css` + font-size: 22px; + font-weight: 300; + display: flex; + align-items: center; + justify-content: center; + `, "visual-builder__overlay--outline": css` position: absolute; outline: 4px solid #715cdd; @@ -571,19 +578,24 @@ export function visualBuilderStyles() { line-height: 100%; color: #647696; `, + "visual-builder__empty-block-field-name": css` + font-weight: 700; + `, "visual-builder__empty-block-add-button": css` height: 32px; border-radius: 4px; background: #f9f8ff; border-color: #6c5ce7; border-width: 1px; - padding: 8px 16px 8px 16px; + padding: 0 16px; font-size: 0.9rem; font-family: Inter; font-weight: 600; color: #6c5ce7; - padding-block: 0px; letter-spacing: 0.01rem; + display: inline-flex; + align-items: center; + justify-content: center; `, "visual-builder__hover-outline": css` position: absolute; @@ -727,6 +739,7 @@ export function visualBuilderStyles() { display: flex; flex-direction: column-reverse; z-index: 2147483647 !important; + position: relative; `, "visual-builder__variant-button": css` display: flex; @@ -739,6 +752,108 @@ export function visualBuilderStyles() { fill: #475161; } `, + "visual-builder__field-location-icons-container": css` + display: flex; + gap: 0.25rem; + align-items: center; + justify-content: center; + margin-left: 0.25rem; + + `, + "visual-builder__field-location-icons-container__divider": css` + height: 32px !important; + width: 1px; + border-radius: 2px; + background-color: #8a8f99; + `, + "visual-builder__field-location-icons-container__app-icon": css` + width: 24px; + height: 24px; + object-fit: cover; + `, + "visual-builder__field-location-app-list": css` + position: absolute; + top: 0; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + z-index: 1000; + min-width: 230px; + max-height: 250px; + min-height: 250px; + overflow-y: auto; + display: flex; + flex-direction: column; + `, + "visual-builder__field-location-app-list--left": css` + right: 100%; + margin-right: 8px; + `, + "visual-builder__field-location-app-list--right": css` + left: 100%; + margin-left: 8px; + `, + "visual-builder__field-location-app-list__search-container": css` + display: flex; + align-items: center; + padding: 10px 16px 0px 16px; + border: none; + border-bottom: 1px solid #f0f0f0; + `, + "visual-builder__field-location-app-list__search-input": css` + width: 100%; + padding: 10px 12px; + font-size: 14px; + outline: none; + box-sizing: border-box; + border: none; + `, + "visual-builder__field-location-app-list__search-icon": css` + width: 14px; + height: 14px; + `, + "visual-builder__field-location-app-list__content": css` + flex: 1; + overflow-y: auto; + `, + "visual-builder__field-location-app-list__no-results": css` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + text-align: center; + `, + "visual-builder__field-location-app-list__no-results-text": css` + color: #373b40; + font-weight: 400; + `, + "visual-builder__field-location-app-list__item": css` + display: flex; + align-items: center; + padding: 10px 16px; + cursor: pointer; + font-size: 14px; + `, + "visual-builder__field-location-app-list__item-icon-container": css` + width: 24px; + height: 24px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; + `, + "visual-builder__field-location-app-list__item-icon": css` + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + `, + "visual-builder__field-location-app-list__item-title": css` + color: #373b40; + font-weight: 400; + `, }; }