Room list: add robustness to custom section loading (#33475)
* refactor: move all access to custom sections settings into `section.ts` * fix: add robustness when getting the order list of custom sections * fix: add robustness when getting the custom section data * fix: ignore malformed section but don't erase them * fix: remove useless await operator * test: add more tests
This commit is contained in:
@@ -40,7 +40,7 @@ import { DefaultTagID } from "./skip-list/tag";
|
|||||||
import { ExcludeTagsFilter } from "./skip-list/filters/ExcludeTagsFilter";
|
import { ExcludeTagsFilter } from "./skip-list/filters/ExcludeTagsFilter";
|
||||||
import { TagFilter } from "./skip-list/filters/TagFilter";
|
import { TagFilter } from "./skip-list/filters/TagFilter";
|
||||||
import { filterBoolean } from "../../utils/arrays";
|
import { filterBoolean } from "../../utils/arrays";
|
||||||
import { CHATS_TAG, createSection, deleteSection, editSection } from "./section";
|
import { CHATS_TAG, createSection, deleteSection, editSection, getOrderedCustomSections } from "./section";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are the filters passed to the room skip list.
|
* These are the filters passed to the room skip list.
|
||||||
@@ -522,7 +522,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
* Load the custom sections from the settings store and update the sorted tags.
|
* Load the custom sections from the settings store and update the sorted tags.
|
||||||
*/
|
*/
|
||||||
private loadCustomSections(): void {
|
private loadCustomSections(): void {
|
||||||
const orderedCustomSections = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
const orderedCustomSections = getOrderedCustomSections();
|
||||||
this.sortedTags = [DefaultTagID.Favourite, ...orderedCustomSections, CHATS_TAG, DefaultTagID.LowPriority];
|
this.sortedTags = [DefaultTagID.Favourite, ...orderedCustomSections, CHATS_TAG, DefaultTagID.LowPriority];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectio
|
|||||||
import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog";
|
import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog";
|
||||||
import { DefaultTagID, type TagID } from "./skip-list/tag";
|
import { DefaultTagID, type TagID } from "./skip-list/tag";
|
||||||
|
|
||||||
type Tag = string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A synthetic tag used to represent the "Chats" section, which contains
|
* A synthetic tag used to represent the "Chats" section, which contains
|
||||||
* every room that does not belong to any other explicit tag section.
|
* every room that does not belong to any other explicit tag section.
|
||||||
@@ -27,12 +25,14 @@ export const CHATS_TAG = "chats";
|
|||||||
*/
|
*/
|
||||||
export const CUSTOM_SECTION_TAG_PREFIX = "element.io.section.";
|
export const CUSTOM_SECTION_TAG_PREFIX = "element.io.section.";
|
||||||
|
|
||||||
|
type CustomTag = `${typeof CUSTOM_SECTION_TAG_PREFIX}${string}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given tag is a custom section tag.
|
* Checks if a given tag is a custom section tag.
|
||||||
* @param tag - The tag to check.
|
* @param tag - The tag to check.
|
||||||
* @returns True if the tag is a custom section tag, false otherwise.
|
* @returns True if the tag is a custom section tag, false otherwise.
|
||||||
*/
|
*/
|
||||||
export function isCustomSectionTag(tag: string): boolean {
|
export function isCustomSectionTag(tag: string): tag is CustomTag {
|
||||||
return tag.startsWith(CUSTOM_SECTION_TAG_PREFIX);
|
return tag.startsWith(CUSTOM_SECTION_TAG_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,18 +58,56 @@ export function isSectionTag(tagId: TagID): boolean {
|
|||||||
* 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.
|
* 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 = {
|
type CustomSection = {
|
||||||
tag: Tag;
|
tag: CustomTag;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if a value is a valid CustomSection object.
|
||||||
|
*/
|
||||||
|
function isValidCustomSection(value: unknown): value is CustomSection {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
isCustomSectionTag((value as Record<string, unknown>).tag as string) &&
|
||||||
|
typeof (value as Record<string, unknown>).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).
|
* 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<Tag, CustomSection>;
|
export type CustomSectionsData = Record<CustomTag, CustomSection>;
|
||||||
/**
|
/**
|
||||||
* Ordered list of custom section tags.
|
* Ordered list of custom section tags.
|
||||||
*/
|
*/
|
||||||
export type OrderedCustomSections = Tag[];
|
export type OrderedCustomSections = CustomTag[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the custom sections data from the settings.
|
||||||
|
* Invalid or malformed entries are dropped and the cleaned data is persisted back to settings.
|
||||||
|
*/
|
||||||
|
export function getCustomSectionData(): CustomSectionsData {
|
||||||
|
const raw = SettingsStore.getValue("RoomList.CustomSectionData");
|
||||||
|
// Data are malformed
|
||||||
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
||||||
|
|
||||||
|
// Filter out invalid entries
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(raw).filter(([key, value]) => isValidCustomSection(value) && value.tag === key),
|
||||||
|
) as CustomSectionsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the ordered list of custom section tags from the settings.
|
||||||
|
* If the settings contain tags that are not present in the custom section data, they will be filtered out and the settings will be updated to remove the unknown tags.
|
||||||
|
*/
|
||||||
|
export function getOrderedCustomSections(): OrderedCustomSections {
|
||||||
|
const sectionData = getCustomSectionData();
|
||||||
|
const rawValue = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
||||||
|
const orderedSections: OrderedCustomSections = Array.isArray(rawValue) ? rawValue : [];
|
||||||
|
return orderedSections.filter((tag) => tag in sectionData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new custom section by showing a dialog to the user to enter the section name.
|
* Creates a new custom section by showing a dialog to the user to enter the section name.
|
||||||
@@ -83,16 +121,16 @@ export async function createSection(): Promise<string | undefined> {
|
|||||||
const [shouldCreateSection, sectionName] = await modal.finished;
|
const [shouldCreateSection, sectionName] = await modal.finished;
|
||||||
if (!shouldCreateSection || !sectionName) return undefined;
|
if (!shouldCreateSection || !sectionName) return undefined;
|
||||||
|
|
||||||
const tag = `${CUSTOM_SECTION_TAG_PREFIX}${window.crypto.randomUUID()}`;
|
const tag: CustomTag = `${CUSTOM_SECTION_TAG_PREFIX}${window.crypto.randomUUID()}`;
|
||||||
const newSection: CustomSection = { tag, name: sectionName };
|
const newSection: CustomSection = { tag, name: sectionName };
|
||||||
|
|
||||||
// Save the new section data
|
// Save the new section data
|
||||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
const sectionData = getCustomSectionData();
|
||||||
sectionData[tag] = newSection;
|
sectionData[tag] = newSection;
|
||||||
await SettingsStore.setValue("RoomList.CustomSectionData", null, SettingLevel.ACCOUNT, sectionData);
|
await SettingsStore.setValue("RoomList.CustomSectionData", null, SettingLevel.ACCOUNT, sectionData);
|
||||||
|
|
||||||
// Add the new section to the ordered list of sections
|
// Add the new section to the ordered list of sections
|
||||||
const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections") || [];
|
const orderedSections = getOrderedCustomSections();
|
||||||
orderedSections.push(tag);
|
orderedSections.push(tag);
|
||||||
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, orderedSections);
|
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, orderedSections);
|
||||||
return tag;
|
return tag;
|
||||||
@@ -103,7 +141,11 @@ export async function createSection(): Promise<string | undefined> {
|
|||||||
* @param tag - The tag of the section to edit.
|
* @param tag - The tag of the section to edit.
|
||||||
*/
|
*/
|
||||||
export async function editSection(tag: string): Promise<void> {
|
export async function editSection(tag: string): Promise<void> {
|
||||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
if (!isCustomSectionTag(tag)) {
|
||||||
|
logger.info("Unknown section tag, cannot edit section", tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sectionData = getCustomSectionData();
|
||||||
const section = sectionData[tag];
|
const section = sectionData[tag];
|
||||||
if (!section) {
|
if (!section) {
|
||||||
logger.info("Unknown section tag, cannot edit section", tag);
|
logger.info("Unknown section tag, cannot edit section", tag);
|
||||||
@@ -127,7 +169,11 @@ export async function editSection(tag: string): Promise<void> {
|
|||||||
* @param isEmpty - Whether the section is empty (has no rooms). If the section is not empty, the confirmation dialog will show a warning message.
|
* @param isEmpty - Whether the section is empty (has no rooms). If the section is not empty, the confirmation dialog will show a warning message.
|
||||||
*/
|
*/
|
||||||
export async function deleteSection(tag: string, isEmpty: boolean): Promise<void> {
|
export async function deleteSection(tag: string, isEmpty: boolean): Promise<void> {
|
||||||
const sectionData = SettingsStore.getValue("RoomList.CustomSectionData");
|
if (!isCustomSectionTag(tag)) {
|
||||||
|
logger.info("Unknown section tag, cannot delete section", tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sectionData = getCustomSectionData();
|
||||||
if (!sectionData[tag]) {
|
if (!sectionData[tag]) {
|
||||||
logger.info("Unknown section tag, cannot delete section", tag);
|
logger.info("Unknown section tag, cannot delete section", tag);
|
||||||
return;
|
return;
|
||||||
@@ -138,8 +184,7 @@ export async function deleteSection(tag: string, isEmpty: boolean): Promise<void
|
|||||||
if (!shouldRemoveSection) return;
|
if (!shouldRemoveSection) return;
|
||||||
|
|
||||||
// Remove the section from the ordered list of sections
|
// Remove the section from the ordered list of sections
|
||||||
const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections");
|
const newOrderedSections = getOrderedCustomSections().filter((sectionTag) => sectionTag !== tag);
|
||||||
const newOrderedSections = orderedSections.filter((sectionTag) => sectionTag !== tag);
|
|
||||||
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, newOrderedSections);
|
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, newOrderedSections);
|
||||||
|
|
||||||
// Remove the section data
|
// Remove the section data
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"
|
|||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { type Call, CallEvent } from "../../models/Call";
|
import { type Call, CallEvent } from "../../models/Call";
|
||||||
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
||||||
|
import { getCustomSectionData, isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
|
||||||
|
|
||||||
interface RoomItemProps {
|
interface RoomItemProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -424,7 +424,7 @@ export class RoomListItemViewModel
|
|||||||
* Order follows the canonical section order from RoomListStoreV3.
|
* Order follows the canonical section order from RoomListStoreV3.
|
||||||
*/
|
*/
|
||||||
private static buildSections(roomTags: Room["tags"]): Section[] {
|
private static buildSections(roomTags: Room["tags"]): Section[] {
|
||||||
const customSectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
const customSectionData = getCustomSectionData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
RoomListStoreV3.instance.orderedSectionTags
|
RoomListStoreV3.instance.orderedSectionTags
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { NotificationStateEvents } from "../../stores/notifications/Notification
|
|||||||
import { type RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
|
import { type RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
|
||||||
import { isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
import { getCustomSectionData, isCustomSectionTag, isDefaultSectionTag } from "../../stores/room-list-v3/section";
|
||||||
|
|
||||||
interface RoomListSectionHeaderViewModelProps {
|
interface RoomListSectionHeaderViewModelProps {
|
||||||
tag: string;
|
tag: string;
|
||||||
@@ -139,8 +139,7 @@ export class RoomListSectionHeaderViewModel
|
|||||||
* Handle changes to custom section data.
|
* Handle changes to custom section data.
|
||||||
*/
|
*/
|
||||||
private onCustomSectionDataChange(): void {
|
private onCustomSectionDataChange(): void {
|
||||||
const customSectionData = SettingsStore.getValue("RoomList.CustomSectionData") || {};
|
const sectionData = isCustomSectionTag(this.props.tag) ? getCustomSectionData()[this.props.tag] : undefined;
|
||||||
const sectionData = customSectionData[this.props.tag];
|
|
||||||
if (sectionData) {
|
if (sectionData) {
|
||||||
this.snapshot.merge({ title: sectionData.name });
|
this.snapshot.merge({ title: sectionData.name });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ import { hasCreateRoomRights } from "./utils";
|
|||||||
import { keepIfSame } from "../../utils/keepIfSame";
|
import { keepIfSame } from "../../utils/keepIfSame";
|
||||||
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
|
||||||
import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel";
|
import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel";
|
||||||
|
import { getCustomSectionData, isCustomSectionTag, CHATS_TAG } from "../../stores/room-list-v3/section";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { tagRoom } from "../../utils/room/tagRoom";
|
import { tagRoom } from "../../utils/room/tagRoom";
|
||||||
import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom";
|
import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom";
|
||||||
import { CHATS_TAG } from "../../stores/room-list-v3/section";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the position of the active room within a specific section.
|
* Tracks the position of the active room within a specific section.
|
||||||
@@ -287,8 +287,7 @@ export class RoomListViewModel
|
|||||||
public getSectionHeaderViewModel(tag: string): RoomListSectionHeaderViewModel {
|
public getSectionHeaderViewModel(tag: string): RoomListSectionHeaderViewModel {
|
||||||
if (this.roomSectionHeaderViewModels.has(tag)) return this.roomSectionHeaderViewModels.get(tag)!;
|
if (this.roomSectionHeaderViewModels.has(tag)) return this.roomSectionHeaderViewModels.get(tag)!;
|
||||||
|
|
||||||
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
|
const title = TAG_TO_TITLE_MAP[tag] || (isCustomSectionTag(tag) && getCustomSectionData()[tag]?.name) || tag;
|
||||||
const title = TAG_TO_TITLE_MAP[tag] || customSections[tag]?.name || tag;
|
|
||||||
const viewModel = new RoomListSectionHeaderViewModel({
|
const viewModel = new RoomListSectionHeaderViewModel({
|
||||||
tag,
|
tag,
|
||||||
title,
|
title,
|
||||||
@@ -692,11 +691,13 @@ function computeSections(
|
|||||||
roomsResult: RoomsResult,
|
roomsResult: RoomsResult,
|
||||||
isSectionExpanded: (tag: string) => boolean,
|
isSectionExpanded: (tag: string) => boolean,
|
||||||
): { sections: Section[]; isFlatList: boolean } {
|
): { sections: Section[]; isFlatList: boolean } {
|
||||||
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
|
const customSections = getCustomSectionData();
|
||||||
|
|
||||||
const sections = roomsResult.sections
|
const sections = roomsResult.sections
|
||||||
// Only include sections that have rooms or are custom sections (which may be empty but should still be shown)
|
// Only include sections that have rooms or are custom sections (which may be empty but should still be shown)
|
||||||
.filter((section) => section.rooms.length > 0 || customSections[section.tag])
|
.filter(
|
||||||
|
(section) => section.rooms.length > 0 || (isCustomSectionTag(section.tag) && customSections[section.tag]),
|
||||||
|
)
|
||||||
// Remove roomIds for sections that are currently collapsed according to their section header view model
|
// Remove roomIds for sections that are currently collapsed according to their section header view model
|
||||||
.map((section) => ({
|
.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
|
|||||||
@@ -833,6 +833,7 @@ describe("RoomListStoreV3", () => {
|
|||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||||
if (setting === "feature_room_list_sections") return true;
|
if (setting === "feature_room_list_sections") return true;
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||||
|
if (setting === "RoomList.CustomSectionData") return {};
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1093,6 +1094,7 @@ describe("RoomListStoreV3", () => {
|
|||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||||
if (setting === "feature_room_list_sections") return true;
|
if (setting === "feature_room_list_sections") return true;
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||||
|
if (setting === "RoomList.CustomSectionData") return {};
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1107,11 +1109,13 @@ describe("RoomListStoreV3", () => {
|
|||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||||
if (setting === "feature_room_list_sections") return true;
|
if (setting === "feature_room_list_sections") return true;
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [customTag];
|
if (setting === "RoomList.OrderedCustomSections") return [customTag];
|
||||||
|
if (setting === "RoomList.CustomSectionData")
|
||||||
|
return { [customTag]: { tag: customTag, name: "Custom" } };
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger the settings watcher
|
// Trigger the settings watcher
|
||||||
settingsWatcher("RoomList.OrderedCustomSections");
|
await Promise.resolve(settingsWatcher("RoomList.OrderedCustomSections"));
|
||||||
|
|
||||||
// Now there should be 4 sections (Favourite, custom, Chats, LowPriority)
|
// Now there should be 4 sections (Favourite, custom, Chats, LowPriority)
|
||||||
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(4);
|
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(4);
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
import Modal from "../../../../src/Modal";
|
import Modal from "../../../../src/Modal";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
import {
|
import {
|
||||||
CHATS_TAG,
|
|
||||||
CUSTOM_SECTION_TAG_PREFIX,
|
|
||||||
createSection,
|
createSection,
|
||||||
editSection,
|
editSection,
|
||||||
deleteSection,
|
deleteSection,
|
||||||
|
getCustomSectionData,
|
||||||
|
getOrderedCustomSections,
|
||||||
isDefaultSectionTag,
|
isDefaultSectionTag,
|
||||||
|
CHATS_TAG,
|
||||||
|
CUSTOM_SECTION_TAG_PREFIX,
|
||||||
isSectionTag,
|
isSectionTag,
|
||||||
} from "../../../../src/stores/room-list-v3/section";
|
} from "../../../../src/stores/room-list-v3/section";
|
||||||
import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog";
|
import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog";
|
||||||
@@ -25,6 +27,68 @@ describe("section", () => {
|
|||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getCustomSectionData", () => {
|
||||||
|
const validTag = "element.io.section.valid";
|
||||||
|
const invalidTag = "element.io.section.invalid";
|
||||||
|
const validEntry = { tag: validTag, name: "Valid" };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, false, 42, "string", []] as const)("returns an empty object when the raw value is %p", (raw) => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(raw as any);
|
||||||
|
expect(getCustomSectionData()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns valid entries and drops invalid ones", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue({
|
||||||
|
[validTag]: validEntry,
|
||||||
|
[invalidTag]: { tag: "element.io.section.mismatch", name: "Bad" },
|
||||||
|
});
|
||||||
|
expect(getCustomSectionData()).toEqual({ [validTag]: validEntry });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops entries that fail the isValidCustomSection check", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue({
|
||||||
|
"element.io.section.null-val": null,
|
||||||
|
"element.io.section.str-val": "not-an-object",
|
||||||
|
"element.io.section.bad-tag": { tag: "not-a-custom-tag", name: "Bad" },
|
||||||
|
"element.io.section.bad-name": { tag: "element.io.section.bad-name", name: 42 },
|
||||||
|
});
|
||||||
|
expect(getCustomSectionData()).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOrderedCustomSections", () => {
|
||||||
|
const tag = "element.io.section.abc";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array when the raw value is not an array", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||||
|
if (setting === "RoomList.OrderedCustomSections") return "not-an-array";
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getOrderedCustomSections();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes unknown sections and saves the cleaned list", () => {
|
||||||
|
const knownTag = "element.io.section.known";
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||||
|
if (setting === "RoomList.CustomSectionData") return { [knownTag]: { tag: knownTag, name: "Known" } };
|
||||||
|
if (setting === "RoomList.OrderedCustomSections") return [knownTag, tag];
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getOrderedCustomSections()).toEqual([knownTag]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("createSection", () => {
|
describe("createSection", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
|
||||||
@@ -69,6 +133,8 @@ describe("section", () => {
|
|||||||
const existingTag = "element.io.section.existing";
|
const existingTag = "element.io.section.existing";
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [existingTag];
|
if (setting === "RoomList.OrderedCustomSections") return [existingTag];
|
||||||
|
if (setting === "RoomList.CustomSectionData")
|
||||||
|
return { [existingTag]: { tag: existingTag, name: "Existing" } };
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||||
@@ -79,15 +145,16 @@ describe("section", () => {
|
|||||||
|
|
||||||
await createSection();
|
await createSection();
|
||||||
|
|
||||||
const customDataCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.CustomSectionData");
|
|
||||||
const savedSection = Object.values(customDataCall![3] as Record<string, { tag: string; name: string }>)[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 orderedCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.OrderedCustomSections");
|
||||||
const savedOrder = orderedCall![3] as string[];
|
const savedOrder = orderedCall![3] as string[];
|
||||||
expect(savedOrder[0]).toBe(existingTag);
|
expect(savedOrder[0]).toBe(existingTag);
|
||||||
expect(savedOrder[1]).toMatch(/^element\.io\.section\./);
|
expect(savedOrder[1]).toMatch(/^element\.io\.section\./);
|
||||||
|
|
||||||
|
const newTag = savedOrder[1];
|
||||||
|
const customDataCall = setValueSpy.mock.calls.find(([name]) => name === "RoomList.CustomSectionData");
|
||||||
|
const savedSection = (customDataCall![3] as Record<string, { tag: string; name: string }>)[newTag];
|
||||||
|
expect(savedSection.name).toBe("My Section");
|
||||||
|
expect(savedSection.tag).toBe(newTag);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,6 +167,12 @@ describe("section", () => {
|
|||||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does nothing if the tag is not a custom section tag", async () => {
|
||||||
|
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||||
|
await editSection("m.favourite");
|
||||||
|
expect(createDialogSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("does nothing if the section does not exist", async () => {
|
it("does nothing if the section does not exist", async () => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||||
@@ -157,13 +230,20 @@ describe("section", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||||
if (setting === "RoomList.CustomSectionData") return { [tag]: { tag, name: "My Section" } };
|
if (setting === "RoomList.CustomSectionData")
|
||||||
|
return { [tag]: { tag, name: "My Section" }, [otherTag]: { tag: otherTag, name: "Other Section" } };
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [otherTag, tag];
|
if (setting === "RoomList.OrderedCustomSections") return [otherTag, tag];
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does nothing if the tag is not a custom section tag", async () => {
|
||||||
|
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||||
|
await deleteSection("m.favourite", false);
|
||||||
|
expect(createDialogSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("does nothing if the section does not exist", async () => {
|
it("does nothing if the section does not exist", async () => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||||
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
const createDialogSpy = jest.spyOn(Modal, "createDialog");
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ describe("RoomListItemViewModel", () => {
|
|||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||||
if (setting === "RoomList.showMessagePreview") return false;
|
if (setting === "RoomList.showMessagePreview") return false;
|
||||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||||
|
if (setting === "RoomList.CustomSectionData") return {};
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||||
|
|
||||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||||
|
|||||||
@@ -154,6 +154,19 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
|
|
||||||
expect(vm.getSnapshot().title).toBe("My Section");
|
expect(vm.getSnapshot().title).toBe("My Section");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not update title when tag is not a custom section tag", () => {
|
||||||
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
|
tag: "m.favourite",
|
||||||
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
|
onToggleExpanded,
|
||||||
|
});
|
||||||
|
|
||||||
|
watchCallback();
|
||||||
|
|
||||||
|
expect(vm.getSnapshot().title).toBe("Favourites");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("editSection", () => {
|
describe("editSection", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user