From 3bc3bc93852b7c09b1aa9f17c26ee7afbe96c226 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 15 Apr 2026 16:02:55 +0200 Subject: [PATCH] feat: add helper to creation section --- apps/web/src/stores/room-list-v3/section.ts | 59 +++++++++++++++ .../stores/room-list-v3/section-test.ts | 71 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 apps/web/src/stores/room-list-v3/section.ts create mode 100644 apps/web/test/unit-tests/stores/room-list-v3/section-test.ts diff --git a/apps/web/src/stores/room-list-v3/section.ts b/apps/web/src/stores/room-list-v3/section.ts new file mode 100644 index 0000000000..f11a028693 --- /dev/null +++ b/apps/web/src/stores/room-list-v3/section.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { v4 as uuidv4 } from "uuid"; + +import { SettingLevel } from "../../settings/SettingLevel"; +import SettingsStore from "../../settings/SettingsStore"; +import Modal from "../../Modal"; +import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectionDialog"; + +type Tag = string; + +/** + * Structure of the custom section stored in the settings. The tag is used as a unique identifier for the section, and the name is given by the user. + */ +type CustomSection = { + tag: Tag; + name: string; +}; + +/** + * The custom sections data is stored as a record in the settings, where the key is the section tag and the value is the section data (name and tag). + */ +export type CustomSectionsData = Record; +/** + * Ordered list of custom section tags. + */ +export type OrderedCustomSections = Tag[]; + +/** + * Creates a new custom section by showing a dialog to the user to enter the section name. + * If the user confirms, it generates a unique tag for the section, saves the section data in the settings, and updates the ordered list of sections. + * + * @return A promise that resolves to true if the section was created, or false if the user cancelled the creation or if there was an error. + */ +export async function createSection(): Promise { + const modal = Modal.createDialog(CreateSectionDialog); + + const [shouldCreateSection, sectionName] = await modal.finished; + if (!shouldCreateSection || !sectionName) return false; + + const tag = `element.io.section.${uuidv4()}`; + const newSection: CustomSection = { tag, name: sectionName }; + + // Save the new section data + const sectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {}; + sectionData[tag] = newSection; + await SettingsStore.setValue("RoomList.CustomSectionData", null, SettingLevel.ACCOUNT, sectionData); + + // Add the new section to the ordered list of sections + const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections") || []; + orderedSections.push(tag); + await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, orderedSections); + return true; +} diff --git a/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts new file mode 100644 index 0000000000..9fb8f40d33 --- /dev/null +++ b/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import Modal from "../../../../src/Modal"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { createSection } from "../../../../src/stores/room-list-v3/section"; +import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog"; + +describe("createSection", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(null); + jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([ + [false, "", false], + [true, "", false], + [true, "My Section", true], + ])("returns %s when shouldCreate=%s and name='%s'", async (shouldCreate, name, expected) => { + jest.spyOn(Modal, "createDialog").mockReturnValue({ + finished: Promise.resolve([shouldCreate, name]), + close: jest.fn(), + } as any); + + const result = await createSection(); + expect(result).toBe(expected); + }); + + it("opens the CreateSectionDialog", async () => { + const createDialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({ + finished: Promise.resolve([false, ""]), + close: jest.fn(), + } as any); + + await createSection(); + expect(createDialogSpy).toHaveBeenCalledWith(CreateSectionDialog); + }); + + it("saves section data and ordered sections at ACCOUNT level when confirmed", async () => { + const existingTag = "element.io.section.existing"; + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.OrderedCustomSections") return [existingTag]; + return null; + }); + jest.spyOn(Modal, "createDialog").mockReturnValue({ + finished: Promise.resolve([true, "My Section"]), + close: jest.fn(), + } as any); + const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); + + await createSection(); + + const customDataCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.CustomSectionData"); + const savedSection = Object.values(customDataCall![3] as Record)[0]; + expect(savedSection.name).toBe("My Section"); + expect(savedSection.tag).toMatch(/^element\.io\.section\./); + + const orderedCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.OrderedCustomSections"); + const savedOrder = orderedCall![3] as string[]; + expect(savedOrder[0]).toBe(existingTag); + expect(savedOrder[1]).toMatch(/^element\.io\.section\./); + }); +});