Skip to content

Commit

Permalink
chore: update handling for next element id (#4695)
Browse files Browse the repository at this point in the history
* updates code to generate element ids
  • Loading branch information
timarney authored Nov 27, 2024
1 parent afbf9c7 commit 4f481a8
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 19 deletions.
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

0 comments on commit 4f481a8

Please sign in to comment.