@semiont/react-ui 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +13 -0
  2. package/dist/{chunk-4NOUO3W6.mjs → chunk-7VWNZ5YX.mjs} +5032 -2876
  3. package/dist/chunk-7VWNZ5YX.mjs.map +1 -0
  4. package/dist/index.d.mts +292 -25
  5. package/dist/index.mjs +1021 -332
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/test-utils.d.mts +1 -1
  8. package/dist/test-utils.mjs +4 -2352
  9. package/dist/test-utils.mjs.map +1 -1
  10. package/package.json +1 -1
  11. package/src/components/StatusDisplay.tsx +1 -1
  12. package/src/components/modals/PermissionDeniedModal.tsx +2 -2
  13. package/src/components/modals/SessionExpiredModal.tsx +4 -4
  14. package/src/components/resource/panels/AssessmentPanel.tsx +4 -0
  15. package/src/components/resource/panels/AssistSection.tsx +10 -1
  16. package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
  17. package/src/components/resource/panels/CommentsPanel.tsx +4 -0
  18. package/src/components/resource/panels/HighlightPanel.tsx +4 -0
  19. package/src/components/resource/panels/ReferencesPanel.tsx +11 -0
  20. package/src/components/resource/panels/TaggingPanel.tsx +10 -0
  21. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +11 -1
  22. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +2 -2
  23. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  24. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  25. package/src/features/admin-exchange/components/ImportCard.tsx +1 -1
  26. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +171 -0
  27. package/src/features/admin-exchange/state/exchange-state-unit.ts +131 -0
  28. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  29. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +68 -0
  30. package/src/features/admin-security/state/admin-security-state-unit.ts +46 -0
  31. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  32. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +86 -0
  33. package/src/features/admin-users/state/admin-users-state-unit.ts +73 -0
  34. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +86 -0
  35. package/src/features/auth-welcome/state/welcome-state-unit.ts +44 -0
  36. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  37. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +102 -0
  38. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +64 -0
  39. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  40. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  41. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  42. package/src/features/resource-compose/__tests__/UploadProgressBar.test.tsx +225 -0
  43. package/src/features/resource-compose/components/ResourceComposePage.tsx +19 -4
  44. package/src/features/resource-compose/components/UploadProgressBar.tsx +94 -0
  45. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +187 -0
  46. package/src/features/resource-compose/state/compose-page-state-unit.ts +209 -0
  47. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  48. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +76 -0
  49. package/src/features/resource-discovery/state/discover-state-unit.ts +54 -0
  50. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +4 -2
  51. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +36 -32
  52. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +46 -0
  53. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +203 -0
  54. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +26 -0
  55. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +180 -0
  56. package/dist/chunk-4NOUO3W6.mjs.map +0 -1
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';
3
+ import { filter } from 'rxjs/operators';
4
+ import { annotationId, resourceId as makeResourceId } from '@semiont/core';
5
+ import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
+ import { createResourceViewerPageStateUnit } from '../resource-viewer-page-state-unit';
7
+ import { makeTestClient, type TestClient } from '../../../../__tests__/test-client';
8
+
9
+ const RID = makeResourceId('res-1');
10
+
11
+ function mockBrowse(): ShellStateUnit {
12
+ return {
13
+ activePanel$: new BehaviorSubject(null).asObservable(),
14
+ scrollToAnnotationId$: new BehaviorSubject(null).asObservable(),
15
+ panelInitialTab$: new BehaviorSubject(null).asObservable(),
16
+ onScrollCompleted: vi.fn(),
17
+ openPanel: vi.fn(),
18
+ closePanel: vi.fn(),
19
+ dispose: vi.fn(),
20
+ } as unknown as ShellStateUnit;
21
+ }
22
+
23
+ function clientWithNamespaces(overrides: {
24
+ annotations$?: BehaviorSubject<unknown[] | undefined>;
25
+ entityTypes$?: BehaviorSubject<string[] | undefined>;
26
+ events$?: BehaviorSubject<unknown[] | undefined>;
27
+ referencedBy$?: BehaviorSubject<unknown[] | undefined>;
28
+ resourceRepresentation?: ReturnType<typeof vi.fn>;
29
+ mediaToken?: ReturnType<typeof vi.fn>;
30
+ } = {}): TestClient {
31
+ const annotations$ = overrides.annotations$ ?? new BehaviorSubject<unknown[] | undefined>([]);
32
+ const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
33
+ const events$ = overrides.events$ ?? new BehaviorSubject<unknown[] | undefined>([]);
34
+ const referencedBy$ = overrides.referencedBy$ ?? new BehaviorSubject<unknown[] | undefined>([]);
35
+
36
+ return makeTestClient({
37
+ browse: {
38
+ annotations: () => annotations$.asObservable(),
39
+ entityTypes: () => entityTypes$.asObservable(),
40
+ events: () => events$.asObservable(),
41
+ referencedBy: () => referencedBy$.asObservable(),
42
+ resourceRepresentation: overrides.resourceRepresentation ?? vi.fn().mockResolvedValue({
43
+ data: new TextEncoder().encode('hello').buffer,
44
+ contentType: 'text/plain',
45
+ }),
46
+ },
47
+ auth: {
48
+ mediaToken: overrides.mediaToken ?? vi.fn().mockResolvedValue({ token: 'tok-123' }),
49
+ },
50
+ mark: {
51
+ annotation: vi.fn().mockResolvedValue({ annotationId: 'ann-new' }),
52
+ delete: vi.fn().mockResolvedValue(undefined),
53
+ assist: vi.fn(() => new Observable(() => {})),
54
+ },
55
+ gather: { annotation: vi.fn(() => new Observable(() => {})) },
56
+ match: { search: vi.fn(() => new Observable(() => {})) },
57
+ yield: { fromAnnotation: vi.fn(() => new Observable(() => {})) },
58
+ bind: { body: vi.fn().mockResolvedValue(undefined) },
59
+ subscribeToResource: vi.fn().mockReturnValue(() => {}),
60
+ });
61
+ }
62
+
63
+ describe('createResourceViewerPageStateUnit', () => {
64
+ let tc: TestClient;
65
+
66
+ afterEach(() => { tc?.bus.destroy(); });
67
+
68
+ it('exposes flow VMs', () => {
69
+ tc = clientWithNamespaces();
70
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
71
+
72
+ expect(stateUnit.beckon).toBeDefined();
73
+ expect(stateUnit.mark).toBeDefined();
74
+ expect(stateUnit.gather).toBeDefined();
75
+ expect(stateUnit.yield).toBeDefined();
76
+ expect(stateUnit.browse).toBeDefined();
77
+
78
+ stateUnit.dispose();
79
+ });
80
+
81
+ it('derives annotations from browse namespace', async () => {
82
+ const annotations$ = new BehaviorSubject<unknown[] | undefined>([
83
+ { id: 'a1', motivation: 'highlighting' },
84
+ ]);
85
+ tc = clientWithNamespaces({ annotations$ });
86
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
87
+
88
+ const anns = await firstValueFrom(stateUnit.annotations$);
89
+ expect(anns).toHaveLength(1);
90
+
91
+ stateUnit.dispose();
92
+ });
93
+
94
+ it('groups annotations by type', async () => {
95
+ const annotations$ = new BehaviorSubject<unknown[] | undefined>([
96
+ { id: 'a1', motivation: 'highlighting', target: { selector: { type: 'TextQuoteSelector', exact: 'x' } } },
97
+ { id: 'a2', motivation: 'commenting', body: [{ type: 'TextualBody', value: 'note' }], target: { selector: { type: 'TextQuoteSelector', exact: 'y' } } },
98
+ ]);
99
+ tc = clientWithNamespaces({ annotations$ });
100
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
101
+
102
+ const groups = await firstValueFrom(stateUnit.annotationGroups$);
103
+ expect(groups.highlights.length + groups.comments.length).toBeGreaterThanOrEqual(1);
104
+
105
+ stateUnit.dispose();
106
+ });
107
+
108
+ it('exposes entity types', async () => {
109
+ tc = clientWithNamespaces();
110
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
111
+
112
+ const types = await firstValueFrom(stateUnit.entityTypes$);
113
+ expect(types).toEqual(['Person']);
114
+
115
+ stateUnit.dispose();
116
+ });
117
+
118
+ it('exposes events from browse namespace', async () => {
119
+ const events$ = new BehaviorSubject<unknown[] | undefined>([{ id: 'e1', type: 'mark:added' }]);
120
+ tc = clientWithNamespaces({ events$ });
121
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
122
+
123
+ const events = await firstValueFrom(stateUnit.events$);
124
+ expect(events).toEqual([{ id: 'e1', type: 'mark:added' }]);
125
+
126
+ stateUnit.dispose();
127
+ });
128
+
129
+ it('exposes referencedBy from browse namespace', async () => {
130
+ const referencedBy$ = new BehaviorSubject<unknown[] | undefined>([{ resourceId: 'r2' }]);
131
+ tc = clientWithNamespaces({ referencedBy$ });
132
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
133
+
134
+ const refs = await firstValueFrom(stateUnit.referencedBy$);
135
+ expect(refs).toEqual([{ resourceId: 'r2' }]);
136
+
137
+ stateUnit.dispose();
138
+ });
139
+
140
+ it('fetches media token for binary types', async () => {
141
+ const mediaToken = vi.fn().mockResolvedValue({ token: 'tok-456' });
142
+ tc = clientWithNamespaces({ mediaToken });
143
+ const stateUnit = createResourceViewerPageStateUnit(
144
+ tc.client, RID, 'en', mockBrowse(),
145
+ { mediaType: 'image/png' },
146
+ );
147
+
148
+ const token = await firstValueFrom(stateUnit.mediaToken$.pipe(filter((t) => t !== null)));
149
+ expect(token).toBe('tok-456');
150
+
151
+ stateUnit.dispose();
152
+ });
153
+
154
+ it('wizard initializes closed', async () => {
155
+ tc = clientWithNamespaces();
156
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
157
+
158
+ const wizard = await firstValueFrom(stateUnit.wizard$);
159
+ expect(wizard.open).toBe(false);
160
+
161
+ stateUnit.dispose();
162
+ });
163
+
164
+ it('bind:initiate opens wizard and fires gather:requested', async () => {
165
+ tc = clientWithNamespaces();
166
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
167
+ const gatherEvents: unknown[] = [];
168
+ tc.bus.get('gather:requested').subscribe((e) => gatherEvents.push(e));
169
+
170
+ tc.bus.get('bind:initiate').next({
171
+ annotationId: annotationId('ann-1'),
172
+ resourceId: makeResourceId('res-1'),
173
+ defaultTitle: 'Test',
174
+ entityTypes: ['Person'],
175
+ });
176
+
177
+ const wizard = await firstValueFrom(stateUnit.wizard$.pipe(filter((w) => w.open)));
178
+ expect(wizard.annotationId).toBe('ann-1');
179
+ expect(gatherEvents).toHaveLength(1);
180
+
181
+ stateUnit.dispose();
182
+ });
183
+
184
+ it('closeWizard resets wizard state', async () => {
185
+ tc = clientWithNamespaces();
186
+ const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
187
+
188
+ tc.bus.get('bind:initiate').next({
189
+ annotationId: annotationId('ann-1'),
190
+ resourceId: makeResourceId('res-1'),
191
+ defaultTitle: 'Test',
192
+ entityTypes: [],
193
+ });
194
+
195
+ await firstValueFrom(stateUnit.wizard$.pipe(filter((w) => w.open)));
196
+ stateUnit.closeWizard();
197
+
198
+ const wizard = await firstValueFrom(stateUnit.wizard$.pipe(filter((w) => !w.open)));
199
+ expect(wizard.open).toBe(false);
200
+
201
+ stateUnit.dispose();
202
+ });
203
+ });
@@ -0,0 +1,26 @@
1
+ import { type Observable, map } from 'rxjs';
2
+ import type { ResourceDescriptor, ResourceId } from '@semiont/core';
3
+ import type { StateUnit } from '@semiont/sdk';
4
+ import type { SemiontClient } from '@semiont/sdk';
5
+
6
+ export interface ResourceLoaderStateUnit extends StateUnit {
7
+ resource$: Observable<ResourceDescriptor | undefined>;
8
+ isLoading$: Observable<boolean>;
9
+ invalidate(): void;
10
+ }
11
+
12
+ export function createResourceLoaderStateUnit(
13
+ client: SemiontClient,
14
+ resourceId: ResourceId,
15
+ ): ResourceLoaderStateUnit {
16
+ const raw$ = client.browse.resource(resourceId);
17
+ const resource$ = raw$;
18
+ const isLoading$: Observable<boolean> = raw$.pipe(map((r) => r === undefined));
19
+
20
+ return {
21
+ resource$,
22
+ isLoading$,
23
+ invalidate: () => client.browse.invalidateResourceDetail(resourceId),
24
+ dispose: () => {},
25
+ };
26
+ }
@@ -0,0 +1,180 @@
1
+ import { BehaviorSubject, type Observable, map } from 'rxjs';
2
+ import type { ResourceId, components } from '@semiont/core';
3
+ import { createDisposer } from '@semiont/sdk';
4
+ import type { StateUnit } from '@semiont/sdk';
5
+ import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
+ import { createBeckonStateUnit, type BeckonStateUnit } from '@semiont/sdk';
7
+ import { createMarkStateUnit, type MarkStateUnit } from '@semiont/sdk';
8
+ import { createGatherStateUnit, type GatherStateUnit } from '@semiont/sdk';
9
+ import { createMatchStateUnit } from '@semiont/sdk';
10
+ import { createYieldStateUnit, type YieldStateUnit } from '@semiont/sdk';
11
+ import type { SemiontClient } from '@semiont/sdk';
12
+ import { decodeWithCharset } from '@semiont/core';
13
+ import { isHighlight, isComment, isAssessment, isReference, isTag } from '@semiont/core';
14
+ import type { ReferencedByEntry } from '@semiont/sdk';
15
+
16
+ import type { Annotation } from '@semiont/core';
17
+
18
+ export interface AnnotationGroups {
19
+ highlights: Annotation[];
20
+ comments: Annotation[];
21
+ assessments: Annotation[];
22
+ references: Annotation[];
23
+ tags: Annotation[];
24
+ }
25
+ type StoredEventResponse = components['schemas']['StoredEventResponse'];
26
+
27
+ export interface WizardState {
28
+ open: boolean;
29
+ annotationId: string | null;
30
+ resourceId: string | null;
31
+ defaultTitle: string;
32
+ entityTypes: string[];
33
+ }
34
+
35
+ const WIZARD_CLOSED: WizardState = {
36
+ open: false, annotationId: null, resourceId: null, defaultTitle: '', entityTypes: [],
37
+ };
38
+
39
+ export interface ResourceViewerPageStateUnit extends StateUnit {
40
+ beckon: BeckonStateUnit;
41
+ browse: ShellStateUnit;
42
+ mark: MarkStateUnit;
43
+ gather: GatherStateUnit;
44
+ yield: YieldStateUnit;
45
+
46
+ annotations$: Observable<Annotation[]>;
47
+ annotationGroups$: Observable<AnnotationGroups>;
48
+ entityTypes$: Observable<string[]>;
49
+ events$: Observable<StoredEventResponse[]>;
50
+ referencedBy$: Observable<ReferencedByEntry[]>;
51
+ content$: Observable<string>;
52
+ contentLoading$: Observable<boolean>;
53
+ mediaToken$: Observable<string | null>;
54
+ wizard$: Observable<WizardState>;
55
+
56
+ closeWizard(): void;
57
+ }
58
+
59
+ export function createResourceViewerPageStateUnit(
60
+ client: SemiontClient,
61
+ resourceId: ResourceId,
62
+ locale: string,
63
+ browse: ShellStateUnit,
64
+ options?: { mediaType?: string },
65
+ ): ResourceViewerPageStateUnit {
66
+ const disposer = createDisposer();
67
+
68
+ const beckon = createBeckonStateUnit(client);
69
+ const mark = createMarkStateUnit(client, resourceId);
70
+ const gather = createGatherStateUnit(client, resourceId);
71
+ const matchStateUnit = createMatchStateUnit(client, resourceId);
72
+ const yieldStateUnit = createYieldStateUnit(client, resourceId, locale);
73
+
74
+ disposer.add(beckon);
75
+ disposer.add(browse);
76
+ disposer.add(mark);
77
+ disposer.add(gather);
78
+ disposer.add(matchStateUnit);
79
+ disposer.add(yieldStateUnit);
80
+
81
+ const annotations$: Observable<Annotation[]> = client.browse.annotations(resourceId).pipe(
82
+ map((a) => a ?? []),
83
+ );
84
+
85
+ const annotationGroups$: Observable<AnnotationGroups> = annotations$.pipe(
86
+ map((anns) => {
87
+ const groups: AnnotationGroups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
88
+ for (const ann of anns) {
89
+ if (isHighlight(ann)) groups.highlights.push(ann);
90
+ else if (isComment(ann)) groups.comments.push(ann);
91
+ else if (isAssessment(ann)) groups.assessments.push(ann);
92
+ else if (isReference(ann)) groups.references.push(ann);
93
+ else if (isTag(ann)) groups.tags.push(ann);
94
+ }
95
+ return groups;
96
+ }),
97
+ );
98
+
99
+ const entityTypes$: Observable<string[]> = client.browse.entityTypes().pipe(
100
+ map((e) => e ?? []),
101
+ );
102
+
103
+ const events$: Observable<StoredEventResponse[]> = client.browse.events(resourceId).pipe(
104
+ map((e) => e ?? []),
105
+ );
106
+
107
+ const referencedBy$: Observable<ReferencedByEntry[]> = client.browse.referencedBy(resourceId).pipe(
108
+ map((r) => r ?? []),
109
+ );
110
+
111
+ const content$ = new BehaviorSubject<string>('');
112
+ const contentLoading$ = new BehaviorSubject<boolean>(false);
113
+ const mediaToken$ = new BehaviorSubject<string | null>(null);
114
+
115
+ const mediaType = options?.mediaType || 'text/plain';
116
+ const isBinaryType = mediaType.startsWith('image/') || mediaType === 'application/pdf';
117
+
118
+ if (!isBinaryType && mediaType) {
119
+ contentLoading$.next(true);
120
+ client.browse.resourceRepresentation(resourceId, { accept: mediaType })
121
+ .then(({ data }) => {
122
+ content$.next(decodeWithCharset(data, mediaType));
123
+ contentLoading$.next(false);
124
+ })
125
+ .catch(() => { contentLoading$.next(false); });
126
+ }
127
+
128
+ if (isBinaryType) {
129
+ client.auth!.mediaToken(resourceId)
130
+ .then(({ token }) => mediaToken$.next(token))
131
+ .catch(() => {});
132
+ }
133
+
134
+ const wizard$ = new BehaviorSubject<WizardState>(WIZARD_CLOSED);
135
+
136
+ const unsubscribeResource = client.subscribeToResource(resourceId);
137
+ disposer.add(unsubscribeResource);
138
+
139
+ const bindInitiateSub = client.bus.get('bind:initiate').subscribe((event) => {
140
+ wizard$.next({
141
+ open: true,
142
+ annotationId: event.annotationId,
143
+ resourceId: event.resourceId,
144
+ defaultTitle: event.defaultTitle,
145
+ entityTypes: event.entityTypes,
146
+ });
147
+ client.bus.get('gather:requested').next({
148
+ correlationId: crypto.randomUUID(),
149
+ annotationId: event.annotationId,
150
+ resourceId: event.resourceId,
151
+ options: { contextWindow: 2000 },
152
+ });
153
+ });
154
+ disposer.add(() => bindInitiateSub.unsubscribe());
155
+
156
+ return {
157
+ beckon,
158
+ browse,
159
+ mark,
160
+ gather,
161
+ yield: yieldStateUnit,
162
+ annotations$,
163
+ annotationGroups$,
164
+ entityTypes$,
165
+ events$,
166
+ referencedBy$,
167
+ content$: content$.asObservable(),
168
+ contentLoading$: contentLoading$.asObservable(),
169
+ mediaToken$: mediaToken$.asObservable(),
170
+ wizard$: wizard$.asObservable(),
171
+ closeWizard: () => wizard$.next(WIZARD_CLOSED),
172
+ dispose: () => {
173
+ wizard$.complete();
174
+ content$.complete();
175
+ contentLoading$.complete();
176
+ mediaToken$.complete();
177
+ disposer.dispose();
178
+ },
179
+ };
180
+ }