graphwise-reactodia 0.0.1-TR7 → 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 (42) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/graphwise-reactodia.cjs.entry.js +256 -41
  3. package/dist/cjs/index.cjs.js +7 -0
  4. package/dist/collection/components/graphwise-reactodia/graphwise-reactodia.js +10 -10
  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/triple.js +1 -0
  8. package/dist/collection/components/graphwise-reactodia/providers/service/service-inject.js +15 -0
  9. package/dist/collection/components/graphwise-reactodia/providers/service/service.js +1 -0
  10. package/dist/collection/components/graphwise-reactodia/providers/service/service.provider.js +20 -0
  11. package/dist/collection/components/graphwise-reactodia/reactodia-app.js +61 -28
  12. package/dist/collection/components/graphwise-reactodia/services/diagram/diagram.service.js +39 -0
  13. package/dist/collection/components/graphwise-reactodia/services/diagram-storage/diagram-storage.service.js +42 -0
  14. package/dist/collection/components/graphwise-reactodia/services/event/event.service.js +22 -0
  15. package/dist/collection/components/graphwise-reactodia/services/window/window.service.js +8 -0
  16. package/dist/collection/components/graphwise-reactodia/testing/storage.mock.js +35 -0
  17. package/dist/collection/index.js +1 -1
  18. package/dist/collection/pages/graphwise-reactodia/main.js +17 -7
  19. package/dist/esm/graphwise-reactodia.entry.js +253 -38
  20. package/dist/esm/index.js +6 -0
  21. package/dist/graphwise-reactodia/graphwise-reactodia.esm.js +1 -1
  22. package/dist/graphwise-reactodia/index.esm.js +1 -0
  23. package/dist/graphwise-reactodia/p-80cb6eef.entry.js +1 -0
  24. package/dist/types/components/graphwise-reactodia/i18n/translations.d.ts +2 -6
  25. package/dist/types/components/graphwise-reactodia/models/clear-diagram-storage-event.d.ts +5 -0
  26. package/dist/types/components/graphwise-reactodia/models/reactodia-app-props.d.ts +6 -7
  27. package/dist/types/components/graphwise-reactodia/models/reactodia-config.d.ts +8 -6
  28. package/dist/types/components/graphwise-reactodia/models/subscription-list.d.ts +26 -0
  29. package/dist/types/components/graphwise-reactodia/models/subscription.d.ts +4 -0
  30. package/dist/types/components/graphwise-reactodia/models/{reactodia-seed-link.d.ts → triple.d.ts} +2 -6
  31. package/dist/types/components/graphwise-reactodia/providers/service/service-inject.d.ts +13 -0
  32. package/dist/types/components/graphwise-reactodia/providers/service/service.d.ts +6 -0
  33. package/dist/types/components/graphwise-reactodia/providers/service/service.provider.d.ts +18 -0
  34. package/dist/types/components/graphwise-reactodia/services/diagram/diagram.service.d.ts +28 -0
  35. package/dist/types/components/graphwise-reactodia/services/diagram-storage/diagram-storage.service.d.ts +26 -0
  36. package/dist/types/components/graphwise-reactodia/services/event/event.service.d.ts +14 -0
  37. package/dist/types/components/graphwise-reactodia/services/window/window.service.d.ts +7 -0
  38. package/dist/types/components/graphwise-reactodia/testing/storage.mock.d.ts +18 -0
  39. package/dist/types/index.d.ts +1 -1
  40. package/package.json +9 -3
  41. package/dist/graphwise-reactodia/p-eb362faa.entry.js +0 -1
  42. /package/dist/collection/components/graphwise-reactodia/models/{reactodia-seed-link.js → subscription.js} +0 -0
@@ -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,15 +41,16 @@ 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
  }
@@ -46,18 +58,13 @@ async function seedCanvas(context, seed, signal) {
46
58
  await performLayout({ signal });
47
59
  }
48
60
  /**
49
- * Places a pre-resolved graph (e.g. a CONSTRUCT query result) on the canvas. The relationships
50
- * are computed and not persisted in the repository, so the edges are drawn directly from the
51
- * supplied data instead of being fetched. The node IRIs do exist in the repository, so only
52
- * their display data is hydrated via {@link DataDiagramModel.requestElementData}.
53
- *
54
- * Note we use `requestElementData` rather than `requestData`: the latter would also query the
55
- * provider for *all* persisted links between the seeded nodes, over-fetching beyond the
56
- * CONSTRUCT's selection. Hydrating elements alone keeps the canvas to exactly the supplied
57
- * edges, while further expansion continues lazily against the SPARQL provider.
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
58
64
  */
59
- async function seedGraphCanvas(context, links, signal) {
65
+ async function seedGraphToCanvas(context, dataProvider, links, signal) {
60
66
  const { model, performLayout } = context;
67
+ await model.createNewDiagram({ dataProvider, signal });
61
68
  const uniqueIris = new Set();
62
69
  links.forEach((link) => {
63
70
  var _a;
@@ -75,11 +82,30 @@ async function seedGraphCanvas(context, links, signal) {
75
82
  });
76
83
  });
77
84
  });
78
- // Request data only for elements.
79
- // Link data is not available in the DB, since we are displaying construct/describe queries
80
85
  await model.requestElementData(Array.from(uniqueIris));
81
86
  await performLayout({ signal });
82
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
+ }
83
109
  /**
84
110
  * The Reactodia workspace React component.
85
111
  *
@@ -90,26 +116,32 @@ async function seedGraphCanvas(context, links, signal) {
90
116
  * provider's lookup. With a seed, the seeded nodes are placed on the canvas on startup.
91
117
  */
92
118
  function ReactodiaApp(props) {
93
- const { language, initialDiagram, config } = props;
119
+ const { language, config } = props;
120
+ const currentDiagram = props.isReload ? exportReactodiaLayout() : undefined;
94
121
  const { onMount } = useLoadedWorkspace(async ({ context, signal }) => {
95
122
  var _a, _b;
96
123
  workspaceContext = context;
97
124
  const { model } = context;
125
+ onDiagramChange(model);
126
+ onClearDiagramStorage();
98
127
  const dataProvider = createDataProvider(props);
99
- // A language change remounts this component to rebuild the workspace with the new
100
- // translation bundle; restoring the exported diagram keeps the user's canvas intact.
101
- // This is done because there is no existing mechanism to re-translate the UI at runtime (not for the UI labels at least)
102
- if (initialDiagram) {
103
- 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);
104
140
  }
105
141
  else {
106
- await model.createNewDiagram({ dataProvider, signal });
107
- if ((_a = config.seedGraph) === null || _a === void 0 ? void 0 : _a.length) {
108
- await seedGraphCanvas(context, config.seedGraph, signal);
109
- }
110
- else if ((_b = config.seedIris) === null || _b === void 0 ? void 0 : _b.length) {
111
- await seedCanvas(context, config.seedIris, signal);
112
- }
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 });
113
145
  }
114
146
  }, [language]);
115
147
  /**
@@ -160,5 +192,6 @@ export function exportReactodiaLayout() {
160
192
  * Unmounts the Reactodia application.
161
193
  */
162
194
  export function unmountReactodia(root) {
195
+ subscriptions.unsubscribeAll();
163
196
  root.unmount();
164
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,18 +1,22 @@
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
 
9
- // Pre-resolved edges (e.g. a CONSTRUCT result) the host asks to draw directly on the canvas,
10
- // without querying the SPARQL endpoint for link data.
11
16
  const SEED_GRAPH = [
12
17
  {
13
18
  source: 'http://example.com/alice',
14
19
  target: 'http://example.com/bob',
15
- predicates: ['knows'],
16
20
  rawPredicates: ['http://example.com/knows'],
17
21
  },
18
22
  ];
@@ -38,3 +42,9 @@ function setRepository(repository) {
38
42
  function setLanguage(language) {
39
43
  reactodia.language = language;
40
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
+ }