graphwise-reactodia 0.0.1-TR6 → 0.0.1-TR8

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 (43) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/graphwise-reactodia.cjs.entry.js +281 -27
  3. package/dist/cjs/index.cjs.js +7 -0
  4. package/dist/collection/components/graphwise-reactodia/graphwise-reactodia.js +11 -11
  5. package/dist/collection/components/graphwise-reactodia/models/clear-diagram-storage-event.js +5 -0
  6. package/dist/collection/components/graphwise-reactodia/models/subscription-list.js +35 -0
  7. package/dist/collection/components/graphwise-reactodia/models/subscription.js +1 -0
  8. package/dist/collection/components/graphwise-reactodia/models/triple.js +1 -0
  9. package/dist/collection/components/graphwise-reactodia/providers/service/service-inject.js +15 -0
  10. package/dist/collection/components/graphwise-reactodia/providers/service/service.js +1 -0
  11. package/dist/collection/components/graphwise-reactodia/providers/service/service.provider.js +20 -0
  12. package/dist/collection/components/graphwise-reactodia/reactodia-app.js +85 -13
  13. package/dist/collection/components/graphwise-reactodia/services/diagram/diagram.service.js +39 -0
  14. package/dist/collection/components/graphwise-reactodia/services/diagram-storage/diagram-storage.service.js +42 -0
  15. package/dist/collection/components/graphwise-reactodia/services/event/event.service.js +22 -0
  16. package/dist/collection/components/graphwise-reactodia/services/window/window.service.js +8 -0
  17. package/dist/collection/components/graphwise-reactodia/testing/storage.mock.js +35 -0
  18. package/dist/collection/index.js +1 -1
  19. package/dist/collection/pages/graphwise-reactodia/main.js +29 -4
  20. package/dist/esm/graphwise-reactodia.entry.js +278 -24
  21. package/dist/esm/index.js +6 -0
  22. package/dist/graphwise-reactodia/graphwise-reactodia.esm.js +1 -1
  23. package/dist/graphwise-reactodia/index.esm.js +1 -0
  24. package/dist/graphwise-reactodia/p-80cb6eef.entry.js +1 -0
  25. package/dist/types/components/graphwise-reactodia/graphwise-reactodia.d.ts +1 -1
  26. package/dist/types/components/graphwise-reactodia/i18n/translations.d.ts +2 -6
  27. package/dist/types/components/graphwise-reactodia/models/clear-diagram-storage-event.d.ts +5 -0
  28. package/dist/types/components/graphwise-reactodia/models/reactodia-app-props.d.ts +6 -7
  29. package/dist/types/components/graphwise-reactodia/models/reactodia-config.d.ts +10 -2
  30. package/dist/types/components/graphwise-reactodia/models/subscription-list.d.ts +26 -0
  31. package/dist/types/components/graphwise-reactodia/models/subscription.d.ts +4 -0
  32. package/dist/types/components/graphwise-reactodia/models/triple.d.ts +19 -0
  33. package/dist/types/components/graphwise-reactodia/providers/service/service-inject.d.ts +13 -0
  34. package/dist/types/components/graphwise-reactodia/providers/service/service.d.ts +6 -0
  35. package/dist/types/components/graphwise-reactodia/providers/service/service.provider.d.ts +18 -0
  36. package/dist/types/components/graphwise-reactodia/services/diagram/diagram.service.d.ts +28 -0
  37. package/dist/types/components/graphwise-reactodia/services/diagram-storage/diagram-storage.service.d.ts +26 -0
  38. package/dist/types/components/graphwise-reactodia/services/event/event.service.d.ts +14 -0
  39. package/dist/types/components/graphwise-reactodia/services/window/window.service.d.ts +7 -0
  40. package/dist/types/components/graphwise-reactodia/testing/storage.mock.d.ts +18 -0
  41. package/dist/types/index.d.ts +1 -1
  42. package/package.json +9 -3
  43. package/dist/graphwise-reactodia/p-5a330bae.entry.js +0 -1
@@ -1,8 +1,13 @@
1
1
  import { createElement } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
- import { DefaultWorkspace, SparqlDataProvider, useLoadedWorkspace, Workspace, } from "@reactodia/workspace";
3
+ import { DefaultWorkspace, SparqlDataProvider, useLoadedWorkspace, Workspace } from "@reactodia/workspace";
4
4
  import { blockingDefaultLayout } from "@reactodia/workspace/layout-sync";
5
5
  import { TRANSLATIONS } from "./i18n/translations";
6
+ import { service } from "./providers/service/service-inject";
7
+ import { DiagramService } from "./services/diagram/diagram.service";
8
+ import { DiagramStorageService } from "./services/diagram-storage/diagram-storage.service";
9
+ import { EventService } from "./services/event/event.service";
10
+ import { SubscriptionList } from "./models/subscription-list";
6
11
  /**
7
12
  * Reactodia ships English as its built-in default bundle, so English needs no override.
8
13
  * Other languages are layered on top as partial bundles; unknown codes fall back to English.
@@ -20,6 +25,12 @@ function translationsForLanguage(language) {
20
25
  * runs outside React and therefore cannot call the `useWorkspace` hook.
21
26
  */
22
27
  let workspaceContext = null;
28
+ /**
29
+ * Active listeners that persist diagram edits to local storage. Populated when the workspace
30
+ * mounts and released in {@link unmountReactodia}, so persistence follows the same lifecycle as
31
+ * the React root.
32
+ */
33
+ const subscriptions = new SubscriptionList();
23
34
  /**
24
35
  * Builds a Reactodia {@link SparqlDataProvider} for the given endpoint using the supplied
25
36
  * query preset. The host owns the query configuration and passes it in via props; when none
@@ -30,21 +41,71 @@ function createDataProvider(props) {
30
41
  return new SparqlDataProvider({
31
42
  endpointUrl: currentRepository,
32
43
  queryMethod: 'POST',
33
- queryFunction: config.queryFunction,
44
+ queryFunction: config.queryFunction
34
45
  }, providerSettings);
35
46
  }
36
47
  /**
37
48
  * Places the seed entities on the canvas. Each IRI is added as a placeholder element, and data is then loaded for the
38
49
  * respective nodes using the {@link DataDiagramModel.requestData} method.
39
50
  */
40
- async function seedCanvas(context, seed, signal) {
51
+ async function seedIrisToCanvas(context, dataProvider, seed, signal) {
41
52
  const { model, performLayout } = context;
53
+ await model.createNewDiagram({ dataProvider, signal });
42
54
  for (const iri of seed) {
43
55
  model.createElement(iri);
44
56
  }
45
57
  await model.requestData();
46
58
  await performLayout({ signal });
47
59
  }
60
+ /**
61
+ * Places a pre-resolved graph (e.g. a CONSTRUCT query result) on the canvas. The difference between this and
62
+ * {@link seedIrisToCanvas} is that this method does not request link data from the SPARQL endpoint, since describe/construct
63
+ * queries are not persisted in the DB. Here we expect provided links and query for element data only
64
+ */
65
+ async function seedGraphToCanvas(context, dataProvider, links, signal) {
66
+ const { model, performLayout } = context;
67
+ await model.createNewDiagram({ dataProvider, signal });
68
+ const uniqueIris = new Set();
69
+ links.forEach((link) => {
70
+ var _a;
71
+ uniqueIris.add(link.source);
72
+ uniqueIris.add(link.target);
73
+ // this is a createOrGet, regardless of the name, so we don't need to worry about duplicates.
74
+ model.createElement(link.source);
75
+ model.createElement(link.target);
76
+ (_a = link.rawPredicates) === null || _a === void 0 ? void 0 : _a.forEach((predicate) => {
77
+ model.createLinks({
78
+ linkTypeId: predicate,
79
+ sourceId: link.source,
80
+ targetId: link.target,
81
+ properties: {}
82
+ });
83
+ });
84
+ });
85
+ await model.requestElementData(Array.from(uniqueIris));
86
+ await performLayout({ signal });
87
+ }
88
+ /**
89
+ * Subscribes to the model's changes so diagram edits are persisted to local storage. The
90
+ * subscription is tracked in {@link subscriptions} so it is released together with the React
91
+ * root in {@link unmountReactodia}.
92
+ */
93
+ function onDiagramChange(model) {
94
+ const storage = service(DiagramStorageService);
95
+ const diagramService = service(DiagramService);
96
+ subscriptions.add(diagramService.subscribeToDiagramChange(model, (diagram) => storage.save(diagram)));
97
+ }
98
+ /**
99
+ * Subscribes to the host's "clear persisted diagram" command so the saved layout is dropped on
100
+ * request (e.g. before a fresh seed should take precedence over previously saved edits). The
101
+ * subscription is tracked in {@link subscriptions} so it is released together with the React
102
+ * root in {@link unmountReactodia}.
103
+ */
104
+ function onClearDiagramStorage() {
105
+ const storage = service(DiagramStorageService);
106
+ const eventService = service(EventService);
107
+ subscriptions.add(eventService.subscribeToClearDiagramStorage(() => storage.clear()));
108
+ }
48
109
  /**
49
110
  * The Reactodia workspace React component.
50
111
  *
@@ -55,22 +116,32 @@ async function seedCanvas(context, seed, signal) {
55
116
  * provider's lookup. With a seed, the seeded nodes are placed on the canvas on startup.
56
117
  */
57
118
  function ReactodiaApp(props) {
58
- const { language, initialDiagram, config } = props;
119
+ const { language, config } = props;
120
+ const currentDiagram = props.isReload ? exportReactodiaLayout() : undefined;
59
121
  const { onMount } = useLoadedWorkspace(async ({ context, signal }) => {
122
+ var _a, _b;
60
123
  workspaceContext = context;
61
124
  const { model } = context;
125
+ onDiagramChange(model);
126
+ onClearDiagramStorage();
62
127
  const dataProvider = createDataProvider(props);
63
- // A language change remounts this component to rebuild the workspace with the new
64
- // translation bundle; restoring the exported diagram keeps the user's canvas intact.
65
- // This is done because there is no existing mechanism to re-translate the UI at runtime (not for the UI labels at least)
66
- if (initialDiagram) {
67
- await model.importLayout({ dataProvider, diagram: initialDiagram, signal });
128
+ const isReload = props.isReload;
129
+ const savedDiagram = service(DiagramStorageService).load();
130
+ if (isReload) {
131
+ // Simply reload without using the current state. This may happen when the user switches the language at runtime.
132
+ // since there is no existing mechanism to re-translate the UI at runtime (not for the UI labels at least)
133
+ await model.importLayout({ dataProvider, diagram: currentDiagram, signal });
134
+ }
135
+ else if ((_a = config.seedIris) === null || _a === void 0 ? void 0 : _a.length) {
136
+ await seedIrisToCanvas(context, dataProvider, config.seedIris, signal);
137
+ }
138
+ else if ((_b = config.seedGraph) === null || _b === void 0 ? void 0 : _b.length) {
139
+ await seedGraphToCanvas(context, dataProvider, config.seedGraph, signal);
68
140
  }
69
141
  else {
70
- await model.createNewDiagram({ dataProvider, signal });
71
- if (config.seedIris) {
72
- await seedCanvas(context, config.seedIris, signal);
73
- }
142
+ // Restore the previously saved layout when present, otherwise start empty. Either way this binds the
143
+ // data provider (savedDiagram is undefined -> empty diagram), so the unified search/lookup works.
144
+ await model.importLayout({ dataProvider, diagram: savedDiagram, signal });
74
145
  }
75
146
  }, [language]);
76
147
  /**
@@ -121,5 +192,6 @@ export function exportReactodiaLayout() {
121
192
  * Unmounts the Reactodia application.
122
193
  */
123
194
  export function unmountReactodia(root) {
195
+ subscriptions.unsubscribeAll();
124
196
  root.unmount();
125
197
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Service for diagram related operations and subscriptions
3
+ */
4
+ export class DiagramService {
5
+ /**
6
+ * Subscribes to diagram changes. On every debounced change the current layout is
7
+ * exported and passed to the callback
8
+ *
9
+ * @param model The diagram model to observe.
10
+ * @param onChangeCallback Called with the exported layout when the diagram changes.
11
+ * @returns A {@link Subscription} that, when called, removes the listener and drops any
12
+ * pending notification.
13
+ */
14
+ subscribeToDiagramChange(model, onChangeCallback) {
15
+ let notifyTimer;
16
+ const onHistoryChange = () => {
17
+ if (notifyTimer) {
18
+ clearTimeout(notifyTimer);
19
+ }
20
+ notifyTimer = setTimeout(() => {
21
+ notifyTimer = undefined;
22
+ onChangeCallback(model.exportLayout());
23
+ }, DiagramService.NOTIFY_DEBOUNCE_MS);
24
+ };
25
+ model.history.events.on(DiagramService.HISTORY_CHANGED_EVENT, onHistoryChange);
26
+ return () => {
27
+ model.history.events.off(DiagramService.HISTORY_CHANGED_EVENT, onHistoryChange);
28
+ if (notifyTimer) {
29
+ clearTimeout(notifyTimer);
30
+ }
31
+ };
32
+ }
33
+ }
34
+ /**
35
+ * Debounce time for the callback trigger. This is needed, because certain events (like moving a node) can trigger
36
+ * multiple `historyChanged` events in a short time.
37
+ */
38
+ DiagramService.NOTIFY_DEBOUNCE_MS = 300;
39
+ DiagramService.HISTORY_CHANGED_EVENT = 'historyChanged';
@@ -0,0 +1,42 @@
1
+ import { service } from "../../providers/service/service-inject";
2
+ import { WindowService } from "../window/window.service";
3
+ /**
4
+ * Service for local store-related diagram operations
5
+ */
6
+ export class DiagramStorageService {
7
+ /**
8
+ * Load the persisted diagram from the local store, or `undefined` when nothing is stored (or the stored value
9
+ * is unreadable).
10
+ */
11
+ load() {
12
+ var _a;
13
+ const raw = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.getItem(DiagramStorageService.STORAGE_KEY);
14
+ if (!raw) {
15
+ return undefined;
16
+ }
17
+ return JSON.parse(raw);
18
+ }
19
+ /**
20
+ * Persists the given diagram serialization.
21
+ */
22
+ save(diagram) {
23
+ var _a;
24
+ (_a = this.storage) === null || _a === void 0 ? void 0 : _a.setItem(DiagramStorageService.STORAGE_KEY, JSON.stringify(diagram));
25
+ }
26
+ /**
27
+ * Removes the persisted diagram. Used when a new view (a seed) should take precedence over
28
+ * previously saved edits.
29
+ */
30
+ clear() {
31
+ var _a;
32
+ (_a = this.storage) === null || _a === void 0 ? void 0 : _a.removeItem(DiagramStorageService.STORAGE_KEY);
33
+ }
34
+ /**
35
+ * Local storage getter
36
+ */
37
+ get storage() {
38
+ var _a;
39
+ return (_a = service(WindowService).getWindow()) === null || _a === void 0 ? void 0 : _a.localStorage;
40
+ }
41
+ }
42
+ DiagramStorageService.STORAGE_KEY = 'diagram.state';
@@ -0,0 +1,22 @@
1
+ import { CLEAR_DIAGRAM_STORAGE_EVENT } from "../../models/clear-diagram-storage-event";
2
+ import { service } from "../../providers/service/service-inject";
3
+ import { WindowService } from "../window/window.service";
4
+ /**
5
+ * Service for event based operations
6
+ */
7
+ export class EventService {
8
+ /**
9
+ * Subscribes to the host's "clear persisted diagram" command.
10
+ *
11
+ * @param onClear Called whenever the event fires.
12
+ * @returns A {@link Subscription} that, when called, removes the listener.
13
+ */
14
+ subscribeToClearDiagramStorage(onClear) {
15
+ const target = service(WindowService).getWindow();
16
+ if (!target) {
17
+ return () => undefined;
18
+ }
19
+ target.addEventListener(CLEAR_DIAGRAM_STORAGE_EVENT, onClear);
20
+ return () => target.removeEventListener(CLEAR_DIAGRAM_STORAGE_EVENT, onClear);
21
+ }
22
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Service that provides access to browser window-related functionality.
3
+ */
4
+ export class WindowService {
5
+ getWindow() {
6
+ return window;
7
+ }
8
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal in-memory {@link Storage}, so services that read `localStorage` can be exercised
3
+ * without a real browser store and asserted against directly. Shared across test suites.
4
+ */
5
+ export class StorageMock {
6
+ constructor() {
7
+ this.store = new Map();
8
+ }
9
+ get length() {
10
+ return this.store.size;
11
+ }
12
+ clear() {
13
+ this.store.clear();
14
+ }
15
+ getItem(key) {
16
+ return this.store.has(key) ? this.store.get(key) : null;
17
+ }
18
+ key(index) {
19
+ var _a;
20
+ return (_a = Array.from(this.store.keys())[index]) !== null && _a !== void 0 ? _a : null;
21
+ }
22
+ removeItem(key) {
23
+ this.store.delete(key);
24
+ }
25
+ setItem(key, value) {
26
+ this.store.set(key, value);
27
+ }
28
+ }
29
+ /**
30
+ * Creates a fresh {@link StorageMock}. Suites install the returned instance as the global
31
+ * `localStorage` themselves.
32
+ */
33
+ export function createStorageMock() {
34
+ return new StorageMock();
35
+ }
@@ -1 +1 @@
1
- export {};
1
+ export { CLEAR_DIAGRAM_STORAGE_EVENT } from './components/graphwise-reactodia/models/clear-diagram-storage-event';
@@ -1,11 +1,26 @@
1
- // Mock all responses as turtle, as we won't be making any SPARQL requests in the dev server.
2
- async function stubQueryFunction(_params) {
3
- return (new Response('', {headers: {'Content-Type': 'text/turtle'}}));
1
+ // There is no SPARQL endpoint behind the dev server, so every query is answered with an empty
2
+ // result. The shape has to match what the provider asks for: SELECT/ASK queries are parsed with
3
+ // `response.json()` and would blow up on an empty body, CONSTRUCT/DESCRIBE queries are parsed as
4
+ // turtle, where an empty body is a valid (empty) graph.
5
+ async function stubQueryFunction(params) {
6
+ const accept = params.headers?.['Accept'] ?? '';
7
+ if (accept.includes('sparql-results+json')) {
8
+ const body = JSON.stringify({head: {vars: []}, results: {bindings: []}, boolean: false});
9
+ return new Response(body, {headers: {'Content-Type': 'application/sparql-results+json'}});
10
+ }
11
+ return new Response('', {headers: {'Content-Type': 'text/turtle'}});
4
12
  }
5
13
 
6
- // IRIs of the entities the host asks to pre-populate the canvas with on startup.
7
14
  const SEED_NODES = ['http://example.com/alice', 'http://example.com/bob'];
8
15
 
16
+ const SEED_GRAPH = [
17
+ {
18
+ source: 'http://example.com/alice',
19
+ target: 'http://example.com/bob',
20
+ rawPredicates: ['http://example.com/knows'],
21
+ },
22
+ ];
23
+
9
24
  const reactodia = document.querySelector('graphwise-reactodia');
10
25
 
11
26
  function setQueryFunction() {
@@ -16,6 +31,10 @@ function setSeed() {
16
31
  reactodia.config = {...reactodia.config, seedIris: SEED_NODES};
17
32
  }
18
33
 
34
+ function setSeedGraph() {
35
+ reactodia.config = {...reactodia.config, seedGraph: SEED_GRAPH};
36
+ }
37
+
19
38
  function setRepository(repository) {
20
39
  reactodia.currentRepository = repository;
21
40
  }
@@ -23,3 +42,9 @@ function setRepository(repository) {
23
42
  function setLanguage(language) {
24
43
  reactodia.language = language;
25
44
  }
45
+
46
+ // The host asks the component to drop its persisted diagram by dispatching a namespaced window
47
+ // event, rather than holding an element reference and calling a method on it.
48
+ function clearDiagramStorage() {
49
+ window.dispatchEvent(new Event('graphwise-reactodia:clear-diagram-storage'));
50
+ }