import { Box, Dialog, DialogActionButton, DialogActions, DialogCancelButton, DialogContent, DialogHeader, } from "@prismicio/editor-ui"; import { GroupFieldType, type SlicePrimaryWidget, } from "@prismicio/types-internal/lib/customtypes"; import { FC, useState } from "react"; import { DropResult } from "react-beautiful-dnd"; import { flushSync } from "react-dom"; import { toast } from "react-toastify"; import { List } from "@/components/List"; import { addField, deleteField, deleteRepeatableZone, reorderField, updateField, } from "@/domain/slice"; import { AiFeedback } from "@/features/aiFeedback"; import { useSliceState } from "@/features/slices/sliceBuilder/SliceBuilderProvider"; import EditModal from "@/legacy/lib/builders/common/EditModal"; import Zone from "@/legacy/lib/builders/common/Zone"; import { Groups } from "@/legacy/lib/models/common/Group"; import { SlicePrimaryFieldSM, WidgetsArea, } from "@/legacy/lib/models/common/Slice"; import { Widgets } from "@/legacy/lib/models/common/widgets"; import { ensureDnDDestination } from "@/legacy/lib/utils"; import { transformKeyAccessor } from "@/legacy/lib/utils/str"; import { trackFieldAdded } from "@/utils/tracking/trackFieldAdded"; import { trackFieldUpdated } from "@/utils/tracking/trackFieldUpdated"; const dataTipText = ` The non-repeatable zone is for fields
that should appear once, like a
section title. `; const dataTipText2 = `The repeatable zone is for a group
of fields that you want to be able to repeat an
indeterminate number of times, like FAQs`; const itemsWidgetsArray = [ Widgets.Image, Widgets.Text, Widgets.StructuredText, Widgets.Link, Widgets.Select, Widgets.Boolean, Widgets.Number, Widgets.Color, Widgets.Date, Widgets.Table, Widgets.Embed, Widgets.Timestamp, Widgets.GeoPoint, Widgets.ContentRelationship, Widgets.LinkToMedia, ]; type OnSaveFieldProps = { apiId: string; newKey: string; value: SlicePrimaryFieldSM; inGroupFieldAction?: "add" | "update"; }; const FieldZones: FC = () => { const { slice, setSlice, variation } = useSliceState(); const [ isDeleteRepeatableZoneDialogOpen, setIsDeleteRepeatableZoneDialogOpen, ] = useState(false); const primaryWidgetsArray = [Widgets.Group, ...itemsWidgetsArray]; // We won't show the Repeatable Zone if no items are configured. const hasItems = Boolean( variation.items && Object.keys(variation.items).length > 0, ); const _onDeleteItem = (widgetArea: WidgetsArea) => (key: string) => { if ( widgetArea === WidgetsArea.Items && variation.items && Object.keys(variation.items).length <= 1 ) { setIsDeleteRepeatableZoneDialogOpen(true); return; } const newSlice = deleteField({ slice, variationId: variation.id, widgetArea, fieldId: key, }); setSlice(newSlice); }; const _onSave = ( widgetArea: WidgetsArea, { apiId: previousKey, newKey, value, inGroupFieldAction }: OnSaveFieldProps, ) => { const newSlice = updateField({ slice, variationId: variation.id, widgetArea, previousFieldId: previousKey, newFieldId: newKey, newField: value as SlicePrimaryWidget, }); setSlice(newSlice, () => { if (inGroupFieldAction === "add") { toast.success("Field added"); } }); // We don't want to track the group field update when it's for the management of a // field within a group (add or update). // It would result in double tracking, the one for the field touched within the group // and the group itself. if (!inGroupFieldAction) { trackFieldUpdated({ previousId: previousKey, id: newKey, field: value }); } }; const _onSaveNewField = ( widgetArea: WidgetsArea, { apiId: id, value: newField }: OnSaveFieldProps, ) => { const { type: widgetTypeName } = newField; const widget = primaryWidgetsArray.find( (sliceBuilderWidget) => sliceBuilderWidget.CUSTOM_NAME === widgetTypeName || sliceBuilderWidget.TYPE_NAME === widgetTypeName, ); if (!widget) { throw new Error(`Unsupported Field Type: ${widgetTypeName}`); } try { widget.schema.validateSync(newField, { stripUnknown: false }); } catch (error) { throw new Error(`Model is invalid for widget "${newField.type}".`); } const newSlice = addField({ slice, variationId: variation.id, widgetArea, newFieldId: id, newField: newField.type === GroupFieldType ? Groups.fromSM(newField) : newField, }); setSlice(newSlice, () => { toast.success(`${widgetTypeName === "Group" ? "Group" : "Field"} added`); }); trackFieldAdded({ id, field: newField }); }; const _onCreateOrSave = (widgetArea: WidgetsArea) => { return (props: OnSaveFieldProps) => { if (props.apiId === "") { return _onSaveNewField(widgetArea, { ...props, apiId: props.newKey }); // create new } return _onSave(widgetArea, props); // update existing }; }; const _onDragEnd = (widgetArea: WidgetsArea) => (result: DropResult) => { if (ensureDnDDestination(result)) return; const { source, destination } = result; if (!destination) { return; } const newSlice = reorderField({ slice, variationId: variation.id, widgetArea, sourceIndex: source.index, destinationIndex: destination.index, }); // When removing redux and replacing it by a simple useState, react-beautiful-dnd (that is deprecated library) was making the fields flickering on reorder. // The problem seems to come from the react non-synchronous way to handle our state update that didn't work well with the library. // It's a hack and since it's used on an old pure JavaScript code with a deprecated library it will be removed when updating the UI of the fields. flushSync(() => setSlice(newSlice)); }; const onDeleteRepeatableZone = () => { const newSlice = deleteRepeatableZone({ slice, variationId: variation.id }); setSlice(newSlice); setIsDeleteRepeatableZoneDialogOpen(false); }; return ( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access `slice.primary${transformKeyAccessor(item.key)}` } renderFieldAccessor={(key) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument `slice.primary${transformKeyAccessor(key)}` } testId="static-zone-content" isRepeatableCustomType={undefined} emptyStateHeading={undefined} /> {hasItems ? ( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access `item${transformKeyAccessor(item.key)}` } renderFieldAccessor={(key) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument `slice.items[i]${transformKeyAccessor(key)}` } testId="slice-repeatable-zone" isRepeatableCustomType={undefined} emptyStateHeading="No fields" /> ) : null} setIsDeleteRepeatableZoneDialogOpen(open)} > {slice.model.variations.length > 1 ? ( <> This action will permanently remove the repeatable zone from the {slice.model.name} slice {variation.name} variation.
Other variations will be left untouched. To reimplement repeatable fields later, use a group field instead of the repeatable zone.
) : ( <> This action will permanently remove the repeatable zone from the {slice.model.name}.
To reimplement repeatable fields later, use a group field instead of the repeatable zone.
)}
Delete
); }; export default FieldZones;