Iterate
This commit is contained in:
@@ -110,14 +110,14 @@ originalComponent: (props: CustomRoomPreviewBarComponentProps) => JSX.Element) =
|
||||
|
||||
// @public
|
||||
export interface DialogApiExtension {
|
||||
openDialog<M, P>(initialOptions: DialogOptions, dialog: ComponentType<P & DialogProps<M>>, props?: P): DialogHandle<M>;
|
||||
openDialog<M, P extends object>(initialOptions: DialogOptions, dialog: ComponentType<P & DialogProps<M>>, props: P): DialogHandle<M>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type DialogHandle<M> = {
|
||||
finished: Promise<{
|
||||
ok: boolean;
|
||||
model: M;
|
||||
model: M | null;
|
||||
}>;
|
||||
close(): void;
|
||||
};
|
||||
@@ -130,7 +130,7 @@ export interface DialogOptions {
|
||||
// @public
|
||||
export type DialogProps<M> = {
|
||||
onSubmit(model: M): void;
|
||||
cancel(): void;
|
||||
onCancel(): void;
|
||||
};
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
@@ -287,7 +287,6 @@ export interface Profile {
|
||||
|
||||
// @public
|
||||
export interface ProfileApiExtension {
|
||||
// Warning: (ae-forgotten-export) The symbol "Watchable" needs to be exported by the entry point index.d.ts
|
||||
readonly profile: Watchable<Profile>;
|
||||
}
|
||||
|
||||
@@ -318,6 +317,18 @@ export type Variables = {
|
||||
[key: string]: number | string | undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class Watchable<T> {
|
||||
constructor(currentValue: T);
|
||||
// (undocumented)
|
||||
unwatch(listener: (value: T) => void): void;
|
||||
// (undocumented)
|
||||
get value(): T;
|
||||
set value(value: T);
|
||||
// (undocumented)
|
||||
watch(listener: (value: T) => void): void;
|
||||
}
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export interface WidgetPermissionsCustomisations<Widget, Capability> {
|
||||
preapproveCapabilities?(widget: Widget, requestedCapabilities: Set<Capability>): Promise<Set<Capability>>;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.6",
|
||||
"vite-plugin-dts": "^4.5.0",
|
||||
"vitest": "^3.0.5",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-sonar-reporter": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -26,7 +26,7 @@ export type DialogHandle<M> = {
|
||||
/**
|
||||
* Promise that resolves when the dialog is finished.
|
||||
*/
|
||||
finished: Promise<{ ok: boolean; model: M }>;
|
||||
finished: Promise<{ ok: boolean; model: M | null }>;
|
||||
/**
|
||||
* Method to close the dialog.
|
||||
*/
|
||||
@@ -46,7 +46,7 @@ export type DialogProps<M> = {
|
||||
/**
|
||||
* Cancel the dialog programmatically.
|
||||
*/
|
||||
cancel(): void;
|
||||
onCancel(): void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,9 +60,9 @@ export interface DialogApiExtension {
|
||||
* @param dialog - The body component to render in the dialog. This component should accept props of type `P`.
|
||||
* @param props - Additional props to pass to the body
|
||||
*/
|
||||
openDialog<M, P>(
|
||||
openDialog<M, P extends object>(
|
||||
initialOptions: DialogOptions,
|
||||
dialog: ComponentType<P & DialogProps<M>>,
|
||||
props?: P,
|
||||
props: P,
|
||||
): DialogHandle<M>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test, vitest } from "vitest";
|
||||
|
||||
import { Watchable } from "./watchable";
|
||||
|
||||
test("initial value is set correctly", () => {
|
||||
const watchable = new Watchable(42);
|
||||
expect(watchable.value).toBe(42);
|
||||
});
|
||||
|
||||
test("value can be updated", () => {
|
||||
const watchable = new Watchable(100);
|
||||
watchable.value = 200;
|
||||
expect(watchable.value).toBe(200);
|
||||
});
|
||||
|
||||
test("watchers are notified on value change", () => {
|
||||
const watchable = new Watchable(1);
|
||||
const listener = vitest.fn();
|
||||
|
||||
watchable.watch(listener);
|
||||
watchable.value = 2; // This should trigger the listener
|
||||
expect(listener).toHaveBeenCalledExactlyOnceWith(2);
|
||||
|
||||
watchable.unwatch(listener); // Clean up after the test
|
||||
});
|
||||
|
||||
test("watchers are not notified if value does not change", () => {
|
||||
const watchable = new Watchable(10);
|
||||
const listener = vitest.fn();
|
||||
|
||||
watchable.watch(listener);
|
||||
watchable.value = 10; // This should not trigger the listener
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
watchable.unwatch(listener); // Clean up after the test
|
||||
});
|
||||
|
||||
test("when value is an object, shallow comparison works", () => {
|
||||
const watchable = new Watchable({ a: 1, b: 2 });
|
||||
const listener = vitest.fn();
|
||||
watchable.watch(listener);
|
||||
|
||||
// Update with a new object that has the same properties
|
||||
watchable.value = { a: 3, b: 2 }; // This should trigger the listener
|
||||
expect(listener).toHaveBeenCalledExactlyOnceWith({ a: 3, b: 2 });
|
||||
listener.mockClear();
|
||||
watchable.value = { a: 3, b: 2 }; // This should not trigger the listener again
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
watchable.unwatch(listener); // Clean up after the test
|
||||
});
|
||||
@@ -7,8 +7,21 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
type WatchFn<T> = (value: T) => void;
|
||||
|
||||
function shallowCompare<T extends object>(obj1: T, obj2: T): boolean {
|
||||
return (
|
||||
Object.keys(obj1).length === Object.keys(obj2).length &&
|
||||
Object.keys(obj1).every((key) => obj1[key as keyof T] === obj2[key as keyof T])
|
||||
);
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is object {
|
||||
return value !== null && typeof value === "object";
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to wrap a value and allow listeners to be notified when the value changes.
|
||||
* If T is an object, it will use a shallow comparison to determine if the value has changed.
|
||||
* @public
|
||||
*/
|
||||
export class Watchable<T> {
|
||||
private readonly listeners = new Set<WatchFn<T>>();
|
||||
@@ -20,11 +33,17 @@ export class Watchable<T> {
|
||||
}
|
||||
|
||||
public set value(value: T) {
|
||||
if (this.currentValue !== value) {
|
||||
this.currentValue = value;
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.currentValue);
|
||||
}
|
||||
// If the value hasn't changed, do nothing.
|
||||
if (value === this.currentValue) {
|
||||
return;
|
||||
}
|
||||
if (isObject(value) && isObject(this.currentValue) && shallowCompare(this.currentValue as object, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentValue = value;
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@ export type * from "./api/auth";
|
||||
export type * from "./api/dialog";
|
||||
export type * from "./api/profile";
|
||||
export type * from "./api/navigation";
|
||||
export * from "./api/watchable";
|
||||
|
||||
Reference in New Issue
Block a user