@semiont/react-ui 0.5.27 → 0.5.28
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-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
- package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
- package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
- package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
- package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
- package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
- package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
- package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
- package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
- package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
- package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
- package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
- package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
- package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
- package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
- package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
- package/dist/index.css +279 -74
- package/dist/index.d.ts +170 -145
- package/dist/index.js +2743 -2365
- package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
- package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
- package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
- package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
- package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
- package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
- package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
- package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
- package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
- package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
- package/dist/test-utils.js +2 -2
- package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
- package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
- package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
- package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
- package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
- package/package.json +9 -8
- package/src/components/AssistProgress.tsx +36 -2
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/__tests__/AssistProgress.test.tsx +59 -0
- package/src/components/modals/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
- package/src/components/modals/ConfigureSearchStep.tsx +19 -0
- package/src/components/modals/ContextSummary.tsx +185 -72
- package/src/components/modals/CorpusPane.tsx +75 -0
- package/src/components/modals/DiscardPrompt.tsx +38 -0
- package/src/components/modals/GatherContextStep.tsx +237 -107
- package/src/components/modals/ReferenceWizardModal.tsx +238 -50
- package/src/components/modals/ResourceGenerateModal.tsx +216 -108
- package/src/components/modals/SearchModal.css +337 -101
- package/src/components/modals/SearchResultsStep.tsx +61 -68
- package/src/components/modals/WizardFooter.tsx +7 -1
- package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +35 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +417 -38
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
- package/src/components/resource/AnnotateView.tsx +3 -3
- package/src/components/resource/BrowseView.tsx +5 -5
- package/src/components/resource/ResourceViewer.tsx +4 -4
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +9 -9
- package/src/components/resource/annotate-renderers.tsx +3 -3
- package/src/components/resource/panels/AssistSection.css +35 -0
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +33 -1
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
- package/src/styles/core/forms.css +35 -8
- package/src/styles/features/resource-viewer.css +0 -22
- package/src/styles/features/resource.css +0 -4
- package/translations/ar.json +38 -22
- package/translations/bn.json +38 -22
- package/translations/cs.json +38 -22
- package/translations/da.json +38 -22
- package/translations/de.json +38 -22
- package/translations/el.json +38 -22
- package/translations/en.json +38 -22
- package/translations/es.json +38 -22
- package/translations/fa.json +38 -22
- package/translations/fi.json +38 -22
- package/translations/fr.json +38 -22
- package/translations/he.json +38 -22
- package/translations/hi.json +38 -22
- package/translations/id.json +38 -22
- package/translations/it.json +38 -22
- package/translations/ja.json +38 -22
- package/translations/ko.json +38 -22
- package/translations/ms.json +38 -22
- package/translations/nl.json +38 -22
- package/translations/no.json +38 -22
- package/translations/pl.json +38 -22
- package/translations/pt.json +38 -22
- package/translations/ro.json +38 -22
- package/translations/sv.json +38 -22
- package/translations/th.json +38 -22
- package/translations/tr.json +38 -22
- package/translations/uk.json +38 -22
- package/translations/vi.json +38 -22
- package/translations/zh.json +38 -22
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
- package/dist/ar-OU3DQGC6.js.map +0 -1
- package/dist/bn-PHWCR6AZ.js.map +0 -1
- package/dist/chunk-5VDTWRXJ.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-PCRRBU7H.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/cs-NEXYZ62I.js.map +0 -1
- package/dist/da-FAT43YCU.js.map +0 -1
- package/dist/de-KRQ3WBCU.js.map +0 -1
- package/dist/el-Q4IU3EKC.js.map +0 -1
- package/dist/en-2KADE7N4.js.map +0 -1
- package/dist/es-2IO3R2MK.js.map +0 -1
- package/dist/fa-Q5GOQFXX.js.map +0 -1
- package/dist/fi-R6NMRWBE.js.map +0 -1
- package/dist/fr-QY4D5E6J.js.map +0 -1
- package/dist/he-X6Y77XHY.js.map +0 -1
- package/dist/hi-XL7IOEQM.js.map +0 -1
- package/dist/id-QZ4ZZS67.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/css-modules-helper.js.map +0 -1
- package/dist/integrations/styled-components-theme.js.map +0 -1
- package/dist/it-ME2Q7OB6.js.map +0 -1
- package/dist/ja-HX7QASRI.js.map +0 -1
- package/dist/ko-EQO5AHAV.js.map +0 -1
- package/dist/ms-UILFSF3I.js.map +0 -1
- package/dist/nl-NNI45PMK.js.map +0 -1
- package/dist/no-MAL5F6P6.js.map +0 -1
- package/dist/pl-4RMY6JAJ.js.map +0 -1
- package/dist/pt-VQH2X3QQ.js.map +0 -1
- package/dist/ro-XXCQO2OG.js.map +0 -1
- package/dist/sv-7Z6EJ6U4.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-L3DXXYVQ.js.map +0 -1
- package/dist/tr-P3DFVLFJ.js.map +0 -1
- package/dist/uk-7C5D3PXS.js.map +0 -1
- package/dist/vi-ZLTLEO7P.js.map +0 -1
- package/dist/zh-5FHVWDIN.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* ResourceComposePage Component
|
|
3
3
|
*
|
|
4
4
|
* Pure React component for creating and editing resources.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports two modes: new resource and clone. (Reference completion moved
|
|
6
|
+
* into the resolve wizard — COMPOSE-IN-MODAL.)
|
|
6
7
|
* All dependencies passed as props - no Next.js hooks!
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React, { useState, useEffect } from 'react';
|
|
10
|
-
import { deriveViews, type GatheredContext } from '@semiont/core';
|
|
11
11
|
import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
|
|
12
12
|
import type { UploadProgress } from '@semiont/sdk';
|
|
13
|
-
import { type CloneData
|
|
13
|
+
import { type CloneData } from '../state/compose-page-state-unit';
|
|
14
14
|
import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
|
|
15
15
|
import { buttonStyles } from '../../../lib/button-styles';
|
|
16
16
|
import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
|
|
@@ -42,10 +42,8 @@ function detectUploadMediaType(file: File): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface ResourceComposePageProps {
|
|
45
|
-
mode: 'new' | 'clone'
|
|
45
|
+
mode: 'new' | 'clone';
|
|
46
46
|
cloneData?: CloneData | null;
|
|
47
|
-
referenceData?: ReferenceData | null;
|
|
48
|
-
gatheredContext?: GatheredContext | null;
|
|
49
47
|
|
|
50
48
|
// Available options
|
|
51
49
|
availableEntityTypes: string[];
|
|
@@ -74,10 +72,7 @@ export interface ResourceComposePageProps {
|
|
|
74
72
|
translations: {
|
|
75
73
|
title: string;
|
|
76
74
|
titleEditClone: string;
|
|
77
|
-
titleCompleteReference: string;
|
|
78
75
|
subtitleClone: string;
|
|
79
|
-
subtitleReference: string;
|
|
80
|
-
linkedNoticePrefix: string;
|
|
81
76
|
resourceName: string;
|
|
82
77
|
resourceNamePlaceholder: string;
|
|
83
78
|
entityTypes: string;
|
|
@@ -99,9 +94,7 @@ export interface ResourceComposePageProps {
|
|
|
99
94
|
cancel: string;
|
|
100
95
|
saving: string;
|
|
101
96
|
creating: string;
|
|
102
|
-
creatingAndLinking: string;
|
|
103
97
|
saveClonedResource: string;
|
|
104
|
-
createAndLinkResource: string;
|
|
105
98
|
createResource: string;
|
|
106
99
|
};
|
|
107
100
|
|
|
@@ -111,7 +104,7 @@ export interface ResourceComposePageProps {
|
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
export interface SaveResourceParams {
|
|
114
|
-
mode: 'new' | 'clone'
|
|
107
|
+
mode: 'new' | 'clone';
|
|
115
108
|
name: string;
|
|
116
109
|
storageUri: string;
|
|
117
110
|
content?: string;
|
|
@@ -121,15 +114,11 @@ export interface SaveResourceParams {
|
|
|
121
114
|
entityTypes?: string[];
|
|
122
115
|
language: string;
|
|
123
116
|
archiveOriginal?: boolean;
|
|
124
|
-
annotationUri?: string;
|
|
125
|
-
sourceDocumentId?: string;
|
|
126
117
|
}
|
|
127
118
|
|
|
128
119
|
export function ResourceComposePage({
|
|
129
120
|
mode,
|
|
130
121
|
cloneData,
|
|
131
|
-
referenceData,
|
|
132
|
-
gatheredContext,
|
|
133
122
|
availableEntityTypes,
|
|
134
123
|
initialLocale,
|
|
135
124
|
theme,
|
|
@@ -184,13 +173,8 @@ export function ResourceComposePage({
|
|
|
184
173
|
if (mode === 'clone' && cloneData) {
|
|
185
174
|
setNewResourceName(cloneData.sourceResource.name);
|
|
186
175
|
setNewResourceContent(cloneData.sourceContent);
|
|
187
|
-
} else if (mode === 'reference' && referenceData) {
|
|
188
|
-
setNewResourceName(referenceData.name);
|
|
189
|
-
if (referenceData.entityTypes.length > 0) {
|
|
190
|
-
setSelectedEntityTypes(referenceData.entityTypes);
|
|
191
|
-
}
|
|
192
176
|
}
|
|
193
|
-
}, [mode, cloneData
|
|
177
|
+
}, [mode, cloneData]);
|
|
194
178
|
|
|
195
179
|
// Handle file upload
|
|
196
180
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -266,20 +250,12 @@ export function ResourceComposePage({
|
|
|
266
250
|
if (mode === 'clone') {
|
|
267
251
|
params.archiveOriginal = archiveOriginal;
|
|
268
252
|
}
|
|
269
|
-
if (referenceData?.annotationUri) {
|
|
270
|
-
params.annotationUri = referenceData.annotationUri;
|
|
271
|
-
}
|
|
272
|
-
if (referenceData?.sourceDocumentId) {
|
|
273
|
-
params.sourceDocumentId = referenceData.sourceDocumentId;
|
|
274
|
-
}
|
|
275
253
|
|
|
276
254
|
await onSaveResource(params);
|
|
277
255
|
|
|
278
256
|
// Announce success based on mode
|
|
279
257
|
const successMessage = mode === 'clone'
|
|
280
258
|
? 'Resource cloned successfully'
|
|
281
|
-
: mode === 'reference'
|
|
282
|
-
? 'Reference completed successfully'
|
|
283
259
|
: 'Resource created successfully';
|
|
284
260
|
announceFormSuccess(successMessage);
|
|
285
261
|
} catch (error) {
|
|
@@ -291,13 +267,6 @@ export function ResourceComposePage({
|
|
|
291
267
|
};
|
|
292
268
|
|
|
293
269
|
const isClone = mode === 'clone';
|
|
294
|
-
const isReferenceCompletion = mode === 'reference';
|
|
295
|
-
|
|
296
|
-
// Gathered-context panel — annotation-focus only for now.
|
|
297
|
-
const composeFocus = gatheredContext?.focus.kind === 'annotation' ? gatheredContext.focus : null;
|
|
298
|
-
const composeConnections = gatheredContext && composeFocus
|
|
299
|
-
? deriveViews(gatheredContext.graph, String(composeFocus.sourceResource.id)).connections
|
|
300
|
-
: [];
|
|
301
270
|
|
|
302
271
|
return (
|
|
303
272
|
<div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
|
|
@@ -306,73 +275,15 @@ export function ResourceComposePage({
|
|
|
306
275
|
{/* Page Title */}
|
|
307
276
|
<div className="semiont-page__header">
|
|
308
277
|
<h1 className="semiont-page__title">
|
|
309
|
-
{isClone ? t.titleEditClone :
|
|
278
|
+
{isClone ? t.titleEditClone : t.title}
|
|
310
279
|
</h1>
|
|
311
|
-
{
|
|
280
|
+
{isClone && (
|
|
312
281
|
<p className="semiont-page__subtitle">
|
|
313
|
-
{
|
|
282
|
+
{t.subtitleClone}
|
|
314
283
|
</p>
|
|
315
284
|
)}
|
|
316
|
-
{isReferenceCompletion && (
|
|
317
|
-
<div className="semiont-page__reference-notice">
|
|
318
|
-
<p className="semiont-page__reference-notice-text">
|
|
319
|
-
{t.linkedNoticePrefix}
|
|
320
|
-
</p>
|
|
321
|
-
</div>
|
|
322
|
-
)}
|
|
323
285
|
</div>
|
|
324
286
|
|
|
325
|
-
{/* Gathered Context Panel (from wizard) */}
|
|
326
|
-
{gatheredContext && (
|
|
327
|
-
<div className="semiont-form__field" style={{
|
|
328
|
-
marginBottom: '1.5rem',
|
|
329
|
-
padding: '1rem',
|
|
330
|
-
backgroundColor: 'var(--semiont-bg-secondary)',
|
|
331
|
-
borderRadius: 'var(--semiont-radius-md)',
|
|
332
|
-
border: '1px solid var(--semiont-border-primary)',
|
|
333
|
-
}}>
|
|
334
|
-
<h3 className="semiont-form__label" style={{ marginBottom: '0.75rem' }}>Gathered Context</h3>
|
|
335
|
-
{composeFocus?.selected && (
|
|
336
|
-
<div style={{
|
|
337
|
-
padding: '0.75rem',
|
|
338
|
-
backgroundColor: 'var(--semiont-bg-primary)',
|
|
339
|
-
borderRadius: 'var(--semiont-radius-md)',
|
|
340
|
-
border: '1px solid var(--semiont-border-primary)',
|
|
341
|
-
maxHeight: '150px',
|
|
342
|
-
overflowY: 'auto',
|
|
343
|
-
marginBottom: '0.5rem',
|
|
344
|
-
}}>
|
|
345
|
-
<div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
|
|
346
|
-
{composeFocus.selected.before && <span>{composeFocus.selected.before}</span>}
|
|
347
|
-
<span style={{
|
|
348
|
-
backgroundColor: 'var(--semiont-color-primary-100)',
|
|
349
|
-
padding: '0 0.25rem',
|
|
350
|
-
fontWeight: 600,
|
|
351
|
-
color: 'var(--semiont-color-primary-900)',
|
|
352
|
-
}}>
|
|
353
|
-
{composeFocus.selected.text}
|
|
354
|
-
</span>
|
|
355
|
-
{composeFocus.selected.after && <span>{composeFocus.selected.after}</span>}
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
)}
|
|
359
|
-
{composeConnections.length > 0 && (
|
|
360
|
-
<div style={{
|
|
361
|
-
padding: '0.5rem 0',
|
|
362
|
-
fontSize: 'var(--semiont-text-sm)',
|
|
363
|
-
color: 'var(--semiont-text-secondary)',
|
|
364
|
-
}}>
|
|
365
|
-
<span style={{ fontSize: 'var(--semiont-text-xs)', fontWeight: 500, marginRight: '0.5rem' }}>Connections:</span>
|
|
366
|
-
{composeConnections.map(conn => (
|
|
367
|
-
<span key={conn.resourceId} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.5rem', marginRight: '0.25rem' }}>
|
|
368
|
-
{conn.resourceName}
|
|
369
|
-
</span>
|
|
370
|
-
))}
|
|
371
|
-
</div>
|
|
372
|
-
)}
|
|
373
|
-
</div>
|
|
374
|
-
)}
|
|
375
|
-
|
|
376
287
|
{/* Create Form */}
|
|
377
288
|
<div className="semiont-form">
|
|
378
289
|
<form onSubmit={handleSaveResource} className="semiont-form__fields semiont-compose-grid">
|
|
@@ -416,7 +327,7 @@ export function ResourceComposePage({
|
|
|
416
327
|
</div>
|
|
417
328
|
|
|
418
329
|
{/* Entity Types Selection */}
|
|
419
|
-
{(
|
|
330
|
+
{(
|
|
420
331
|
<div className="semiont-form__field semiont-form__entity-types">
|
|
421
332
|
<div className="semiont-form__label">
|
|
422
333
|
{t.entityTypes}
|
|
@@ -462,30 +373,6 @@ export function ResourceComposePage({
|
|
|
462
373
|
</div>
|
|
463
374
|
)}
|
|
464
375
|
|
|
465
|
-
{/* Entity Types Display for reference completion */}
|
|
466
|
-
{isReferenceCompletion && selectedEntityTypes.length > 0 && (
|
|
467
|
-
<div className="semiont-form__field semiont-form__entity-types-display" role="region" aria-labelledby="selected-entity-types-label">
|
|
468
|
-
<h3 id="selected-entity-types-label" className="semiont-form__label">
|
|
469
|
-
{t.entityTypes}
|
|
470
|
-
</h3>
|
|
471
|
-
<div className="semiont-form__entity-type-tags" role="list">
|
|
472
|
-
{selectedEntityTypes.map((type) => (
|
|
473
|
-
<span
|
|
474
|
-
key={type}
|
|
475
|
-
role="listitem"
|
|
476
|
-
className="semiont-form__entity-type-tag"
|
|
477
|
-
aria-label={`Entity type: ${type}`}
|
|
478
|
-
>
|
|
479
|
-
{type}
|
|
480
|
-
</span>
|
|
481
|
-
))}
|
|
482
|
-
</div>
|
|
483
|
-
<p className="semiont-form__helper-text" id="reference-entity-types-description">
|
|
484
|
-
These entity types were selected when creating the reference
|
|
485
|
-
</p>
|
|
486
|
-
</div>
|
|
487
|
-
)}
|
|
488
|
-
|
|
489
376
|
{/* Language Selector */}
|
|
490
377
|
<div className="semiont-form__field">
|
|
491
378
|
<label htmlFor="language-select" className="semiont-form__label">
|
|
@@ -512,7 +399,7 @@ export function ResourceComposePage({
|
|
|
512
399
|
<div className="semiont-compose-grid__content">
|
|
513
400
|
|
|
514
401
|
{/* Content Source Toggle - only show for new resources */}
|
|
515
|
-
{!isClone &&
|
|
402
|
+
{!isClone && (
|
|
516
403
|
<div className="semiont-form__field">
|
|
517
404
|
<label className="semiont-form__label">
|
|
518
405
|
{t.contentSource}
|
|
@@ -554,7 +441,7 @@ export function ResourceComposePage({
|
|
|
554
441
|
)}
|
|
555
442
|
|
|
556
443
|
{/* Upload File Section */}
|
|
557
|
-
{!isClone &&
|
|
444
|
+
{!isClone && inputMethod === 'upload' && (
|
|
558
445
|
<div className="semiont-form__upload-section">
|
|
559
446
|
<div>
|
|
560
447
|
<div className="semiont-form__upload-container">
|
|
@@ -631,10 +518,10 @@ export function ResourceComposePage({
|
|
|
631
518
|
)}
|
|
632
519
|
|
|
633
520
|
{/* Write Content Section */}
|
|
634
|
-
{(isClone ||
|
|
521
|
+
{(isClone || inputMethod === 'write') && (
|
|
635
522
|
<div className="semiont-form__write-section">
|
|
636
523
|
{/* Format Selector - only for manual entry, not clones */}
|
|
637
|
-
{!isClone &&
|
|
524
|
+
{!isClone && (
|
|
638
525
|
<div className="semiont-form__field">
|
|
639
526
|
<label htmlFor="format-select" className="semiont-form__label">
|
|
640
527
|
{t.format}
|
|
@@ -673,7 +560,7 @@ export function ResourceComposePage({
|
|
|
673
560
|
</div>
|
|
674
561
|
|
|
675
562
|
{/* Encoding Selector - only for manual text entry */}
|
|
676
|
-
{!isClone &&
|
|
563
|
+
{!isClone && (
|
|
677
564
|
<div className="semiont-form__field">
|
|
678
565
|
<label htmlFor="charset-select" className="semiont-form__label">
|
|
679
566
|
{t.encoding}
|
|
@@ -731,8 +618,8 @@ export function ResourceComposePage({
|
|
|
731
618
|
className={buttonStyles.primary.base}
|
|
732
619
|
>
|
|
733
620
|
{isCreating
|
|
734
|
-
? (isClone ? t.saving :
|
|
735
|
-
: (isClone ? t.saveClonedResource :
|
|
621
|
+
? (isClone ? t.saving : t.creating)
|
|
622
|
+
: (isClone ? t.saveClonedResource : t.createResource)}
|
|
736
623
|
</button>
|
|
737
624
|
</div>
|
|
738
625
|
|
|
@@ -64,80 +64,6 @@ describe('createComposePageStateUnit', () => {
|
|
|
64
64
|
stateUnit.dispose();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it('detects reference mode from params', async () => {
|
|
68
|
-
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
69
|
-
annotationUri: 'ann-1',
|
|
70
|
-
sourceDocumentId: 'doc-1',
|
|
71
|
-
name: 'Reference Doc',
|
|
72
|
-
entityTypes: 'Person,Place',
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const mode = await firstValueFrom(stateUnit.mode$);
|
|
76
|
-
expect(mode).toBe('reference');
|
|
77
|
-
|
|
78
|
-
const ref = await firstValueFrom(stateUnit.referenceData$.pipe(filter((r) => r !== null)));
|
|
79
|
-
expect(ref!.annotationUri).toBe('ann-1');
|
|
80
|
-
expect(ref!.entityTypes).toEqual(['Person', 'Place']);
|
|
81
|
-
|
|
82
|
-
stateUnit.dispose();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('parses storedContext in reference mode', async () => {
|
|
86
|
-
// A real `GatheredContext` shape. The old fixture here was
|
|
87
|
-
// `{ annotation, sourceContext }` — not a context at all — and it round-tripped
|
|
88
|
-
// because nothing checked. That is the hole the guard closes.
|
|
89
|
-
const context = {
|
|
90
|
-
focus: { kind: 'annotation', annotation: { id: 'ann-1' }, sourceResource: { id: 'doc-1' } },
|
|
91
|
-
graph: { nodes: [], edges: [] },
|
|
92
|
-
metadata: {},
|
|
93
|
-
};
|
|
94
|
-
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
95
|
-
annotationUri: 'ann-1',
|
|
96
|
-
sourceDocumentId: 'doc-1',
|
|
97
|
-
name: 'Ref',
|
|
98
|
-
storedContext: JSON.stringify(context),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const gathered = await firstValueFrom(stateUnit.gatheredContext$.pipe(filter((g) => g !== null)));
|
|
102
|
-
expect(gathered).toEqual(context);
|
|
103
|
-
|
|
104
|
-
stateUnit.dispose();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('ignores a stash that parses but is not a GatheredContext', async () => {
|
|
108
|
-
// The realistic case: an entry written by an older build, read back after a
|
|
109
|
-
// deploy. It used to sail through and reach `deriveViews`, which maps
|
|
110
|
-
// `graph.nodes` during render and throws instead of degrading.
|
|
111
|
-
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
112
|
-
annotationUri: 'ann-1',
|
|
113
|
-
sourceDocumentId: 'doc-1',
|
|
114
|
-
name: 'Ref',
|
|
115
|
-
storedContext: JSON.stringify({ annotation: { id: 'ann-1' }, sourceContext: 'text' }),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Treated as NO stash: the ordinary gather path owns the outcome from here.
|
|
119
|
-
expect(await firstValueFrom(stateUnit.gatheredContext$)).toBeNull();
|
|
120
|
-
|
|
121
|
-
stateUnit.dispose();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('ignores malformed storedContext', async () => {
|
|
125
|
-
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
126
|
-
annotationUri: 'ann-1',
|
|
127
|
-
sourceDocumentId: 'doc-1',
|
|
128
|
-
name: 'Ref',
|
|
129
|
-
storedContext: 'not-json{{{',
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const loading = await firstValueFrom(stateUnit.loading$.pipe(filter((l) => !l)));
|
|
133
|
-
expect(loading).toBe(false);
|
|
134
|
-
|
|
135
|
-
const gathered = await firstValueFrom(stateUnit.gatheredContext$);
|
|
136
|
-
expect(gathered).toBeNull();
|
|
137
|
-
|
|
138
|
-
stateUnit.dispose();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
67
|
it('exposes entity types', async () => {
|
|
142
68
|
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
|
|
143
69
|
|
|
@@ -166,31 +92,6 @@ describe('createComposePageStateUnit', () => {
|
|
|
166
92
|
stateUnit.dispose();
|
|
167
93
|
});
|
|
168
94
|
|
|
169
|
-
it('save in reference mode calls yield.resource then bind.body', async () => {
|
|
170
|
-
const resource = mockUpload('new-4');
|
|
171
|
-
const body = vi.fn().mockResolvedValue(undefined);
|
|
172
|
-
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource, body })), mockBrowse(), {
|
|
173
|
-
annotationUri: 'ann-1',
|
|
174
|
-
sourceDocumentId: 'doc-1',
|
|
175
|
-
name: 'Ref',
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const id = await stateUnit.save({
|
|
179
|
-
mode: 'reference',
|
|
180
|
-
name: 'Ref Doc',
|
|
181
|
-
storageUri: '/docs/ref.md',
|
|
182
|
-
content: 'content',
|
|
183
|
-
language: 'en',
|
|
184
|
-
annotationUri: 'ann-1',
|
|
185
|
-
sourceDocumentId: 'doc-1',
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(id).toBe('new-4');
|
|
189
|
-
expect(body).toHaveBeenCalledOnce();
|
|
190
|
-
|
|
191
|
-
stateUnit.dispose();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
95
|
it('save in clone mode calls yield.createFromToken', async () => {
|
|
195
96
|
const createFromToken = vi.fn().mockResolvedValue({ resourceId: 'cloned-1' });
|
|
196
97
|
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ createFromToken })), mockBrowse(), {
|
|
@@ -220,7 +121,7 @@ describe('ComposePageStateUnit — StateUnit axioms', () => {
|
|
|
220
121
|
const browse = disposeProbe();
|
|
221
122
|
return { unit: createComposePageStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
|
|
222
123
|
},
|
|
223
|
-
surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.
|
|
124
|
+
surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.uploadProgress$],
|
|
224
125
|
});
|
|
225
126
|
});
|
|
226
127
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
|
|
3
|
+
import { resourceId as makeResourceId } from '@semiont/core';
|
|
4
4
|
import { createDisposer } from '@semiont/sdk';
|
|
5
5
|
import type { StateUnit } from '@semiont/core';
|
|
6
6
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
@@ -9,16 +9,11 @@ import type { SemiontSession } from '@semiont/sdk';
|
|
|
9
9
|
import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
|
|
10
10
|
import type { UploadProgress } from '@semiont/sdk';
|
|
11
11
|
|
|
12
|
-
export type ComposeMode = 'new' | 'clone'
|
|
12
|
+
export type ComposeMode = 'new' | 'clone';
|
|
13
13
|
|
|
14
14
|
export interface ComposeParams {
|
|
15
15
|
mode?: string | undefined;
|
|
16
16
|
token?: string | undefined;
|
|
17
|
-
annotationUri?: string | undefined;
|
|
18
|
-
sourceDocumentId?: string | undefined;
|
|
19
|
-
name?: string | undefined;
|
|
20
|
-
entityTypes?: string | undefined;
|
|
21
|
-
storedContext?: string | undefined;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
export interface CloneData {
|
|
@@ -26,13 +21,6 @@ export interface CloneData {
|
|
|
26
21
|
sourceContent: string;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
export interface ReferenceData {
|
|
30
|
-
annotationUri: string;
|
|
31
|
-
sourceDocumentId: string;
|
|
32
|
-
name: string;
|
|
33
|
-
entityTypes: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
24
|
export interface SaveResourceParams {
|
|
37
25
|
mode: ComposeMode;
|
|
38
26
|
name: string;
|
|
@@ -44,8 +32,6 @@ export interface SaveResourceParams {
|
|
|
44
32
|
entityTypes?: string[];
|
|
45
33
|
language: string;
|
|
46
34
|
archiveOriginal?: boolean;
|
|
47
|
-
annotationUri?: string;
|
|
48
|
-
sourceDocumentId?: string;
|
|
49
35
|
}
|
|
50
36
|
|
|
51
37
|
export interface ComposePageStateUnit extends StateUnit {
|
|
@@ -53,8 +39,6 @@ export interface ComposePageStateUnit extends StateUnit {
|
|
|
53
39
|
mode$: Observable<ComposeMode>;
|
|
54
40
|
loading$: Observable<boolean>;
|
|
55
41
|
cloneData$: Observable<CloneData | null>;
|
|
56
|
-
referenceData$: Observable<ReferenceData | null>;
|
|
57
|
-
gatheredContext$: Observable<GatheredContext | null>;
|
|
58
42
|
entityTypes: ListState<string[]>;
|
|
59
43
|
/**
|
|
60
44
|
* Live upload-progress for the in-flight `save(...)` call. Emits the
|
|
@@ -78,43 +62,19 @@ export function createComposePageStateUnit(
|
|
|
78
62
|
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
79
63
|
// shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
|
|
80
64
|
|
|
81
|
-
const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
|
|
82
65
|
const isCloneMode = params.mode === 'clone' && Boolean(params.token);
|
|
83
|
-
const pageMode: ComposeMode = isCloneMode ? 'clone' :
|
|
66
|
+
const pageMode: ComposeMode = isCloneMode ? 'clone' : 'new';
|
|
84
67
|
|
|
85
68
|
const mode$ = new BehaviorSubject<ComposeMode>(pageMode);
|
|
86
69
|
const loading$ = new BehaviorSubject<boolean>(true);
|
|
87
70
|
const cloneData$ = new BehaviorSubject<CloneData | null>(null);
|
|
88
|
-
const referenceData$ = new BehaviorSubject<ReferenceData | null>(null);
|
|
89
|
-
const gatheredContext$ = new BehaviorSubject<GatheredContext | null>(null);
|
|
90
71
|
const uploadProgress$ = new BehaviorSubject<UploadProgress | null>(null);
|
|
91
72
|
|
|
92
73
|
const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
|
|
93
74
|
disposer.add(entityTypes.dispose);
|
|
94
75
|
|
|
95
76
|
// Initialize based on mode
|
|
96
|
-
if (
|
|
97
|
-
const entityTypes = params.entityTypes ? params.entityTypes.split(',') : [];
|
|
98
|
-
referenceData$.next({
|
|
99
|
-
annotationUri: params.annotationUri!,
|
|
100
|
-
sourceDocumentId: params.sourceDocumentId!,
|
|
101
|
-
name: params.name!,
|
|
102
|
-
entityTypes,
|
|
103
|
-
});
|
|
104
|
-
if (params.storedContext) {
|
|
105
|
-
// The stash is the one place a `GatheredContext` arrives with its type
|
|
106
|
-
// history severed (sessionStorage → JSON.parse), so it is checked here and
|
|
107
|
-
// trusted everywhere after. A stale entry — a tab held open across a deploy
|
|
108
|
-
// — used to reach `deriveViews`, which maps `graph.nodes` during render and
|
|
109
|
-
// throws rather than degrading. A failed guard is treated as NO stash: the
|
|
110
|
-
// ordinary gather path runs, with the loading and error states it already has.
|
|
111
|
-
try {
|
|
112
|
-
const parsed: unknown = JSON.parse(params.storedContext);
|
|
113
|
-
if (isGatheredContext(parsed)) gatheredContext$.next(parsed);
|
|
114
|
-
} catch { /* ignore malformed */ }
|
|
115
|
-
}
|
|
116
|
-
loading$.next(false);
|
|
117
|
-
} else if (isCloneMode) {
|
|
77
|
+
if (isCloneMode) {
|
|
118
78
|
void (async () => {
|
|
119
79
|
try {
|
|
120
80
|
const tokenResult = await client.yield.fromToken(params.token!);
|
|
@@ -184,14 +144,6 @@ export function createComposePageStateUnit(
|
|
|
184
144
|
});
|
|
185
145
|
});
|
|
186
146
|
|
|
187
|
-
if (saveParams.mode === 'reference' && saveParams.annotationUri && saveParams.sourceDocumentId) {
|
|
188
|
-
await client.bind.body(
|
|
189
|
-
makeResourceId(saveParams.sourceDocumentId),
|
|
190
|
-
makeAnnotationId(saveParams.annotationUri) as AnnotationId,
|
|
191
|
-
[{ op: 'add', item: { type: 'SpecificResource' as const, source: newResourceId, purpose: 'linking' as const } }],
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
147
|
return newResourceId;
|
|
196
148
|
};
|
|
197
149
|
|
|
@@ -200,8 +152,6 @@ export function createComposePageStateUnit(
|
|
|
200
152
|
mode$: mode$.asObservable(),
|
|
201
153
|
loading$: loading$.asObservable(),
|
|
202
154
|
cloneData$: cloneData$.asObservable(),
|
|
203
|
-
referenceData$: referenceData$.asObservable(),
|
|
204
|
-
gatheredContext$: gatheredContext$.asObservable(),
|
|
205
155
|
entityTypes: entityTypes.state,
|
|
206
156
|
uploadProgress$: uploadProgress$.asObservable(),
|
|
207
157
|
save,
|
|
@@ -209,8 +159,6 @@ export function createComposePageStateUnit(
|
|
|
209
159
|
mode$.complete();
|
|
210
160
|
loading$.complete();
|
|
211
161
|
cloneData$.complete();
|
|
212
|
-
referenceData$.complete();
|
|
213
|
-
gatheredContext$.complete();
|
|
214
162
|
uploadProgress$.complete();
|
|
215
163
|
disposer.dispose();
|
|
216
164
|
},
|
|
@@ -14,7 +14,8 @@ import { ToastProvider } from '../../../components/Toast';
|
|
|
14
14
|
import { ThemeProvider } from '../../../contexts/ThemeContext';
|
|
15
15
|
import { LineNumbersProvider } from '../../../contexts/LineNumbersContext';
|
|
16
16
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
17
|
-
import type { ResourceId } from '@semiont/core';
|
|
17
|
+
import type { BodyOperation, EventMap, ResourceId, UserId } from '@semiont/core';
|
|
18
|
+
import { annotationId as makeAnnotationId, resourceId as makeResourceId } from '@semiont/core';
|
|
18
19
|
|
|
19
20
|
// jsdom doesn't implement window.matchMedia — mock it for useTheme
|
|
20
21
|
Object.defineProperty(window, 'matchMedia', {
|
|
@@ -102,24 +103,22 @@ useDebouncedCallback: (fn: any) => fn,
|
|
|
102
103
|
announceResourceLoaded: vi.fn(),
|
|
103
104
|
}),
|
|
104
105
|
useEventSubscriptions: vi.fn(),
|
|
105
|
-
useResourceAnnotations: () =>
|
|
106
|
-
clearNewAnnotationId: vi.fn(),
|
|
107
|
-
newAnnotationIds: new Set(),
|
|
108
|
-
markAnnotation: vi.fn(),
|
|
109
|
-
deleteAnnotation: vi.fn(),
|
|
110
|
-
triggerSparkleAnimation: vi.fn(),
|
|
111
|
-
}),
|
|
106
|
+
useResourceAnnotations: () => sparkleContext,
|
|
112
107
|
};
|
|
113
108
|
});
|
|
114
109
|
|
|
110
|
+
// Shared, assertable spies — the page's sparkle wiring is pinned against
|
|
111
|
+
// these (a per-call `vi.fn()` object would be unobservable from the tests).
|
|
112
|
+
const sparkleContext = vi.hoisted(() => ({
|
|
113
|
+
clearSparkle: vi.fn(),
|
|
114
|
+
sparkleAnnotationIds: new Set<string>(),
|
|
115
|
+
markAnnotation: vi.fn(),
|
|
116
|
+
deleteAnnotation: vi.fn(),
|
|
117
|
+
triggerSparkleAnimation: vi.fn(),
|
|
118
|
+
}));
|
|
119
|
+
|
|
115
120
|
vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
|
|
116
|
-
useResourceAnnotations: () =>
|
|
117
|
-
clearNewAnnotationId: vi.fn(),
|
|
118
|
-
newAnnotationIds: new Set(),
|
|
119
|
-
markAnnotation: vi.fn(),
|
|
120
|
-
deleteAnnotation: vi.fn(),
|
|
121
|
-
triggerSparkleAnimation: vi.fn(),
|
|
122
|
-
}),
|
|
121
|
+
useResourceAnnotations: () => sparkleContext,
|
|
123
122
|
ResourceAnnotationsProvider: ({ children }: any) => children,
|
|
124
123
|
}));
|
|
125
124
|
|
|
@@ -206,6 +205,63 @@ describe('ResourceViewerPage — outcome toasts reach the user', () => {
|
|
|
206
205
|
});
|
|
207
206
|
});
|
|
208
207
|
|
|
208
|
+
// RESOLUTION-SPARKLE D2: a reference resolving — from ANY strategy, local or
|
|
209
|
+
// remote — arrives as mark:body-updated with a linking-add operation, and the
|
|
210
|
+
// page turns exactly that into a sparkle. The harness mocks
|
|
211
|
+
// useEventSubscriptions, so the pin drives the captured handler directly.
|
|
212
|
+
describe('ResourceViewerPage — resolution sparkles', () => {
|
|
213
|
+
const bodyUpdated = (operations: BodyOperation[]): EventMap['mark:body-updated'] => ({
|
|
214
|
+
id: 'evt-1',
|
|
215
|
+
timestamp: '2026-08-21T12:00:00Z',
|
|
216
|
+
resourceId: makeResourceId('test-123'),
|
|
217
|
+
userId: 'did:web:example.com:users:alice' as UserId,
|
|
218
|
+
version: 1,
|
|
219
|
+
type: 'mark:body-updated',
|
|
220
|
+
payload: { annotationId: makeAnnotationId('ann-7'), operations },
|
|
221
|
+
metadata: { sequenceNumber: 1 },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const renderAndGetHandler = () => {
|
|
225
|
+
mockUseEventSubscriptions.mockClear();
|
|
226
|
+
sparkleContext.triggerSparkleAnimation.mockClear();
|
|
227
|
+
renderWithProviders(<ResourceViewerPage {...createMockProps()} />);
|
|
228
|
+
const map = mockUseEventSubscriptions.mock.calls.at(-1)?.[0] as
|
|
229
|
+
Record<string, (event: EventMap['mark:body-updated']) => void>;
|
|
230
|
+
return map['mark:body-updated'];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
it('an operation adding a linking SpecificResource sparkles the annotation', () => {
|
|
234
|
+
const handler = renderAndGetHandler();
|
|
235
|
+
expect(handler).toBeDefined();
|
|
236
|
+
|
|
237
|
+
handler!(bodyUpdated([
|
|
238
|
+
{ op: 'add', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
|
|
239
|
+
]));
|
|
240
|
+
|
|
241
|
+
expect(sparkleContext.triggerSparkleAnimation).toHaveBeenCalledWith('ann-7');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('unlink (remove-only operations) stays dark — A2', () => {
|
|
245
|
+
const handler = renderAndGetHandler();
|
|
246
|
+
|
|
247
|
+
handler?.(bodyUpdated([
|
|
248
|
+
{ op: 'remove', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
|
|
249
|
+
]));
|
|
250
|
+
|
|
251
|
+
expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('a TextualBody add stays dark', () => {
|
|
255
|
+
const handler = renderAndGetHandler();
|
|
256
|
+
|
|
257
|
+
handler?.(bodyUpdated([
|
|
258
|
+
{ op: 'add', item: { type: 'TextualBody', value: 'Person', purpose: 'tagging' } },
|
|
259
|
+
]));
|
|
260
|
+
|
|
261
|
+
expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
209
265
|
describe('ResourceViewerPage', () => {
|
|
210
266
|
beforeEach(() => {
|
|
211
267
|
});
|