mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-07 06:58:25 +00:00
Iterate
This commit is contained in:
@@ -110,14 +110,14 @@ originalComponent: (props: CustomRoomPreviewBarComponentProps) => JSX.Element) =
|
|||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface DialogApiExtension {
|
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
|
// @public
|
||||||
export type DialogHandle<M> = {
|
export type DialogHandle<M> = {
|
||||||
finished: Promise<{
|
finished: Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
model: M;
|
model: M | null;
|
||||||
}>;
|
}>;
|
||||||
close(): void;
|
close(): void;
|
||||||
};
|
};
|
||||||
@@ -130,7 +130,7 @@ export interface DialogOptions {
|
|||||||
// @public
|
// @public
|
||||||
export type DialogProps<M> = {
|
export type DialogProps<M> = {
|
||||||
onSubmit(model: M): void;
|
onSubmit(model: M): void;
|
||||||
cancel(): void;
|
onCancel(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @alpha @deprecated (undocumented)
|
// @alpha @deprecated (undocumented)
|
||||||
@@ -287,7 +287,6 @@ export interface Profile {
|
|||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface ProfileApiExtension {
|
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>;
|
readonly profile: Watchable<Profile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +317,18 @@ export type Variables = {
|
|||||||
[key: string]: number | string | undefined;
|
[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)
|
// @alpha @deprecated (undocumented)
|
||||||
export interface WidgetPermissionsCustomisations<Widget, Capability> {
|
export interface WidgetPermissionsCustomisations<Widget, Capability> {
|
||||||
preapproveCapabilities?(widget: Widget, requestedCapabilities: Set<Capability>): Promise<Set<Capability>>;
|
preapproveCapabilities?(widget: Widget, requestedCapabilities: Set<Capability>): Promise<Set<Capability>>;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.1.6",
|
"vite": "^6.1.6",
|
||||||
"vite-plugin-dts": "^4.5.0",
|
"vite-plugin-dts": "^4.5.0",
|
||||||
"vitest": "^3.0.5",
|
"vitest": "^3.2.4",
|
||||||
"vitest-sonar-reporter": "^2.0.0"
|
"vitest-sonar-reporter": "^2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type DialogHandle<M> = {
|
|||||||
/**
|
/**
|
||||||
* Promise that resolves when the dialog is finished.
|
* 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.
|
* Method to close the dialog.
|
||||||
*/
|
*/
|
||||||
@@ -46,7 +46,7 @@ export type DialogProps<M> = {
|
|||||||
/**
|
/**
|
||||||
* Cancel the dialog programmatically.
|
* 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 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
|
* @param props - Additional props to pass to the body
|
||||||
*/
|
*/
|
||||||
openDialog<M, P>(
|
openDialog<M, P extends object>(
|
||||||
initialOptions: DialogOptions,
|
initialOptions: DialogOptions,
|
||||||
dialog: ComponentType<P & DialogProps<M>>,
|
dialog: ComponentType<P & DialogProps<M>>,
|
||||||
props?: P,
|
props: P,
|
||||||
): DialogHandle<M>;
|
): 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;
|
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.
|
* 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> {
|
export class Watchable<T> {
|
||||||
private readonly listeners = new Set<WatchFn<T>>();
|
private readonly listeners = new Set<WatchFn<T>>();
|
||||||
@@ -20,11 +33,17 @@ export class Watchable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public set value(value: T) {
|
public set value(value: T) {
|
||||||
if (this.currentValue !== value) {
|
// If the value hasn't changed, do nothing.
|
||||||
this.currentValue = value;
|
if (value === this.currentValue) {
|
||||||
for (const listener of this.listeners) {
|
return;
|
||||||
listener(this.currentValue);
|
}
|
||||||
}
|
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/dialog";
|
||||||
export type * from "./api/profile";
|
export type * from "./api/profile";
|
||||||
export type * from "./api/navigation";
|
export type * from "./api/navigation";
|
||||||
|
export * from "./api/watchable";
|
||||||
|
|||||||
Reference in New Issue
Block a user