@semiont/react-ui 0.5.9 → 0.5.10
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.
- package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js → PdfAnnotationCanvas.client-P37SXBWQ.js} +2 -2
- package/dist/{ar-WA47UUWA.js → ar-ZHKO4XDR.js} +45 -4
- package/dist/ar-ZHKO4XDR.js.map +1 -0
- package/dist/{bn-5ANDRIU6.js → bn-3BUDKAK5.js} +45 -4
- package/dist/bn-3BUDKAK5.js.map +1 -0
- package/dist/{chunk-WHUGODTB.js → chunk-5LFFAVDI.js} +32 -32
- package/dist/{chunk-YBGN4ACY.js → chunk-O23TLLXW.js} +1 -6
- package/dist/{chunk-GERSK2DY.js → chunk-R52YAGHK.js} +42 -1
- package/dist/chunk-R52YAGHK.js.map +1 -0
- package/dist/{cs-3RU7F4JX.js → cs-2EFEUKTW.js} +45 -4
- package/dist/cs-2EFEUKTW.js.map +1 -0
- package/dist/{da-GSW5P5HG.js → da-SUD3Q44Q.js} +45 -4
- package/dist/da-SUD3Q44Q.js.map +1 -0
- package/dist/{de-JUAUYF4Z.js → de-6IC5QRX7.js} +45 -4
- package/dist/de-6IC5QRX7.js.map +1 -0
- package/dist/{el-JNLWCKEC.js → el-Q4NAXFRC.js} +45 -4
- package/dist/el-Q4NAXFRC.js.map +1 -0
- package/dist/{en-RBN2GUHF.js → en-P2L4X27N.js} +5 -3
- package/dist/{es-WPOX225H.js → es-YUKZBD4W.js} +45 -4
- package/dist/es-YUKZBD4W.js.map +1 -0
- package/dist/{fa-TAEP6N77.js → fa-OJ47LILE.js} +45 -4
- package/dist/fa-OJ47LILE.js.map +1 -0
- package/dist/{fi-ZVZHANSP.js → fi-QFFL5CUB.js} +45 -4
- package/dist/fi-QFFL5CUB.js.map +1 -0
- package/dist/{fr-VLZW7M4N.js → fr-BOQZZWP4.js} +45 -4
- package/dist/fr-BOQZZWP4.js.map +1 -0
- package/dist/{he-QFAFYA77.js → he-LXDY3TFA.js} +45 -4
- package/dist/he-LXDY3TFA.js.map +1 -0
- package/dist/{hi-AO3DQPCV.js → hi-HOJER77V.js} +45 -4
- package/dist/hi-HOJER77V.js.map +1 -0
- package/dist/{id-GTXF42WM.js → id-I5WQ6EWD.js} +45 -4
- package/dist/id-I5WQ6EWD.js.map +1 -0
- package/dist/index.d.ts +119 -9
- package/dist/index.js +4417 -18858
- package/dist/index.js.map +1 -1
- package/dist/integrations/css-modules-helper.js +1 -1
- package/dist/integrations/styled-components-theme.js +1 -1
- package/dist/{it-AS5GM232.js → it-UYH642IJ.js} +45 -4
- package/dist/it-UYH642IJ.js.map +1 -0
- package/dist/{ja-GZ4HLUOF.js → ja-JTVWAC4L.js} +45 -4
- package/dist/ja-JTVWAC4L.js.map +1 -0
- package/dist/{ko-A4XUXFGJ.js → ko-IHXFMOIZ.js} +45 -4
- package/dist/ko-IHXFMOIZ.js.map +1 -0
- package/dist/{ms-YBAO3S6K.js → ms-JH25VFLO.js} +45 -4
- package/dist/ms-JH25VFLO.js.map +1 -0
- package/dist/{nl-3TJGIIIU.js → nl-SURXW7IQ.js} +45 -4
- package/dist/nl-SURXW7IQ.js.map +1 -0
- package/dist/{no-4AXIQPRQ.js → no-HXIKQFDZ.js} +45 -4
- package/dist/no-HXIKQFDZ.js.map +1 -0
- package/dist/{pl-5GP6GKXO.js → pl-7A4KU6XK.js} +45 -4
- package/dist/pl-7A4KU6XK.js.map +1 -0
- package/dist/{pt-26Y6JFG5.js → pt-6MGYP7Q5.js} +45 -4
- package/dist/pt-6MGYP7Q5.js.map +1 -0
- package/dist/{ro-C7UXFRWJ.js → ro-IROCHNIA.js} +45 -4
- package/dist/ro-IROCHNIA.js.map +1 -0
- package/dist/{sv-44DVD76T.js → sv-WDJD75CV.js} +45 -4
- package/dist/sv-WDJD75CV.js.map +1 -0
- package/dist/test-utils.js +3 -3
- package/dist/{th-GIQRTBOY.js → th-RIO2E6B3.js} +45 -4
- package/dist/th-RIO2E6B3.js.map +1 -0
- package/dist/{tr-WMQWO4D6.js → tr-TZ7ERCIN.js} +45 -4
- package/dist/tr-TZ7ERCIN.js.map +1 -0
- package/dist/{uk-I7ML6RGG.js → uk-ZFWDKLUE.js} +45 -4
- package/dist/uk-ZFWDKLUE.js.map +1 -0
- package/dist/{vi-FGWECASQ.js → vi-UIOSP2JG.js} +45 -4
- package/dist/vi-UIOSP2JG.js.map +1 -0
- package/dist/{zh-L5FN73XC.js → zh-6R6YUY46.js} +45 -4
- package/dist/zh-6R6YUY46.js.map +1 -0
- package/package.json +17 -8
- package/src/components/AnnotateReferencesProgressWidget.tsx +35 -16
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +41 -91
- package/src/components/modals/ConfigureGatherStep.tsx +102 -0
- package/src/components/modals/ContextSummary.tsx +15 -6
- package/src/components/modals/GatherContextStep.tsx +104 -59
- package/src/components/modals/ResourceGenerateModal.tsx +258 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +82 -0
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +107 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +197 -0
- package/src/components/resource/panels/ReferencesPanel.tsx +10 -0
- package/src/components/resource/panels/ResourceInfoPanel.tsx +22 -0
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +26 -0
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +14 -0
- package/src/features/admin-exchange/state/exchange-state-unit.ts +4 -2
- package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +14 -0
- package/src/features/admin-security/state/admin-security-state-unit.ts +4 -2
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +12 -5
- package/src/features/admin-users/state/admin-users-state-unit.ts +4 -2
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +11 -0
- package/src/features/auth-welcome/state/welcome-state-unit.ts +1 -1
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +14 -0
- package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +4 -2
- package/src/features/resource-compose/components/ResourceComposePage.tsx +13 -7
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +13 -0
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -2
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +15 -9
- package/src/features/resource-discovery/state/discover-state-unit.ts +5 -2
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +79 -0
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +11 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +30 -0
- package/src/features/resource-viewer/state/resource-loader-state-unit.ts +1 -1
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +5 -2
- package/translations/ar.json +41 -2
- package/translations/bn.json +41 -2
- package/translations/cs.json +41 -2
- package/translations/da.json +41 -2
- package/translations/de.json +41 -2
- package/translations/el.json +41 -2
- package/translations/en.json +39 -0
- package/translations/es.json +41 -2
- package/translations/fa.json +41 -2
- package/translations/fi.json +41 -2
- package/translations/fr.json +41 -2
- package/translations/he.json +41 -2
- package/translations/hi.json +41 -2
- package/translations/id.json +41 -2
- package/translations/it.json +41 -2
- package/translations/ja.json +41 -2
- package/translations/ko.json +41 -2
- package/translations/ms.json +41 -2
- package/translations/nl.json +41 -2
- package/translations/no.json +41 -2
- package/translations/pl.json +41 -2
- package/translations/pt.json +41 -2
- package/translations/ro.json +41 -2
- package/translations/sv.json +41 -2
- package/translations/th.json +41 -2
- package/translations/tr.json +41 -2
- package/translations/uk.json +41 -2
- package/translations/vi.json +41 -2
- package/translations/zh.json +41 -2
- package/dist/ar-WA47UUWA.js.map +0 -1
- package/dist/bn-5ANDRIU6.js.map +0 -1
- package/dist/chunk-GERSK2DY.js.map +0 -1
- package/dist/cs-3RU7F4JX.js.map +0 -1
- package/dist/da-GSW5P5HG.js.map +0 -1
- package/dist/de-JUAUYF4Z.js.map +0 -1
- package/dist/el-JNLWCKEC.js.map +0 -1
- package/dist/es-WPOX225H.js.map +0 -1
- package/dist/fa-TAEP6N77.js.map +0 -1
- package/dist/fi-ZVZHANSP.js.map +0 -1
- package/dist/fr-VLZW7M4N.js.map +0 -1
- package/dist/he-QFAFYA77.js.map +0 -1
- package/dist/hi-AO3DQPCV.js.map +0 -1
- package/dist/id-GTXF42WM.js.map +0 -1
- package/dist/it-AS5GM232.js.map +0 -1
- package/dist/ja-GZ4HLUOF.js.map +0 -1
- package/dist/ko-A4XUXFGJ.js.map +0 -1
- package/dist/ms-YBAO3S6K.js.map +0 -1
- package/dist/nl-3TJGIIIU.js.map +0 -1
- package/dist/no-4AXIQPRQ.js.map +0 -1
- package/dist/pl-5GP6GKXO.js.map +0 -1
- package/dist/pt-26Y6JFG5.js.map +0 -1
- package/dist/ro-C7UXFRWJ.js.map +0 -1
- package/dist/sv-44DVD76T.js.map +0 -1
- package/dist/th-GIQRTBOY.js.map +0 -1
- package/dist/tr-WMQWO4D6.js.map +0 -1
- package/dist/uk-I7ML6RGG.js.map +0 -1
- package/dist/vi-FGWECASQ.js.map +0 -1
- package/dist/zh-L5FN73XC.js.map +0 -1
- /package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js.map → PdfAnnotationCanvas.client-P37SXBWQ.js.map} +0 -0
- /package/dist/{chunk-WHUGODTB.js.map → chunk-5LFFAVDI.js.map} +0 -0
- /package/dist/{chunk-YBGN4ACY.js.map → chunk-O23TLLXW.js.map} +0 -0
- /package/dist/{en-RBN2GUHF.js.map → en-P2L4X27N.js.map} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATE-FROM-BUTTON P2/P4 — the gather-options form.
|
|
3
|
+
*
|
|
4
|
+
* Pure presentational form (no providers): it owns the gather config in local
|
|
5
|
+
* state and emits a `ResourceGatherConfig` on submit. The `children` slot hosts
|
|
6
|
+
* the Phase-4 exclusion multi-select.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
10
|
+
import '@testing-library/jest-dom';
|
|
11
|
+
import { ConfigureGatherStep } from '../ConfigureGatherStep';
|
|
12
|
+
|
|
13
|
+
const t = {
|
|
14
|
+
intro: 'Choose what to include.',
|
|
15
|
+
includeContent: 'Include content',
|
|
16
|
+
includeSummary: 'Include summary',
|
|
17
|
+
depth: 'Depth',
|
|
18
|
+
maxResources: 'Max resources',
|
|
19
|
+
cancel: 'Cancel',
|
|
20
|
+
gather: 'Gather',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('ConfigureGatherStep', () => {
|
|
24
|
+
it('renders the intro and the default option values', () => {
|
|
25
|
+
render(<ConfigureGatherStep onGather={vi.fn()} onCancel={vi.fn()} translations={t} />);
|
|
26
|
+
expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByLabelText('Include content')).toBeChecked();
|
|
28
|
+
expect(screen.getByLabelText('Include summary')).toBeChecked();
|
|
29
|
+
expect(screen.getByLabelText('Depth')).toHaveValue(2);
|
|
30
|
+
expect(screen.getByLabelText('Max resources')).toHaveValue(10);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('emits the default config on submit', () => {
|
|
34
|
+
const onGather = vi.fn();
|
|
35
|
+
render(<ConfigureGatherStep onGather={onGather} onCancel={vi.fn()} translations={t} />);
|
|
36
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
37
|
+
expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reflects edits in the emitted config', () => {
|
|
41
|
+
const onGather = vi.fn();
|
|
42
|
+
render(<ConfigureGatherStep onGather={onGather} onCancel={vi.fn()} translations={t} />);
|
|
43
|
+
fireEvent.click(screen.getByLabelText('Include content')); // uncheck
|
|
44
|
+
fireEvent.click(screen.getByLabelText('Include summary')); // uncheck
|
|
45
|
+
fireEvent.change(screen.getByLabelText('Depth'), { target: { value: '4' } });
|
|
46
|
+
fireEvent.change(screen.getByLabelText('Max resources'), { target: { value: '25' } });
|
|
47
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
48
|
+
expect(onGather).toHaveBeenCalledWith({ includeContent: false, includeSummary: false, depth: 4, maxResources: 25 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('honors the defaults prop', () => {
|
|
52
|
+
const onGather = vi.fn();
|
|
53
|
+
render(
|
|
54
|
+
<ConfigureGatherStep
|
|
55
|
+
defaults={{ includeSummary: false, depth: 1, maxResources: 5 }}
|
|
56
|
+
onGather={onGather}
|
|
57
|
+
onCancel={vi.fn()}
|
|
58
|
+
translations={t}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
expect(screen.getByLabelText('Include summary')).not.toBeChecked();
|
|
62
|
+
expect(screen.getByLabelText('Depth')).toHaveValue(1);
|
|
63
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
64
|
+
expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: false, depth: 1, maxResources: 5 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('calls onCancel from the cancel button', () => {
|
|
68
|
+
const onCancel = vi.fn();
|
|
69
|
+
render(<ConfigureGatherStep onGather={vi.fn()} onCancel={onCancel} translations={t} />);
|
|
70
|
+
fireEvent.click(screen.getByRole('button', { name: /Cancel/ }));
|
|
71
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders the children slot (exclusion picker)', () => {
|
|
75
|
+
render(
|
|
76
|
+
<ConfigureGatherStep onGather={vi.fn()} onCancel={vi.fn()} translations={t}>
|
|
77
|
+
<div>EXCLUDE-SLOT</div>
|
|
78
|
+
</ConfigureGatherStep>,
|
|
79
|
+
);
|
|
80
|
+
expect(screen.getByText('EXCLUDE-SLOT')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATE-FROM-BUTTON P1 — the GatheredContext display renders a resource focus.
|
|
3
|
+
*
|
|
4
|
+
* ContextSummary's graph views are focus-agnostic; GatherContextStep gains a
|
|
5
|
+
* resource-focus strip and hides the annotation-only controls (hint + the
|
|
6
|
+
* Bind/Generate/Compose footer) for a resource focus.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
+
import { render } from '@testing-library/react';
|
|
10
|
+
import type { GatheredContext } from '@semiont/core';
|
|
11
|
+
import { GatherContextStep } from '../GatherContextStep';
|
|
12
|
+
import { ContextSummary } from '../ContextSummary';
|
|
13
|
+
|
|
14
|
+
const t = {
|
|
15
|
+
title: 'Gather',
|
|
16
|
+
loadingContext: 'Loading…',
|
|
17
|
+
failedContext: 'Failed',
|
|
18
|
+
search: 'Search',
|
|
19
|
+
generate: 'Generate',
|
|
20
|
+
compose: 'Compose',
|
|
21
|
+
resolutionStrategyLabel: 'Strategy',
|
|
22
|
+
sourceContextLabel: 'Source',
|
|
23
|
+
connectionsLabel: 'Connections',
|
|
24
|
+
citedByLabel: 'Cited by',
|
|
25
|
+
userHintLabel: 'Hint',
|
|
26
|
+
userHintPlaceholder: 'hint…',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resourceContext(): GatheredContext {
|
|
30
|
+
return {
|
|
31
|
+
focus: {
|
|
32
|
+
kind: 'resource',
|
|
33
|
+
resource: { id: 'res-1', name: 'My Resource' },
|
|
34
|
+
summary: 'A short summary',
|
|
35
|
+
suggestedReferences: ['Suggested Topic'],
|
|
36
|
+
content: { main: 'main content' },
|
|
37
|
+
},
|
|
38
|
+
graph: {
|
|
39
|
+
nodes: [
|
|
40
|
+
{ id: 'res-1', type: 'resource', label: 'My Resource' },
|
|
41
|
+
{ id: 'res-2', type: 'resource', label: 'Related Resource', entityTypes: ['Topic'] },
|
|
42
|
+
],
|
|
43
|
+
edges: [{ source: 'res-1', target: 'res-2', type: 'peer' }],
|
|
44
|
+
},
|
|
45
|
+
metadata: { entityTypes: ['Topic'] },
|
|
46
|
+
} as unknown as GatheredContext;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('GatheredContext display — resource focus', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
// jsdom doesn't implement scrollIntoView — GatherContextStep's annotation
|
|
52
|
+
// strip calls it on mount. (Same stub the panel tests use.)
|
|
53
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('ContextSummary renders graph views (connections) for a resource focus', () => {
|
|
57
|
+
const { container } = render(<ContextSummary context={resourceContext()} translations={t} />);
|
|
58
|
+
expect(container.textContent).toContain('Related Resource'); // peer connection from deriveViews
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('GatherContextStep shows the resource strip and hides the annotation-only footer', () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<GatherContextStep
|
|
64
|
+
context={resourceContext()}
|
|
65
|
+
contextLoading={false}
|
|
66
|
+
contextError={null}
|
|
67
|
+
translations={t}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
expect(container.textContent).toContain('My Resource'); // focal resource name
|
|
71
|
+
expect(container.textContent).toContain('A short summary'); // resource summary
|
|
72
|
+
expect(container.textContent).toContain('Suggested Topic'); // suggestedReferences chip
|
|
73
|
+
expect(container.textContent).toContain('Related Resource'); // graph view via ContextSummary
|
|
74
|
+
// annotation-only controls are gated out for a resource focus
|
|
75
|
+
expect(container.textContent).not.toContain('Strategy');
|
|
76
|
+
expect(container.querySelector('.semiont-gather__footer')).toBeNull();
|
|
77
|
+
expect(container.querySelector('.semiont-gather__hint-textarea')).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('GatherContextStep still shows the footer for an annotation focus', () => {
|
|
81
|
+
const annotationContext = {
|
|
82
|
+
focus: {
|
|
83
|
+
kind: 'annotation',
|
|
84
|
+
annotation: { id: 'anno-1', motivation: 'linking' },
|
|
85
|
+
sourceResource: { id: 'res-1', name: 'Host' },
|
|
86
|
+
selected: { before: 'a ', text: 'term', after: ' b' },
|
|
87
|
+
},
|
|
88
|
+
graph: { nodes: [], edges: [] },
|
|
89
|
+
metadata: {},
|
|
90
|
+
} as unknown as GatheredContext;
|
|
91
|
+
const { container } = render(
|
|
92
|
+
<GatherContextStep
|
|
93
|
+
context={annotationContext}
|
|
94
|
+
contextLoading={false}
|
|
95
|
+
contextError={null}
|
|
96
|
+
userHint=""
|
|
97
|
+
onUserHintChange={() => {}}
|
|
98
|
+
onBind={() => {}}
|
|
99
|
+
onGenerate={() => {}}
|
|
100
|
+
onCompose={() => {}}
|
|
101
|
+
translations={t}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
expect(container.querySelector('.semiont-gather__footer')).not.toBeNull();
|
|
105
|
+
expect(container.textContent).toContain('Strategy');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATE-FROM-BUTTON P2/P4 — the resource-generate flow modal.
|
|
3
|
+
*
|
|
4
|
+
* Drives the step machine: configure-gather → review → configure-generation.
|
|
5
|
+
* `useResourceGather` is mocked so we control the gathered context and assert
|
|
6
|
+
* how the step wires `gather()` (incl. the Phase-4 exclusion threading) and
|
|
7
|
+
* `onGenerateSubmit`. `useSemiont` is mocked to feed the entity-type options.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
10
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import { BehaviorSubject, of } from 'rxjs';
|
|
13
|
+
import type { GatheredContext } from '@semiont/core';
|
|
14
|
+
import type { GenerationConfig } from '../ConfigureGenerationStep';
|
|
15
|
+
|
|
16
|
+
const RESOURCE_CONTEXT = {
|
|
17
|
+
focus: { kind: 'resource', resource: { id: 'res-1', name: 'My Resource' }, summary: 'A short summary' },
|
|
18
|
+
graph: { nodes: [{ id: 'res-1', type: 'resource', label: 'My Resource' }], edges: [] },
|
|
19
|
+
metadata: {},
|
|
20
|
+
} as unknown as GatheredContext;
|
|
21
|
+
|
|
22
|
+
// Mutable hook state + spies, shared with the hoisted vi.mock factory.
|
|
23
|
+
const h = vi.hoisted(() => ({
|
|
24
|
+
gather: vi.fn(),
|
|
25
|
+
reset: vi.fn(),
|
|
26
|
+
state: { context: null as unknown, loading: false, error: null as Error | null },
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('../../../hooks/useResourceGather', () => ({
|
|
30
|
+
useResourceGather: () => ({
|
|
31
|
+
context: h.state.context,
|
|
32
|
+
loading: h.state.loading,
|
|
33
|
+
error: h.state.error,
|
|
34
|
+
gather: h.gather,
|
|
35
|
+
reset: h.reset,
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Stable observable instance — the modal calls `entityTypes()` inline in
|
|
40
|
+
// render, so a fresh observable each call would re-subscribe every render and
|
|
41
|
+
// loop. The real SDK returns a cached observable; mirror that here.
|
|
42
|
+
const entityTypes$ = of<string[]>(['Person', 'Topic']);
|
|
43
|
+
const mockClient = { browse: { entityTypes: () => entityTypes$ } };
|
|
44
|
+
const activeSession$ = new BehaviorSubject<unknown>({ client: mockClient });
|
|
45
|
+
vi.mock('../../../session/SemiontProvider', async () => {
|
|
46
|
+
const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>('../../../session/SemiontProvider');
|
|
47
|
+
return { ...actual, useSemiont: () => ({ activeSession$ }) };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
import { ResourceGenerateModal } from '../ResourceGenerateModal';
|
|
51
|
+
|
|
52
|
+
const T = {
|
|
53
|
+
gatherTitle: 'Configure Gather',
|
|
54
|
+
reviewTitle: 'Review Context',
|
|
55
|
+
configureTitle: 'Configure Generation',
|
|
56
|
+
next: 'Next',
|
|
57
|
+
back: 'Back',
|
|
58
|
+
cancel: 'Cancel',
|
|
59
|
+
gatherIntro: 'Choose what to include.',
|
|
60
|
+
includeContent: 'Include content',
|
|
61
|
+
includeSummary: 'Include summary',
|
|
62
|
+
gatherDepth: 'Depth',
|
|
63
|
+
gatherMaxResources: 'Max resources',
|
|
64
|
+
gatherButton: 'Gather',
|
|
65
|
+
excludeLabel: 'Exclude from recall',
|
|
66
|
+
loadingContext: 'Gathering…',
|
|
67
|
+
failedContext: 'Failed',
|
|
68
|
+
sourceContextLabel: 'Resource',
|
|
69
|
+
connectionsLabel: 'Connections',
|
|
70
|
+
citedByLabel: 'Cited by',
|
|
71
|
+
resourceTitle: 'New resource title',
|
|
72
|
+
resourceTitlePlaceholder: 'Title…',
|
|
73
|
+
additionalInstructions: 'Additional Instructions',
|
|
74
|
+
additionalInstructionsPlaceholder: 'Optional…',
|
|
75
|
+
language: 'Language',
|
|
76
|
+
languageHelp: 'Language help',
|
|
77
|
+
creativity: 'Creativity',
|
|
78
|
+
creativityFocused: 'Focused',
|
|
79
|
+
creativityCreative: 'Creative',
|
|
80
|
+
maxLength: 'Max Length',
|
|
81
|
+
maxLengthHelp: 'Max help',
|
|
82
|
+
generate: 'Generate',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let onClose: Mock<() => void>;
|
|
86
|
+
let onGenerateSubmit: Mock<(resourceId: string, config: GenerationConfig) => void>;
|
|
87
|
+
|
|
88
|
+
function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerateModal>> = {}) {
|
|
89
|
+
return render(
|
|
90
|
+
<ResourceGenerateModal
|
|
91
|
+
isOpen
|
|
92
|
+
onClose={onClose}
|
|
93
|
+
resourceId="res-1"
|
|
94
|
+
defaultTitle="Default Title"
|
|
95
|
+
locale="en"
|
|
96
|
+
onGenerateSubmit={onGenerateSubmit}
|
|
97
|
+
translations={T}
|
|
98
|
+
{...props}
|
|
99
|
+
/>,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('ResourceGenerateModal', () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
vi.clearAllMocks();
|
|
106
|
+
onClose = vi.fn<() => void>();
|
|
107
|
+
onGenerateSubmit = vi.fn<(resourceId: string, config: GenerationConfig) => void>();
|
|
108
|
+
h.state = { context: null, loading: false, error: null };
|
|
109
|
+
activeSession$.next({ client: mockClient });
|
|
110
|
+
// jsdom doesn't implement scrollIntoView; GatherContextStep may call it.
|
|
111
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders nothing when closed', () => {
|
|
115
|
+
renderModal({ isOpen: false });
|
|
116
|
+
expect(screen.queryByText('Configure Gather')).not.toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('opens on the configure-gather step with the exclusion options', () => {
|
|
120
|
+
renderModal();
|
|
121
|
+
expect(screen.getByText('Configure Gather')).toBeInTheDocument(); // step title
|
|
122
|
+
expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByLabelText('Include content')).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByRole('button', { name: 'Person' })).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByRole('button', { name: 'Topic' })).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('submitting the gather step calls gather() and advances to review (exclusion omitted when none picked)', () => {
|
|
129
|
+
renderModal();
|
|
130
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
131
|
+
expect(h.gather).toHaveBeenCalledWith('res-1', { includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
|
|
132
|
+
expect(screen.getByText('Review Context')).toBeInTheDocument(); // step title flipped
|
|
133
|
+
expect(screen.getByRole('button', { name: /Next/ })).toBeDisabled(); // no context yet
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('threads picked entity types into the gather call', () => {
|
|
137
|
+
renderModal();
|
|
138
|
+
fireEvent.click(screen.getByRole('button', { name: 'Person' })); // select to exclude
|
|
139
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
140
|
+
expect(h.gather).toHaveBeenCalledWith('res-1', {
|
|
141
|
+
includeContent: true,
|
|
142
|
+
includeSummary: true,
|
|
143
|
+
depth: 2,
|
|
144
|
+
maxResources: 10,
|
|
145
|
+
excludeEntityTypes: ['Person'],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('walks gather → review → configure-generation → emits onGenerateSubmit then closes', () => {
|
|
150
|
+
h.state.context = RESOURCE_CONTEXT; // gather already resolved
|
|
151
|
+
renderModal();
|
|
152
|
+
|
|
153
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
|
|
154
|
+
const next = screen.getByRole('button', { name: /Next/ });
|
|
155
|
+
expect(next).toBeEnabled();
|
|
156
|
+
fireEvent.click(next); // → configure-generation
|
|
157
|
+
|
|
158
|
+
fireEvent.change(screen.getByLabelText('New resource title'), { target: { value: 'Generated Doc' } });
|
|
159
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
|
|
160
|
+
fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
|
|
161
|
+
|
|
162
|
+
expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(onGenerateSubmit).toHaveBeenCalledWith(
|
|
164
|
+
'res-1',
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
title: 'Generated Doc',
|
|
167
|
+
storagePath: 'file://generated/out.md',
|
|
168
|
+
context: RESOURCE_CONTEXT,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
expect(onClose).toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('Back from configure-generation returns to review', () => {
|
|
175
|
+
h.state.context = RESOURCE_CONTEXT;
|
|
176
|
+
renderModal();
|
|
177
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
|
|
178
|
+
fireEvent.click(screen.getByRole('button', { name: /Next/ })); // → configure-generation
|
|
179
|
+
expect(screen.getByText('Configure Generation')).toBeInTheDocument();
|
|
180
|
+
fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → review
|
|
181
|
+
expect(screen.getByText('Review Context')).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('Back from review returns to the configure-gather step', () => {
|
|
185
|
+
renderModal();
|
|
186
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
|
|
187
|
+
expect(screen.getByText('Review Context')).toBeInTheDocument();
|
|
188
|
+
fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → configure-gather
|
|
189
|
+
expect(screen.getByText('Configure Gather')).toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('the header close button calls onClose', () => {
|
|
193
|
+
renderModal();
|
|
194
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
195
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -455,6 +455,16 @@ export function ReferencesPanel({
|
|
|
455
455
|
<AnnotateReferencesProgressWidget
|
|
456
456
|
progress={progress}
|
|
457
457
|
annotationType="reference"
|
|
458
|
+
cancelJobType="annotation"
|
|
459
|
+
translations={{
|
|
460
|
+
title: t('annotationProgressTitle'),
|
|
461
|
+
cancel: t('cancelAnnotation'),
|
|
462
|
+
inProgress: t('annotating'),
|
|
463
|
+
complete: t('complete'),
|
|
464
|
+
failed: t('failed'),
|
|
465
|
+
found: (count) => t('found', { count }),
|
|
466
|
+
current: (entityType) => t('current', { entityType }),
|
|
467
|
+
}}
|
|
458
468
|
/>
|
|
459
469
|
)}
|
|
460
470
|
</>
|
|
@@ -23,6 +23,12 @@ interface Props {
|
|
|
23
23
|
wasAttributedTo?: Agent | Agent[] | undefined;
|
|
24
24
|
wasDerivedFrom?: string | string[] | undefined;
|
|
25
25
|
generator?: Agent | Agent[] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Open the resource-generate flow. UI-only — the SDK isn't involved in
|
|
28
|
+
* *opening* the display (if collaborative gather ever lands, this graduates
|
|
29
|
+
* to a verb). Omit to hide the Generate action.
|
|
30
|
+
*/
|
|
31
|
+
onGenerate?: () => void;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -45,6 +51,7 @@ export function ResourceInfoPanel({
|
|
|
45
51
|
wasAttributedTo,
|
|
46
52
|
wasDerivedFrom,
|
|
47
53
|
generator,
|
|
54
|
+
onGenerate,
|
|
48
55
|
}: Props) {
|
|
49
56
|
const t = useTranslations('ResourceInfoPanel');
|
|
50
57
|
const session = useObservable(useSemiont().activeSession$);
|
|
@@ -177,6 +184,21 @@ export function ResourceInfoPanel({
|
|
|
177
184
|
</div>
|
|
178
185
|
)}
|
|
179
186
|
|
|
187
|
+
{/* Generate Action — opens the resource-generate flow (UI-only) */}
|
|
188
|
+
{onGenerate && (
|
|
189
|
+
<div className="semiont-resource-info-panel__action-section">
|
|
190
|
+
<button
|
|
191
|
+
onClick={onGenerate}
|
|
192
|
+
className="semiont-resource-button semiont-resource-button--secondary"
|
|
193
|
+
>
|
|
194
|
+
✨ {t('generate')}
|
|
195
|
+
</button>
|
|
196
|
+
<p className="semiont-resource-info-panel__description">
|
|
197
|
+
{t('generateDescription')}
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
180
202
|
{/* Clone Action */}
|
|
181
203
|
<div className="semiont-resource-info-panel__action-section">
|
|
182
204
|
<button
|
|
@@ -20,6 +20,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
20
20
|
storageUri: 'Storage',
|
|
21
21
|
clone: 'Clone',
|
|
22
22
|
cloneDescription: 'Generate a shareable clone link for this resource',
|
|
23
|
+
generate: 'Generate',
|
|
24
|
+
generateDescription: "Generate a new resource from this one's context",
|
|
23
25
|
archive: 'Archive',
|
|
24
26
|
archiveDescription: 'Move this resource to archived status',
|
|
25
27
|
unarchive: 'Unarchive',
|
|
@@ -269,6 +271,30 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
269
271
|
});
|
|
270
272
|
});
|
|
271
273
|
|
|
274
|
+
describe('Generate Action', () => {
|
|
275
|
+
it('renders the Generate button when onGenerate is provided', () => {
|
|
276
|
+
renderWithEventBus(
|
|
277
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={() => {}} />
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByRole('button', { name: /Generate/i })).toBeInTheDocument();
|
|
280
|
+
expect(screen.getByText("Generate a new resource from this one's context")).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('hides the Generate button when onGenerate is omitted', () => {
|
|
284
|
+
renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
|
|
285
|
+
expect(screen.queryByRole('button', { name: /Generate/i })).not.toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('calls onGenerate when clicked', () => {
|
|
289
|
+
const onGenerate = vi.fn();
|
|
290
|
+
renderWithEventBus(
|
|
291
|
+
<ResourceInfoPanel {...defaultProps} onGenerate={onGenerate} />
|
|
292
|
+
);
|
|
293
|
+
fireEvent.click(screen.getByRole('button', { name: /Generate/i }));
|
|
294
|
+
expect(onGenerate).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
272
298
|
describe('Provenance Section', () => {
|
|
273
299
|
it('should not render provenance section when no provenance data', () => {
|
|
274
300
|
renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
import { firstValueFrom, of, throwError } from 'rxjs';
|
|
3
3
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
4
4
|
import { createExchangeStateUnit } from '../exchange-state-unit';
|
|
5
|
+
import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
|
|
5
6
|
|
|
6
7
|
function mockBrowse(): ShellStateUnit {
|
|
7
8
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
@@ -169,3 +170,16 @@ describe('createExchangeStateUnit', () => {
|
|
|
169
170
|
stateUnit.dispose();
|
|
170
171
|
});
|
|
171
172
|
});
|
|
173
|
+
|
|
174
|
+
describe('ExchangeStateUnit — StateUnit axioms', () => {
|
|
175
|
+
it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
|
|
176
|
+
assertStateUnitAxioms({
|
|
177
|
+
setup: () => {
|
|
178
|
+
const browse = disposeProbe();
|
|
179
|
+
return { unit: createExchangeStateUnit(browse as unknown as ShellStateUnit, vi.fn(), vi.fn()), passedIn: [browse] };
|
|
180
|
+
},
|
|
181
|
+
surfaces: (u) => [u.selectedFile$, u.preview$, u.importPhase$, u.importMessage$, u.importResult$, u.isExporting$, u.isImporting$],
|
|
182
|
+
invocations: (u) => [() => u.cancelImport(), () => u.doImport()],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BehaviorSubject, lastValueFrom, type Observable } from 'rxjs';
|
|
2
2
|
import { tap } from 'rxjs/operators';
|
|
3
3
|
import { createDisposer } from '@semiont/sdk';
|
|
4
|
-
import type { StateUnit } from '@semiont/
|
|
4
|
+
import type { StateUnit } from '@semiont/core';
|
|
5
5
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
6
6
|
import type { BackendDownload, ProgressEvent } from '@semiont/core';
|
|
7
7
|
|
|
@@ -33,7 +33,9 @@ export function createExchangeStateUnit(
|
|
|
33
33
|
importFn: (file: File) => Observable<ProgressEvent>,
|
|
34
34
|
): ExchangeStateUnit {
|
|
35
35
|
const disposer = createDisposer();
|
|
36
|
-
|
|
36
|
+
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
37
|
+
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
38
|
+
// shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
|
|
37
39
|
|
|
38
40
|
const selectedFile$ = new BehaviorSubject<File | null>(null);
|
|
39
41
|
const preview$ = new BehaviorSubject<ImportPreview | null>(null);
|
|
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
|
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
6
6
|
import { createAdminSecurityStateUnit } from '../admin-security-state-unit';
|
|
7
|
+
import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
|
|
7
8
|
|
|
8
9
|
function mockBrowse(): ShellStateUnit {
|
|
9
10
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
@@ -66,3 +67,16 @@ describe('createAdminSecurityStateUnit', () => {
|
|
|
66
67
|
stateUnit.dispose();
|
|
67
68
|
});
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
describe('AdminSecurityStateUnit — StateUnit axioms', () => {
|
|
72
|
+
it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
|
|
73
|
+
assertStateUnitAxioms({
|
|
74
|
+
setup: () => {
|
|
75
|
+
const browse = disposeProbe();
|
|
76
|
+
const client = mockClient(vi.fn().mockResolvedValue({ providers: [], allowedDomains: [] }));
|
|
77
|
+
return { unit: createAdminSecurityStateUnit(client, browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
78
|
+
},
|
|
79
|
+
surfaces: (u) => [u.providers$, u.allowedDomains$, u.isLoading$],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import { createDisposer } from '@semiont/sdk';
|
|
3
|
-
import type { StateUnit } from '@semiont/
|
|
3
|
+
import type { StateUnit } from '@semiont/core';
|
|
4
4
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
5
5
|
import type { SemiontClient } from '@semiont/sdk';
|
|
6
6
|
|
|
@@ -16,7 +16,9 @@ export function createAdminSecurityStateUnit(
|
|
|
16
16
|
browse: ShellStateUnit,
|
|
17
17
|
): AdminSecurityStateUnit {
|
|
18
18
|
const disposer = createDisposer();
|
|
19
|
-
|
|
19
|
+
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
20
|
+
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
21
|
+
// shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
|
|
20
22
|
|
|
21
23
|
const providers$ = new BehaviorSubject<unknown[]>([]);
|
|
22
24
|
const allowedDomains$ = new BehaviorSubject<string[]>([]);
|
|
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
|
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
6
6
|
import { createAdminUsersStateUnit } from '../admin-users-state-unit';
|
|
7
|
+
import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
|
|
7
8
|
|
|
8
9
|
function mockBrowse(): ShellStateUnit {
|
|
9
10
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
@@ -77,10 +78,16 @@ describe('createAdminUsersStateUnit', () => {
|
|
|
77
78
|
stateUnit.dispose();
|
|
78
79
|
});
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('AdminUsersStateUnit — StateUnit axioms', () => {
|
|
84
|
+
it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
|
|
85
|
+
assertStateUnitAxioms({
|
|
86
|
+
setup: () => {
|
|
87
|
+
const browse = disposeProbe();
|
|
88
|
+
return { unit: createAdminUsersStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
89
|
+
},
|
|
90
|
+
surfaces: (u) => [u.users$, u.stats$, u.usersLoading$, u.statsLoading$],
|
|
91
|
+
});
|
|
85
92
|
});
|
|
86
93
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import { userDID } from '@semiont/core';
|
|
3
3
|
import { createDisposer } from '@semiont/sdk';
|
|
4
|
-
import type { StateUnit } from '@semiont/
|
|
4
|
+
import type { StateUnit } from '@semiont/core';
|
|
5
5
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
6
6
|
import type { SemiontClient } from '@semiont/sdk';
|
|
7
7
|
|
|
@@ -19,7 +19,9 @@ export function createAdminUsersStateUnit(
|
|
|
19
19
|
browse: ShellStateUnit,
|
|
20
20
|
): AdminUsersStateUnit {
|
|
21
21
|
const disposer = createDisposer();
|
|
22
|
-
|
|
22
|
+
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
23
|
+
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
24
|
+
// shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
|
|
23
25
|
|
|
24
26
|
const users$ = new BehaviorSubject<unknown[]>([]);
|
|
25
27
|
const stats$ = new BehaviorSubject<unknown | null>(null);
|
|
@@ -3,6 +3,7 @@ import { firstValueFrom } from 'rxjs';
|
|
|
3
3
|
import { filter } from 'rxjs/operators';
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
import { createWelcomeStateUnit } from '../welcome-state-unit';
|
|
6
|
+
import { assertStateUnitAxioms } from '@semiont/core/testing';
|
|
6
7
|
|
|
7
8
|
function mockClient(overrides: {
|
|
8
9
|
getMe?: ReturnType<typeof vi.fn>;
|
|
@@ -84,3 +85,13 @@ describe('createWelcomeStateUnit', () => {
|
|
|
84
85
|
stateUnit.dispose();
|
|
85
86
|
});
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
describe('WelcomeStateUnit — StateUnit axioms', () => {
|
|
90
|
+
it('satisfies the StateUnit axioms', () => {
|
|
91
|
+
assertStateUnitAxioms({
|
|
92
|
+
setup: () => createWelcomeStateUnit(mockClient()),
|
|
93
|
+
surfaces: (u) => [u.userData$, u.isProcessing$],
|
|
94
|
+
invocations: (u) => [() => u.acceptTerms()],
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import { createDisposer } from '@semiont/sdk';
|
|
3
|
-
import type { StateUnit } from '@semiont/
|
|
3
|
+
import type { StateUnit } from '@semiont/core';
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
|
|
6
6
|
export interface WelcomeStateUnit extends StateUnit {
|