Skip to content

Commit

Permalink
feat: Repeating sets (#4263)
Browse files Browse the repository at this point in the history
Adds repeating sets behind a feature flag.

Co-authored-by: Dave Samojlenko <[email protected]>
Co-authored-by: Anik Brazeau <[email protected]>
Co-authored-by: Pete <[email protected]>
  • Loading branch information
4 people authored Sep 27, 2024
1 parent bfd702f commit 0f9431a
Show file tree
Hide file tree
Showing 31 changed files with 823 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const PanelBody = ({
<Question item={item} onQuestionChange={onQuestionChange} />
</div>

<div className={cn(isDynamicRow && "pl-8 mb-2")}>
<div className={cn(isDynamicRow && "mb-2")}>
<SelectedElement
key={`item-${item.id}-${translationLanguagePriority}`}
item={item}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ export const SelectedElement = ({
case "combobox":
if (elIndex !== -1) {
element = (
<SubOptions elIndex={elIndex} item={item} renderIcon={(index) => `${index + 1}.`} />
<>
<ShortAnswer>{t("addElementDialog.combobox.title")}</ShortAnswer>
{!item.properties.managedChoices && (
<SubOptions elIndex={elIndex} item={item} renderIcon={(index) => `${index + 1}.`} />
)}
</>
);
} else {
element = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AddElementButton } from "../AddElementButton";

describe("<AddElementButton />", () => {
beforeEach(() => {
cy.intercept("/api/flags/experimentalBlocks/check", { status: true });
cy.intercept("/api/flags/repeatingSets/check", { status: true });
});

it("opens the add element dialog", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ describe("<ElementDialog />", () => {
cy.get('[data-testid="preset-filter').click();
cy.get('[data-testid="listbox"] li[role="option"]').should("have.length", 7);
cy.get('[data-testid="other-filter').click();

cy.get('[data-testid="listbox"] li[role="option"]').should("have.length", 1);

cy.get('[data-testid="all-filter').click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,21 @@ export const SubElement = ({
subMoveDown,
subDuplicateElement,
removeSubItem,
subElements,
propertyPath,
setChangeKey,
} = useTemplateStore((s) => ({
updateField: s.updateField,
subMoveUp: s.subMoveUp,
subMoveDown: s.subMoveDown,
subDuplicateElement: s.subDuplicateElement,
removeSubItem: s.removeSubItem,
subElements: s.form.elements[elIndex].properties.subElements,
getLocalizationAttribute: s.getLocalizationAttribute,
propertyPath: s.propertyPath,
setChangeKey: s.setChangeKey,
}));

const subElements = item.properties.subElements;

const { handleAddSubElement } = useHandleAdd();

const onQuestionChange = (itemId: number, val: string, lang: Language) => {
Expand All @@ -61,12 +63,17 @@ export const SubElement = ({
return elements.filter((element) => !notAllowed.includes(element.id));
};

const forceRefresh = () => {
setChangeKey(String(new Date().getTime())); //Force a re-render
};

if (!subElements || subElements.length < 1)
return (
<div className="ml-4 mt-10">
<AddToSetButton
handleAdd={(type?: FormElementTypes) => {
handleAddSubElement(elIndex, 0, type);
forceRefresh();
}}
filterElements={elementFilter}
/>
Expand All @@ -89,18 +96,23 @@ export const SubElement = ({
totalItems={subElements.length}
handleAdd={(type?: FormElementTypes) => {
handleAddSubElement(elIndex, subIndex, type);
forceRefresh();
}}
handleRemove={() => {
removeSubItem(elIndex, item.id);
forceRefresh();
}}
handleMoveUp={() => {
subMoveUp(elIndex, subIndex);
forceRefresh();
}}
handleMoveDown={() => {
subMoveDown(elIndex, subIndex);
forceRefresh();
}}
handleDuplicate={() => {
subDuplicateElement(elIndex, subIndex);
forceRefresh();
}}
moreButtonRenderer={(moreButton) => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export const TranslateCustomizeSet = ({
return null;
}

// row title
const rowTitle = "rowTitle";

const rowTitlePropEn = localizeField(rowTitle, primaryLanguage);
const rowTitleEnValue = dynamicRowProps[rowTitlePropEn];
const rowTitlePropFr = localizeField(rowTitle, secondaryLanguage);
const rowTitleFrValue = dynamicRowProps[rowTitlePropFr];

// Add button
const addButtonText = "addButtonText";

Expand All @@ -46,6 +54,56 @@ export const TranslateCustomizeSet = ({

return (
<>
{/* Start row title inputs */}
<fieldset>
<FieldsetLegend>
{t("dynamicRow.translate.repeatingSet")} {":"} {t("dynamicRow.translate.rowTitleText")}
</FieldsetLegend>
<div className="mb-10 flex gap-px divide-x-2 border border-gray-300" key={primaryLanguage}>
<div className="relative w-1/2 flex-1">
<label htmlFor={`row-title-text-english-${element.id}`} className="sr-only">
<>{primaryLanguage}</>
</label>
<LanguageLabel id={`row-title-text-english-desc-${element.id}`} lang={primaryLanguage}>
<>{primaryLanguage}</>
</LanguageLabel>
<textarea
className="size-full p-4 focus:outline-blue-focus"
id={`row-title-text-french-${element.id}`}
aria-describedby={`row-title-text-english-desc-${element.id}`}
value={rowTitleEnValue}
onChange={(e) => {
updateField(
propertyPath(element.id, `dynamicRow.${rowTitle}`, primaryLanguage),
e.target.value
);
}}
/>
</div>
<div className="relative w-1/2 flex-1">
<label htmlFor={`row-title-text-french-${element.id}`} className="sr-only">
<>{secondaryLanguage}</>
</label>
<LanguageLabel id={`row-title-text-french-desc-${element.id}`} lang={secondaryLanguage}>
<>{secondaryLanguage}</>
</LanguageLabel>
<textarea
className="size-full p-4 focus:outline-blue-focus"
id={`row-title-text-french-${element.id}`}
aria-describedby={`row-title-text-french-desc-${element.id}`}
value={rowTitleFrValue}
onChange={(e) => {
updateField(
propertyPath(element.id, `dynamicRow.${rowTitle}`, secondaryLanguage),
e.target.value
);
}}
/>
</div>
</div>
</fieldset>
{/* End row title inputs */}

{/* Start add button inputs */}
<fieldset>
<FieldsetLegend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { SaveButton } from "@formBuilder/components/shared/SaveButton";

import { FormElement } from "@lib/types";
import { SkipLinkReusable } from "@clientComponents/globals/SkipLinkReusable";
import { alphabet } from "@lib/utils/form-builder";
import { sortGroup } from "@lib/utils/form-builder/groupedFormHelpers";
import { Group } from "@lib/formContext";
import { TranslateCustomizeSet } from "./TranslateCustomizeSet";
Expand Down Expand Up @@ -114,20 +113,12 @@ const Element = ({
const { t } = useTranslation("form-builder");

if (element.type === "dynamicRow") {
let subElementIndex = -1;
subElements = element.properties.subElements?.map((subElement) => {
let questionNumber = t("pageText");
if (subElement.type !== "richText") {
subElementIndex++;
questionNumber = alphabet[subElementIndex];
}

return (
<Element
key={subElement.id}
element={subElement}
index={subElement.id}
questionNumber={questionNumber}
primaryLanguage={primaryLanguage}
secondaryLanguage={secondaryLanguage}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,12 @@ export const getTranslatedDynamicRowProperties = async () => {
const { t: fr } = await serverTranslation("form-builder", { lang: "fr" });

return {
addButtonTextEn: en("dynamicRow.addButtonText"),
removeButtonTextEn: en("dynamicRow.removeButtonText"),
addButtonTextFr: fr("dynamicRow.addButtonText"),
removeButtonTextFr: fr("dynamicRow.removeButtonText"),
rowTitleEn: en("dynamicRow.defaultRowTitle"),
rowTitleFr: fr("dynamicRow.defaultRowTitle"),
addButtonTextEn: en("dynamicRow.defaultAddButtonText"),
removeButtonTextEn: en("dynamicRow.defaultRemoveButtonText"),
addButtonTextFr: fr("dynamicRow.defaultAddButtonText"),
removeButtonTextFr: fr("dynamicRow.defaultRemoveButtonText"),
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import React, { useState } from "react";
import { useTranslation } from "@i18n/client";
import { Button } from "@clientComponents/globals";
import { Button, Alert } from "@clientComponents/globals";
import { useDialogRef, Dialog } from "./Dialog";
import { useTemplateStore } from "@lib/store/useTemplateStore";

Expand Down Expand Up @@ -30,6 +30,8 @@ export const DynamicRowDialog = ({
const dialog = useDialogRef();
const { t } = useTranslation("form-builder");

const [error, setError] = useState<boolean | null>(null);

const { updateField, elements } = useTemplateStore((s) => ({
elements: s.form.elements,
updateField: s.updateField,
Expand All @@ -38,7 +40,11 @@ export const DynamicRowDialog = ({
const item = elements.find((el) => el.id === itemId);
const rowProps = item?.properties?.dynamicRow;

const [rowTitleValueEn, setRowTitleValueEn] = useState(rowProps?.rowTitleEn || "");
const [rowTitleValueFr, setRowTitleValueFr] = useState(rowProps?.rowTitleFr || "");

const [addButtonValueEn, setAddButtonValueEn] = useState(rowProps?.addButtonTextEn || "");

const [addButtonValueFr, setAddButtonValueFr] = useState(rowProps?.addButtonTextFr || "");

const [removeButtonValueEn, setRemoveButtonValueEn] = useState(
Expand All @@ -48,6 +54,9 @@ export const DynamicRowDialog = ({
rowProps?.removeButtonTextFr || ""
);

const rowTitleTextA11yEn = t("dynamicRow.rowTitleTextA11yEn");
const rowTitleTextA11yFr = t("dynamicRow.rowTitleTextA11yFr");

const addButtonTextA11yEn = t("dynamicRow.addButtonTextA11yEn");
const addButtonTextA11yFr = t("dynamicRow.addButtonTextA11yFr");

Expand All @@ -69,13 +78,28 @@ export const DynamicRowDialog = ({
className="ml-5"
theme="primary"
onClick={() => {
setError(null);
if (
rowTitleValueEn === "" ||
rowTitleValueFr === "" ||
addButtonValueEn === "" ||
addButtonValueFr === "" ||
removeButtonValueEn === "" ||
removeButtonValueFr === ""
) {
setError(true);
return;
}

dialog.current?.close();

if (!item || !item.properties) return;

const properties = {
...item.properties,
dynamicRow: {
rowTitleEn: rowTitleValueEn,
rowTitleFr: rowTitleValueFr,
addButtonTextEn: addButtonValueEn,
addButtonTextFr: addButtonValueFr,
removeButtonTextEn: removeButtonValueEn,
Expand All @@ -100,7 +124,34 @@ export const DynamicRowDialog = ({
title={t("dynamicRow.dialog.title")}
>
<div className="p-5">
{/* Add button */}
{error && (
<Alert.Danger className="mb-2" focussable>
<Alert.Title headingTag="h3">{t("dynamicRow.dialog.error.title")}</Alert.Title>
<p>{t("dynamicRow.dialog.error.message")}</p>
</Alert.Danger>
)}

{/* Title input */}
<div className="mb-8">
<h4 className="mb-4 block font-bold">{t("dynamicRow.dialog.rowTitle.title")}</h4>
<p className="mb-4 text-sm">{t("dynamicRow.dialog.rowTitle.description")}</p>
<TextInput label={t("dynamicRow.dialog.english")}>
<input
aria-label={rowTitleTextA11yEn}
value={rowTitleValueEn}
onChange={(e) => setRowTitleValueEn(e.target.value)}
/>
</TextInput>
<TextInput label={t("dynamicRow.dialog.french")}>
<input
aria-label={rowTitleTextA11yFr}
value={rowTitleValueFr}
onChange={(e) => setRowTitleValueFr(e.target.value)}
/>
</TextInput>
</div>

{/* Add button input */}
<div className="mb-8">
<h4 className="mb-4 block font-bold">{t("dynamicRow.dialog.addButton.title")}</h4>
<p className="mb-4 text-sm">{t("dynamicRow.dialog.addButton.description")}</p>
Expand All @@ -120,7 +171,7 @@ export const DynamicRowDialog = ({
</TextInput>
</div>

{/* Remove button */}
{/* Remove button input */}
<div>
<label className="mb-4 block font-bold">
{t("dynamicRow.dialog.removeButton.title")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const Item = ({
arrow: ReactNode;
context: TreeItemRenderContext;
children: ReactNode | ReactElement;
handleDelete: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
handleDelete?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
}) => {
const { t } = useTranslation("form-builder");
const { refs } = useRefsContext();
Expand Down Expand Up @@ -104,7 +104,7 @@ export const Item = ({
context.interactiveElementProps.onDragStart(e);

// Customize dragging image for form elements
if (isFormElement) {
if (isFormElement && item.data.type !== "dynamicRow") {
// Get the box inside the element being dragged
const el = e.currentTarget.children[0];

Expand All @@ -127,7 +127,7 @@ export const Item = ({
>
{arrow}
{isRenaming ? (
<div className="relative flex h-[60px] w-[100%] items-center overflow-hidden text-sm">
<div className="relative flex h-[60px] w-full items-center overflow-hidden text-sm">
<EditableInput isSection={isSectionElement} title={titleText} context={context} />
</div>
) : (
Expand Down Expand Up @@ -173,7 +173,7 @@ export const Item = ({
lockClassName={cn(isFormElement && "absolute right-0", "mr-2 ")}
/>
)}
{titleText !== "" && title && title}
{titleText !== "" && title}
{titleText === "" &&
isFormElement &&
fieldType === "richText" &&
Expand All @@ -197,7 +197,7 @@ const Title = ({ title }: { title: string }) => {
title = t("logic.end");
}

return <div className="w-5/6 truncate">{title}</div>;
return <div className={cn("w-5/6 truncate")}>{title}</div>;
};

const Arrow = ({ item, context }: { item: TreeItem; context: TreeItemRenderContext }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ type ItemActionProps = {
context: TreeItemRenderContext;
arrow: ReactNode;
lockClassName: string;
handleDelete: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
handleDelete?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
};

export const ItemActions = ({ context, arrow, lockClassName, handleDelete }: ItemActionProps) => {
return context.canDrag ? (
<>
{context.isExpanded && (
{context.isExpanded && handleDelete && (
<button className="cursor-pointer" onClick={handleDelete}>
<DeleteIcon title="Delete group" className="absolute right-5 top-0 mr-10 scale-50" />
</button>
Expand Down
Loading

0 comments on commit 0f9431a

Please sign in to comment.