Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update handling for next element id #4695

Merged
merged 11 commits into from
Nov 27, 2024
Merged
317 changes: 317 additions & 0 deletions lib/store/__tests__/generateElementId.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
/**
* @jest-environment jsdom
*/
import React from "react";
import { useTemplateStore, TemplateStoreProvider } from "../useTemplateStore";
import { renderHook, act } from "@testing-library/react";
import { FormElementTypes } from "@lib/types";

const createStore = () => {
const wrapper = ({ children }: React.PropsWithChildren) => (
<TemplateStoreProvider isPublished={false}>{children}</TemplateStoreProvider>
);

const { result } = renderHook(() => useTemplateStore((s) => s), { wrapper });

act(() => {
result.current.initialize();
});

return result;
};

const defaultElements = [
{
id: 1,
type: FormElementTypes.textField,
properties: {
titleEn: "question 1 fr",
titleFr: "question 1 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
},
{
id: 2,
type: FormElementTypes.textField,
properties: {
titleEn: "question 2 en",
titleFr: "question 2 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
},
{
id: 3,
type: FormElementTypes.textField,
properties: {
titleEn: "question 3 en",
titleFr: "question 3 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
},
];

describe("generateElementId", () => {
it("existing ids in order", async () => {
const result = createStore();

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
elements: defaultElements, layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");
expect(result.current.form.elements[0].id).toBe(1);
expect(result.current.form.elements[1].id).toBe(2);
expect(result.current.form.elements[2].id).toBe(3);
expect(result.current.form.lastGeneratedElementId).toBe(undefined);

act(() => {
result.current.add(1);
});

expect(result.current.form.lastGeneratedElementId).toBe(4);

act(() => {
result.current.add(3);
});

expect(result.current.form.lastGeneratedElementId).toBe(5);
});

it("handles ids out of sequence", async () => {
const result = createStore();
const element = {
id: 19, // <-- This is out of sequence
type: FormElementTypes.textField,
properties: {
titleEn: "question 19 en",
titleFr: "question 19 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
};

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
// Add element with ID of 19 at the start
elements: [element, ...defaultElements], layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

act(() => {
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(20);

act(() => {
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(21);
});

it("handles deleting an element", async () => {
const result = createStore();

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
// Add element with ID of 19 at the start
elements: defaultElements, layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

act(() => {
// This will add an element with ID of 4
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(4);

act(() => {
// Remove an element
result.current.remove(1);
// Adding annother item should increment the lastGeneratedElementId by 1
// and not reuse the ID of the deleted element
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(5);
});

it("handles 3 digit ids", async () => {
const result = createStore();

const element = {
id: 201,
type: FormElementTypes.textField,
properties: {
titleEn: "question 201 en",
titleFr: "question 201 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
};

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
elements: [element], layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

act(() => {
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(202);

});

it("handles 4 digit ids", async () => {
const result = createStore();

const element = {
id: 2022,
type: FormElementTypes.textField,
properties: {
titleEn: "question 2022 en",
titleFr: "question 2022 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
};

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
elements: [element], layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

act(() => {
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(2023);

});

it("handles starting a form from scratch", async () => {
const result = createStore();

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
elements: [], layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

act(() => {
// This will add an element with ID of 4
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(1);


act(() => {
result.current.add(0);
result.current.add(0);
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(4);

act(() => {
result.current.remove(2);
result.current.remove(3);
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(5);

// Move items
act(() => {
result.current.moveDown(4);
result.current.add(0);
});

expect(result.current.form.lastGeneratedElementId).toBe(6);

});

it("gets highest element id", async () => {
const result = createStore();

const element = {
id: 201,
type: FormElementTypes.textField,
properties: {
titleEn: "question 201 en",
titleFr: "question 201 fr",
choices: [],
validation: { required: false },
descriptionEn: "description en",
descriptionFr: "descrption fr",
},
};

result.current.form = {
titleEn: "Title en",
titleFr: "Title fr",
elements: [
defaultElements[0],
defaultElements[1],
element, // <- Out of sequence and high id for testing purposes
defaultElements[2],
defaultElements[10] // <-- This is purposely undefined - to test check for element.id
],
layout: []
};

// Ensure we have a default form to work with
expect(result.current.form.titleEn).toBe("Title en");
expect(result.current.form.titleFr).toBe("Title fr");

// Check that the highest element id is 201
expect(result.current.getHighestElementId()).toBe(201);

});

});
1 change: 1 addition & 0 deletions lib/store/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export const defaultForm = {
},
layout: [],
elements: [],
lastGeneratedElementId: 0,
groups: {},
};
2 changes: 2 additions & 0 deletions lib/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export interface TemplateStoreState extends TemplateStoreProps {
setChangeKey: (key: string) => void;
getGroupsEnabled: () => boolean;
setGroupsLayout: (layout: string[]) => void;
getHighestElementId: () => number;
generateElementId: () => number;
}

export interface InitialTemplateStoreProps extends TemplateStoreProps {
Expand Down
32 changes: 29 additions & 3 deletions lib/store/useTemplateStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
moveElementUp,
removeElementById,
removeById,
incrementElementId,
getSchemaFromState,
incrementSubElementId,
cleanInput,
Expand Down Expand Up @@ -212,11 +211,12 @@ const createTemplateStore = (initProps?: Partial<InitialTemplateStoreProps>) =>
});
},
add: async (elIndex = 0, type = FormElementTypes.radio, data, groupId) => {
const id = get().generateElementId();

return new Promise((resolve) => {
set((state) => {
const allowGroups = state.allowGroupsFlag;

const id = incrementElementId(state.form.elements);
const item = {
...defaultField,
...data,
Expand Down Expand Up @@ -365,8 +365,8 @@ const createTemplateStore = (initProps?: Partial<InitialTemplateStoreProps>) =>
},
duplicateElement: (itemId, groupId = "", copyEn = "", copyFr = "") => {
const elIndex = get().form.elements.findIndex((el) => el.id === itemId);
const id = get().generateElementId();
set((state) => {
const id = incrementElementId(state.form.elements);
// deep copy the element
const element = JSON.parse(JSON.stringify(state.form.elements[elIndex]));
element.id = id;
Expand Down Expand Up @@ -523,6 +523,32 @@ const createTemplateStore = (initProps?: Partial<InitialTemplateStoreProps>) =>
state.form.groupsLayout = layout;
});
},
getHighestElementId: () => {
const validIds = get()
.form.elements.filter(
(element) => element && typeof element.id === "number" && !isNaN(element.id)
)
.map((element) => Number(element.id));

return validIds.length > 0 ? Math.max(...validIds) : 0;
},
generateElementId: () => {
set((state) => {
const lastId = state.form.lastGeneratedElementId || 0;

// Ensure backwards compatibility with existing forms
const highestId = state.getHighestElementId();

if (lastId < highestId) {
state.form.lastGeneratedElementId = highestId + 1;
return;
}

state.form.lastGeneratedElementId = lastId + 1;
});

return get().form.lastGeneratedElementId || 1;
},
}),
{
name: "form-storage",
Expand Down
Loading
Loading