Room list: edit or remove custom sections (#33283)

* feat(sc): add section menu to section header

* feat(rls): add edit and remove sections

* feat(dialog): add editing mode to CreateSectionDialog

* feat(dialog): add remove section dialog

* feat(vm): wire up vm and stores

* test: update existing snapshots

* test(e2e): add playwright tests to edit and remove a section

* chore: fix remove section i18n key

* fix: able to send empty sections

* chore: update create section editing docs

* chore: remove useless fallback

* chore: add logs when section is unknown

* feat: use different wording when removing an empty section

* fix: only animate the chevron icon in the section header

* fix: change dialog subtitle weight to medium
This commit is contained in:
Florian Duros
2026-04-28 10:16:34 +00:00
committed by GitHub
parent 1dd5748d6f
commit c363d2eb82
22 changed files with 1090 additions and 160 deletions
@@ -14,6 +14,11 @@ import DialogButtons from "../elements/DialogButtons";
import { _t } from "../../../languageHandler";
interface CreateSectionDialogProps {
/**
* The name of the section being edited if defined. Otherwise, create a new section.
*/
sectionToEdit?: string;
/**
* Callback called when the dialog is closed.
* @param shouldCreateSection Whether a section should be created or not. This will be false if the user cancels the dialog.
@@ -25,36 +30,43 @@ interface CreateSectionDialogProps {
/**
* Dialog shown to the user to create a new section in the room list.
*/
export function CreateSectionDialog({ onFinished }: CreateSectionDialogProps): JSX.Element {
const [value, setValue] = useState("");
export function CreateSectionDialog({ onFinished, sectionToEdit }: CreateSectionDialogProps): JSX.Element {
const isEdition = Boolean(sectionToEdit);
const [value, setValue] = useState(sectionToEdit ?? "");
const isInvalid = Boolean(value.trim().length === 0);
return (
<BaseDialog
className="mx_CreateSectionDialog"
onFinished={() => onFinished(false, value)}
title={_t("create_section_dialog|title")}
title={isEdition ? _t("create_section_dialog|title_edition") : _t("create_section_dialog|title")}
hasCancel={true}
>
<Flex gap="var(--cpd-space-6x)" direction="column" className="mx_CreateSectionDialog_content">
<Text as="span" weight="semibold">
<Text as="span" weight="medium">
{_t("create_section_dialog|description")}
</Text>
<Form.Root
className="mx_CreateSectionDialog_form"
onSubmit={(e) => {
onFinished(true, value);
e.preventDefault();
if (!isInvalid) onFinished(true, value);
}}
>
<Form.Field name="sectionName">
<Form.Label> {_t("create_section_dialog|label")}</Form.Label>
<Form.TextControl onChange={(evt) => setValue(evt.target.value)} required={true} />
<Form.TextControl
value={value}
onChange={(evt) => setValue(evt.target.value)}
required={true}
/>
</Form.Field>
</Form.Root>
</Flex>
<DialogButtons
primaryButton={_t("create_section_dialog|create_section")}
primaryButton={
isEdition ? _t("create_section_dialog|edit_section") : _t("create_section_dialog|create_section")
}
primaryDisabled={isInvalid}
hasCancel={true}
onCancel={() => onFinished(false, "")}
@@ -0,0 +1,48 @@
/*
* 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 React from "react";
import { type JSX } from "react";
import { Text } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface RemoveSectionDialogProps {
onFinished: (shouldRemoveSection: boolean) => void;
/** Whether the section is empty */
isEmpty: boolean;
}
/**
* Dialog shown to the user to remove section in the room list.
*/
export function RemoveSectionDialog({ onFinished, isEmpty }: RemoveSectionDialogProps): JSX.Element {
return (
<BaseDialog
className="mx_RemoveSectionDialog"
onFinished={() => onFinished(false)}
title={_t("remove_section_dialog|title")}
hasCancel={true}
>
<Text as="span">{_t("remove_section_dialog|confirmation")}</Text>
{!isEmpty && (
<>
<br />
<Text as="span">{_t("remove_section_dialog|description")}</Text>
</>
)}
<DialogButtons
primaryButton={_t("remove_section_dialog|remove_section")}
hasCancel={true}
onCancel={() => onFinished(false)}
onPrimaryButtonClick={() => onFinished(true)}
/>
</BaseDialog>
);
}