From 56f2d8635493244bb23caccf3037008c2c9bc20d Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sun, 21 Jun 2026 13:59:21 +0300 Subject: [PATCH] Update docs for @reactodia/workspace@0.35.0 --- .github/workflows/ci-checks.yml | 2 +- .github/workflows/deploy-pages.yml | 2 +- docs/components/form-input.md | 32 ++-- docs/components/workspace.md | 58 +++--- docs/concepts/event-system.md | 2 +- docs/concepts/graph-layout.md | 2 +- docs/concepts/graph-model.md | 2 +- docs/concepts/i18n.md | 137 ++++++++------ docs/concepts/workspace-context.md | 24 ++- docs/examples/live.md | 7 +- package-lock.json | 8 +- package.json | 2 +- src/examples/PlaygroundBasic.tsx | 9 +- src/examples/PlaygroundClassicWorkspace.tsx | 28 ++- src/examples/PlaygroundGraphAuthoring.tsx | 21 ++- src/examples/PlaygroundRdfExplorer.tsx | 9 +- src/examples/PlaygroundSparql.tsx | 9 +- src/examples/PlaygroundStressTest.tsx | 10 +- src/examples/PlaygroundStyleCustomization.tsx | 35 ++-- src/examples/PlaygroundWikidata.tsx | 9 +- .../GenealogicalMetadataProvider.ts | 3 +- .../GenealogicalTree/GenealogicalTree.tsx | 168 +++++++++--------- 22 files changed, 336 insertions(+), 243 deletions(-) diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index 33bef40e..6f43270c 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -10,7 +10,7 @@ on: branches: [ "main" ] env: - reactodia_workspace_ref: 'v0.34.1' + reactodia_workspace_ref: 'v0.35.0' jobs: build: diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index ae01fa85..58fed323 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: - reactodia_workspace_ref: 'v0.34.1' + reactodia_workspace_ref: 'v0.35.0' jobs: build: diff --git a/docs/components/form-input.md b/docs/components/form-input.md index a878d4c4..c7ed90ee 100644 --- a/docs/components/form-input.md +++ b/docs/components/form-input.md @@ -22,8 +22,21 @@ Currently form input components are considered **unstable** so there might be br ```tsx live noInline function Example() { const GRAPH_DATA = 'https://reactodia.github.io/resources/orgOntology.ttl'; + const RDF_COMMENT = 'http://www.w3.org/2000/01/rdf-schema#comment'; const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + metadataProvider: new Reactodia.BaseMetadataProvider({ + canModifyEntity: () => ({canEdit: true}), + getEntityShape: types => ({ + properties: new Map([ + [Reactodia.rdfs.label, {valueShape: {termType: 'Literal'}}], + [RDF_COMMENT, {valueShape: {termType: 'Literal'}}], + ]) + }), + }), + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, editor, getCommandBus, performLayout} = context; @@ -41,23 +54,10 @@ function Example() { .trigger('editEntity', {target: element}); }, []); - const RDF_COMMENT = 'http://www.w3.org/2000/01/rdf-schema#comment'; - - const [metadataProvider] = React.useState(() => new Reactodia.BaseMetadataProvider({ - canModifyEntity: () => ({canEdit: true}), - getEntityShape: types => ({ - properties: new Map([ - [Reactodia.rdfs.label, {valueShape: {termType: 'Literal'}}], - [RDF_COMMENT, {valueShape: {termType: 'Literal'}}], - ]) - }), - })); - return (
- + - +
); } diff --git a/docs/components/workspace.md b/docs/components/workspace.md index 0bfdccf8..dcb9725f 100644 --- a/docs/components/workspace.md +++ b/docs/components/workspace.md @@ -4,7 +4,9 @@ title: # Workspace Components -[``](/docs/api/workspace/classes/Workspace) is a top-level component which establishes [workspace context](/docs/api/workspace/interfaces/WorkspaceContext), which stores graph data and provides means to display and interact with the diagram. +The recommended way to display a workspace is to use [`createWorkspace()`](/docs/api/workspace/functions/createWorkspace.md) to create new [workspace context](/docs/api/workspace/interfaces/WorkspaceContext), which stores graph data and provides means to display and interact with the diagram. Then the context can be provided to child components with [``](/docs/api/workspace/functions/WorkspaceProvider.md). + +Alternatively, [``](/docs/api/workspace/classes/Workspace) can be used as a top-level utility component which both creates and provides the context. ## Hooks @@ -12,9 +14,9 @@ title: [`useWorkspace()`](/docs/api/workspace/functions/useWorkspace) hook can be used from inside `` child components to access workspace context; see [workspace context](/docs/concepts/workspace-context.md) for details. -## Workspace Layout +## `` and built-in layouts -The UI of the workspace is defined by the child components provided to the ``: +The UI of the workspace is composed of various components which use provided [workspace context](/docs/api/workspace/interfaces/WorkspaceContext): Default built-in layout is provided as [``](/docs/api/workspace/functions/DefaultWorkspace) component which includes the [unified search](/docs/components/unified-search.md), main menu and action [toolbars](/docs/components/toolbar.md) and all built-in [canvas widgets](/docs/components/canvas.md#widgets). @@ -27,24 +29,27 @@ When providing a custom workspace layout it is required to use [` Reactodia.createWorkspace({ + defaultLayout, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { - const {model, view} = context; - const dataProvider = new Reactodia.EmptyDataProvider(); - await model.createNewDiagram({dataProvider, signal}); - model.createElement('http://example.com/entity'); - const canvas = view.findAnyCanvas(); - canvas.zoomToFit(); + const {model, view} = context; + const dataProvider = new Reactodia.EmptyDataProvider(); + await model.createNewDiagram({dataProvider, signal}); + model.createElement('http://example.com/entity'); + const canvas = view.findAnyCanvas(); + canvas.zoomToFit(); }, []); return (
- + - +
); } @@ -52,24 +57,25 @@ function Example() { ## Customize type styles -Reactodia displays [entities](/docs/concepts/graph-model.md) in different places throughout the workspace components. It is possible to customize overall style based on entity types (accent color and icon) by providing a custom [`TypeStyleResolver`](/docs/api/workspace/type-aliases/TypeStyleResolver.md) to the ``: +Reactodia displays [entities](/docs/concepts/graph-model.md) in different places throughout the workspace components. It is possible to customize overall style based on entity types (accent color and icon) by providing a custom [`typeStyleResolver`](/docs/api/workspace/type-aliases/TypeStyleResolver.md) to the [`createWorkspace()`](/docs/api/workspace/functions/createWorkspace.md): ```tsx - Reactodia.createWorkspace({ + defaultLayout, typeStyleResolver={types => { - if (types.includes('http://www.w3.org/2000/01/rdf-schema#Class')) { - return {icon: CERTIFICATE_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#Class')) { - return {icon: CERTIFICATE_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#ObjectProperty')) { - return {icon: COG_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#DatatypeProperty')) { - return {color: '#00b9f2'}; - } else { - return undefined; - } + if (types.includes('http://www.w3.org/2000/01/rdf-schema#Class')) { + return {icon: CERTIFICATE_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#Class')) { + return {icon: CERTIFICATE_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#ObjectProperty')) { + return {icon: COG_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#DatatypeProperty')) { + return {color: '#00b9f2'}; + } else { + return undefined; + } }} - ...> +})); ``` By default, the colors are assigned deterministically based on total hash of the types in the [entity data](/docs/api/workspace/interfaces/ElementModel.md). diff --git a/docs/concepts/event-system.md b/docs/concepts/event-system.md index c290d8c6..3c9c2136 100644 --- a/docs/concepts/event-system.md +++ b/docs/concepts/event-system.md @@ -165,4 +165,4 @@ function OtherComponent() { } ``` -Each Reactodia [``](/docs/components/workspace.md) instance maintains its own command buses for each topic not connected to other workspaces in any way. +Each [workspace context](/docs/concepts/workspace-context.md) instance maintains its own command buses for each topic not connected to other workspaces in any way. diff --git a/docs/concepts/graph-layout.md b/docs/concepts/graph-layout.md index 8dfa097b..2b6f9bde 100644 --- a/docs/concepts/graph-layout.md +++ b/docs/concepts/graph-layout.md @@ -49,7 +49,7 @@ const onClick = async () => { ## Default layout configuration -`defaultLayout` is the required prop for the top-level [``](/docs/components/workspace.md) component and Reactodia provides two approaches to get a good default layout function: +`defaultLayout` is the required prop for [`createWorkspace()`](/docs/components/workspace.md) or the top-level [``](/docs/components/workspace.md) component and Reactodia provides two approaches to get a good default layout function: ### Layout via Web Workers diff --git a/docs/concepts/graph-model.md b/docs/concepts/graph-model.md index ee98c2b0..69ea1634 100644 --- a/docs/concepts/graph-model.md +++ b/docs/concepts/graph-model.md @@ -131,7 +131,7 @@ The diagram layout (which includes cell positions and states) can be exported (s ```tsx function WorkingWithImportExport() { - const {model, onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { + const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model} = context; // Import a diagram layout on mount diff --git a/docs/concepts/i18n.md b/docs/concepts/i18n.md index a91bb0f9..5dd37aa3 100644 --- a/docs/concepts/i18n.md +++ b/docs/concepts/i18n.md @@ -32,42 +32,54 @@ This schema is available as [`@reactodia/workspace/i18n/i18n.schema.json`](https and can be used with external JSON validation tool (e.g. [`ajv-cli`](https://github.com/ajv-validator/ajv-cli)) to check the translations. -To provide customized translation, additional bundles can be passed to -[`Workspace`](/docs/components/workspace.md) component with `translations` property: +To provide customized translation, all bundles with localized strings can be passed to [`DefaultTranslation`](/docs/api/workspace/classes/DefaultTranslation.md) instance provided with `translation` option to [`createWorkspace()`](/docs/api/workspace/functions/createWorkspace.md): ```tsx function TranslationOverride() { - return ( - - - - ); + const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + translation: new Reactodia.DefaultTranslation({ + bundles: [ + { + 'default_workspace': { + 'search_section_entity_types.label': 'Class Tree', + 'search_section_entity_types.title': 'Class tree hierarchy', + } + }, + // Fallback bundle with built-in "en" translation strings + Reactodia.DefaultTranslationBundle, + ], + }) + })); + return ( + + + + ); } ``` Additional built-in or custom translation can be used the same way by loading the JSON bundle externally and -pass them with `translations` property in th order of the one with the high priority to low priority. -Default `en` translation [`@reactodia/workspace/i18n/translations/en.reactodia-translation.json`](https://github.com/reactodia/reactodia-workspace/blob/master/i18n/translations/en.reactodia-translation.json) is always -enabled as a fallback unless `useDefaultTranslation={false}` is specified: +pass them with `bundles` property in the high priority to low priority order. + +It is also possible to exclude default `en` translation [`@reactodia/workspace/i18n/translations/en.reactodia-translation.json`](https://github.com/reactodia/reactodia-workspace/blob/master/i18n/translations/en.reactodia-translation.json) by omitting [`DefaultTranslationBundle`](/docs/api/workspace/variables/DefaultTranslationBundle.md) from `bundles` array: ```tsx import enUkTranslation from './en-uk.reactodia-translation.json'; import deTranslation from './de.reactodia-translation.json'; function MultipleTranslations() { - return ( - - - - ); + const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + translation: new Reactodia.DefaultTranslation({ + bundles: [deTranslation, enUkTranslation], + }), + })); + return ( + + + + ); } ``` @@ -80,36 +92,59 @@ The localization mechanism can be used for a custom component nested inside the In the following example, additional custom translation keys are added to the workspace to provide localizable component labels: ```tsx live noInline function MyComponent() { - const t = Reactodia.useTranslation(); - return ( - alert('Done')}> - {t.text('my_component.do_action.label')} - - ); + const t = Reactodia.useTranslation(); + return ( + alert('Done')}> + {t.text('my_component.do_action.label')} + + ); } function CustomTranslationKeys() { - const {defaultLayout} = Reactodia.useWorker(Layouts); - return ( -
- - } - /> - -
- ); + const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + translation: new Reactodia.DefaultTranslation({ + bundles: [ + { + 'my_component': { + 'do_action.label': 'Do the impossible!', + 'do_action.title': 'Perform the impossible action', + } + }, + Reactodia.DefaultTranslationBundle, + ], + }), + })); + return ( +
+ + } + /> + +
+ ); } render(); ``` + +## Translation context + +Translation mechanism can be used outside the [workspace context](/docs/concepts/workspace-context.md) in other parts of an application when needed by establishing the context with [``](/docs/api/workspace/functions/TranslationProvider.md) in order to make use of [`useTranslation()`](/docs/api/workspace/functions/useTranslation.md) hook: + +```tsx +function App() { + const translation = React.useMemo(() => new Reactodia.DefaultTranslation({ + bundles: [/*...*/], + }), []); + return ( + + {/* app components */} + + ); +} +``` diff --git a/docs/concepts/workspace-context.md b/docs/concepts/workspace-context.md index ba030330..a1381cc2 100644 --- a/docs/concepts/workspace-context.md +++ b/docs/concepts/workspace-context.md @@ -19,32 +19,38 @@ The [`WorkspaceContext`](/docs/api/workspace/interfaces/WorkspaceContext) contai ## Getting the workspace context -The [`WorkspaceContext`](/docs/api/workspace/interfaces/WorkspaceContext) instance can be acquired in two ways. -The first one is from the result of `useLoadedWorkspace()` hook which is used for initialization of the [`Workspace`](/docs/components/workspace.md) component: +The [`WorkspaceContext`](/docs/api/workspace/interfaces/WorkspaceContext) instance can be acquired in multiple ways. + +At the top level it is directly available when creating the workspace with [`createWorkspace()`](/docs/api/workspace/functions/createWorkspace.md): ```tsx function Example() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); + + // `workspace` is a workspace context instance - const {onMount, getContext} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { - /* ... */ + const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { + // The context is also available via `context` }, []); const onSomething = () => { - const {model, editor, /* etc */} = getContext(); + const {model, editor, /* etc */} = workspace; // Use workspace context }; return ( - + {/* ... */} - +
); } ``` -The second way is to use [`useWorkspace()`](/docs/api/workspace/functions/useWorkspace) hook inside the [`Workspace`](/docs/components/workspace.md) component itself, e.g. a canvas widget: +There is also a [`useWorkspace()`](/docs/api/workspace/functions/useWorkspace) hook to get the context from children rendered inside the [`Workspace`](/docs/components/workspace.md) or [`WorkspaceProvider`](/docs/components/workspace.md) components, e.g. a canvas widget: ```tsx function MyWidget() { diff --git a/docs/examples/live.md b/docs/examples/live.md index 6aa59f32..d0ff9db0 100644 --- a/docs/examples/live.md +++ b/docs/examples/live.md @@ -12,6 +12,9 @@ function SimpleExample() { const GRAPH_DATA = 'https://reactodia.github.io/resources/orgOntology.ttl'; const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, performLayout} = context; @@ -35,8 +38,8 @@ function SimpleExample() { return (
- +
diff --git a/package-lock.json b/package-lock.json index 9fddfad8..6bfacc6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@easyops-cn/docusaurus-search-local": "^0.51.0", "@mdx-js/react": "^3.1.0", "@reactodia/hashmap": "^0.2.1", - "@reactodia/workspace": "^0.34.1", + "@reactodia/workspace": "^0.35.0", "@zip.js/zip.js": "^2.8.23", "clsx": "^2.1.1", "n3": "^1.17.2", @@ -4752,9 +4752,9 @@ "license": "MIT" }, "node_modules/@reactodia/workspace": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@reactodia/workspace/-/workspace-0.34.1.tgz", - "integrity": "sha512-dqZJ/BxKzKGabfQbHDFSdvDW81Vi89c2v0aOs7YFyG/bodqWCQEzuQ4yBd/bB6Z8hKm9uHmybtkFjkCpXAHcJQ==", + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@reactodia/workspace/-/workspace-0.35.0.tgz", + "integrity": "sha512-3eIjJ567YyzRyluz2etsSh4Wwcfe1oVBnO+KJhTFKX6z1oNzddmZcTWCb2uAsigIzJ269/kAsCswVMwBBvMfOg==", "license": "LGPL-2.1-or-later", "dependencies": { "@reactodia/hashmap": "^0.2.1", diff --git a/package.json b/package.json index adad7aa4..fc04b5a7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@easyops-cn/docusaurus-search-local": "^0.51.0", "@mdx-js/react": "^3.1.0", "@reactodia/hashmap": "^0.2.1", - "@reactodia/workspace": "^0.34.1", + "@reactodia/workspace": "^0.35.0", "@zip.js/zip.js": "^2.8.23", "clsx": "^2.1.1", "n3": "^1.17.2", diff --git a/src/examples/PlaygroundBasic.tsx b/src/examples/PlaygroundBasic.tsx index 0a65dfbf..0a123b11 100644 --- a/src/examples/PlaygroundBasic.tsx +++ b/src/examples/PlaygroundBasic.tsx @@ -10,6 +10,9 @@ export function PlaygroundBasic() { const GRAPH_DATA = 'https://reactodia.github.io/resources/orgOntology.ttl'; const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, performLayout} = context; @@ -32,9 +35,9 @@ export function PlaygroundBasic() { }, []); return ( - + - + ); } diff --git a/src/examples/PlaygroundClassicWorkspace.tsx b/src/examples/PlaygroundClassicWorkspace.tsx index ff92c17c..1695e5eb 100644 --- a/src/examples/PlaygroundClassicWorkspace.tsx +++ b/src/examples/PlaygroundClassicWorkspace.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import * as Reactodia from '@reactodia/workspace'; -import { SemanticTypeStyles, makeOntologyLinkTemplates } from '@reactodia/workspace/legacy-styles'; +import { + SemanticTypeStyles, makeOntologyLinkTemplates, +} from '@reactodia/workspace/legacy-styles'; import * as N3 from 'n3'; import { ExampleToolbarMenu } from './ExampleCommon'; +import { ExampleMetadataProvider, ExampleValidationProvider } from './ExampleMetadata'; const OntologyLinkTemplates = makeOntologyLinkTemplates(Reactodia); const Layouts = Reactodia.defineLayoutWorker(() => new Worker( @@ -16,6 +19,13 @@ type TurtleDataSource = export function PlaygroundClassicWorkspace() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + metadataProvider: new ExampleMetadataProvider(), + validationProvider: new ExampleValidationProvider(), + renameLinkProvider: new RenameSubclassOfProvider(), + typeStyleResolver: SemanticTypeStyles, + })); const [dataSource, setDataSource] = React.useState({ type: 'url', @@ -48,9 +58,8 @@ export function PlaygroundClassicWorkspace() { }, [dataSource]); return ( - + { @@ -75,10 +84,19 @@ export function PlaygroundClassicWorkspace() { ) }} /> - + ); } +class RenameSubclassOfProvider extends Reactodia.RenameLinkToLinkStateProvider { + override canRename(link: Reactodia.Link): boolean { + return ( + link instanceof Reactodia.AnnotationLink || + link.typeId === 'http://www.w3.org/2000/01/rdf-schema#subClassOf' + ); + } +} + function ToolbarActionOpenTurtleGraph(props: { onOpen: (dataSource: TurtleDataSource) => void; }) { diff --git a/src/examples/PlaygroundGraphAuthoring.tsx b/src/examples/PlaygroundGraphAuthoring.tsx index 5ec07795..30cbc13e 100644 --- a/src/examples/PlaygroundGraphAuthoring.tsx +++ b/src/examples/PlaygroundGraphAuthoring.tsx @@ -3,10 +3,10 @@ import * as Reactodia from '@reactodia/workspace'; import * as Forms from '@reactodia/workspace/forms'; import * as N3 from 'n3'; +import { ExampleToolbarMenu } from './ExampleCommon'; import { ExampleMetadataProvider, ExampleValidationProvider, rdfs, example, } from './ExampleMetadata'; -import { ExampleToolbarMenu } from './ExampleCommon'; const Layouts = Reactodia.defineLayoutWorker(() => new Worker( new URL('@reactodia/workspace/layout.worker', import.meta.url) @@ -18,6 +18,12 @@ type TurtleDataSource = export function PlaygroundGraphAuthoring() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + metadataProvider: new ExampleMetadataProvider(), + validationProvider: new ExampleValidationProvider(), + renameLinkProvider: new RenameSubclassOfProvider(), + })); const [dataSource, setDataSource] = React.useState({ type: 'url', @@ -73,16 +79,9 @@ export function PlaygroundGraphAuthoring() { } }, [dataSource]); - const [metadataProvider] = React.useState(() => new ExampleMetadataProvider()); - const [validationProvider] = React.useState(() => new ExampleValidationProvider()); - const [renameLinkProvider] = React.useState(() => new RenameSubclassOfProvider()); - return ( - + @@ -119,7 +118,7 @@ export function PlaygroundGraphAuthoring() { ), }} /> - + ); } diff --git a/src/examples/PlaygroundRdfExplorer.tsx b/src/examples/PlaygroundRdfExplorer.tsx index 38ef7571..cd7f43f0 100644 --- a/src/examples/PlaygroundRdfExplorer.tsx +++ b/src/examples/PlaygroundRdfExplorer.tsx @@ -14,6 +14,9 @@ type TurtleDataSource = export function PlaygroundRdfExplorer() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const [dataSource, setDataSource] = React.useState({ type: 'url', @@ -45,8 +48,8 @@ export function PlaygroundRdfExplorer() { }, [dataSource]); return ( - + @@ -67,7 +70,7 @@ export function PlaygroundRdfExplorer() { {code: 'zh', label: '汉语'}, ]} /> - + ); } diff --git a/src/examples/PlaygroundSparql.tsx b/src/examples/PlaygroundSparql.tsx index 417d0616..b5af371f 100644 --- a/src/examples/PlaygroundSparql.tsx +++ b/src/examples/PlaygroundSparql.tsx @@ -13,6 +13,9 @@ const Layouts = Reactodia.defineLayoutWorker(() => new Worker( export function PlaygroundSparql() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const [connectionSettings, setConnectionSettings] = React.useState( (): SparqlConnectionSettings | undefined => { @@ -49,8 +52,8 @@ export function PlaygroundSparql() { }, [connectionSettings]); return ( - + } languages={[ @@ -72,6 +75,6 @@ export function PlaygroundSparql() { /> - + ); } diff --git a/src/examples/PlaygroundStressTest.tsx b/src/examples/PlaygroundStressTest.tsx index 092efb61..b62e4245 100644 --- a/src/examples/PlaygroundStressTest.tsx +++ b/src/examples/PlaygroundStressTest.tsx @@ -15,6 +15,9 @@ export function PlaygroundStressTest(props: { const {nodeCount = 500, edgesPerNode = 2, propertiesPerEdge = 0} = props; const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, view} = context; @@ -54,14 +57,15 @@ export function PlaygroundStressTest(props: { }, []); return ( - + } search={null} navigator={{expanded: false}} /> - + ); } diff --git a/src/examples/PlaygroundStyleCustomization.tsx b/src/examples/PlaygroundStyleCustomization.tsx index b0b094db..07edab2a 100644 --- a/src/examples/PlaygroundStyleCustomization.tsx +++ b/src/examples/PlaygroundStyleCustomization.tsx @@ -14,6 +14,22 @@ const Layouts = Reactodia.defineLayoutWorker(() => new Worker( export function PlaygroundStyleCustomization() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + typeStyleResolver: types => { + if (types.includes('http://www.w3.org/2000/01/rdf-schema#Class')) { + return {icon: CERTIFICATE_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#Class')) { + return {icon: CERTIFICATE_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#ObjectProperty')) { + return {icon: COG_ICON, iconMonochrome: true}; + } else if (types.includes('http://www.w3.org/2002/07/owl#DatatypeProperty')) { + return {color: '#00b9f2'}; + } else { + return undefined; + } + }, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model} = context; @@ -40,21 +56,8 @@ export function PlaygroundStyleCustomization() { }, []); return ( - { - if (types.includes('http://www.w3.org/2000/01/rdf-schema#Class')) { - return {icon: CERTIFICATE_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#Class')) { - return {icon: CERTIFICATE_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#ObjectProperty')) { - return {icon: COG_ICON, iconMonochrome: true}; - } else if (types.includes('http://www.w3.org/2002/07/owl#DatatypeProperty')) { - return {color: '#00b9f2'}; - } else { - return undefined; - } - }}> + { @@ -76,7 +79,7 @@ export function PlaygroundStyleCustomization() { menu={}> - + ); } diff --git a/src/examples/PlaygroundWikidata.tsx b/src/examples/PlaygroundWikidata.tsx index 6c44faa8..b38c0ded 100644 --- a/src/examples/PlaygroundWikidata.tsx +++ b/src/examples/PlaygroundWikidata.tsx @@ -9,6 +9,9 @@ const Layouts = Reactodia.defineLayoutWorker(() => new Worker( export function PlaygroundWikidata() { const {defaultLayout} = Reactodia.useWorker(Layouts); + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + })); const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, getCommandBus} = context; @@ -53,8 +56,8 @@ export function PlaygroundWikidata() { }, []); return ( - + @@ -71,7 +74,7 @@ export function PlaygroundWikidata() { { code: 'zh', label: '汉语' }, ]} /> - + ); } diff --git a/src/tools/GenealogicalTree/GenealogicalMetadataProvider.ts b/src/tools/GenealogicalTree/GenealogicalMetadataProvider.ts index c3463b63..51f1be37 100644 --- a/src/tools/GenealogicalTree/GenealogicalMetadataProvider.ts +++ b/src/tools/GenealogicalTree/GenealogicalMetadataProvider.ts @@ -179,6 +179,7 @@ export class GenealogicalMetadataProvider extends Reactodia.BaseMetadataProvider }); this.schemaProvider = schemaProvider; this.defaultNamespaceBase = defaultNamespaceBase; + this.loadedSchema.catch(() => {/* Silence initial rejected or cancelled task */}); } private async getSchema(): Promise { @@ -186,7 +187,7 @@ export class GenealogicalMetadataProvider extends Reactodia.BaseMetadataProvider } loadSchema(params: { signal: AbortSignal }) { - this.loadedSchema.catch(() => {/* Silence initial rejected or cancelled task */}); + this.loadedSchema.catch(() => {/* Silence previous load operation */}); this.loadedSchema = loadOwlShaclSchema({ schemaProvider: this.schemaProvider, signal: params.signal, diff --git a/src/tools/GenealogicalTree/GenealogicalTree.tsx b/src/tools/GenealogicalTree/GenealogicalTree.tsx index 158d0280..a929d647 100644 --- a/src/tools/GenealogicalTree/GenealogicalTree.tsx +++ b/src/tools/GenealogicalTree/GenealogicalTree.tsx @@ -32,8 +32,6 @@ interface DataSource { export function ToolGenealogicalTree() { const {defaultLayout} = Reactodia.useWorker(Layouts); - const [dataSource, setDataSource] = React.useState(); - const [schemaProvider] = React.useState(() => { const provider = new (class extends Reactodia.RdfDataProvider { override async knownElementTypes(params: { signal?: AbortSignal; }): Promise { @@ -55,17 +53,45 @@ export function ToolGenealogicalTree() { } return provider; }); - const [metadataProvider] = React.useState(() => new GenealogicalMetadataProvider({ - schemaProvider, - defaultNamespaceBase: GenealogicalPackage.DEFAULT_NAMESPACE_BASE, + + const [workspace] = React.useState(() => Reactodia.createWorkspace({ + defaultLayout, + translation: new Reactodia.DefaultTranslation({ + bundles: [ + { + ...enTranslation, + 'visual_authoring': { + 'property.load_error.text': '⚠ Failed to load variants', + }, + }, + Reactodia.DefaultTranslationBundle, + ], + }), + metadataProvider: new GenealogicalMetadataProvider({ + schemaProvider, + defaultNamespaceBase: GenealogicalPackage.DEFAULT_NAMESPACE_BASE, + }), + validationProvider: new GenealogicalValidationProvider(), + renameLinkProvider: new RenameGenealogicalLinksProvider(), + typeStyleResolver: types => { + if (types.includes(schema.Male)) { + return {color: '#01baef', icon: PERSON_ICON, iconMonochrome: true}; + } else if (types.includes(schema.Female)) { + return {color: '#f48da7ff', icon: PERSON_ICON, iconMonochrome: true}; + } else if (types.includes(schema.Person)) { + return {color: '#b5d2cb', icon: PERSON_ICON, iconMonochrome: true}; + } + }, })); - const [validationProvider] = React.useState(() => new GenealogicalValidationProvider()); - const [renameLinkProvider] = React.useState(() => new RenameGenealogicalLinksProvider()); - const {onMount, getContext} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { + const [dataSource, setDataSource] = React.useState(); + + const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => { const {model, editor, overlay, translation: t, getCommandBus, performLayout} = context; + const metadataProvider = editor.metadataProvider as GenealogicalMetadataProvider; + const validationProvider = editor.validationProvider as GenealogicalValidationProvider; - metadataProvider.loadSchema({signal}); + metadataProvider.loadSchema({signal}); editor.setAuthoringState(Reactodia.AuthoringState.empty); editor.setAuthoringMode(true); @@ -75,43 +101,43 @@ export function ToolGenealogicalTree() { }); signal.addEventListener('abort', () => listener.stopListening()); - let sourcePackage: GenealogicalPackage; - if (dataSource) { - try { - sourcePackage = await GenealogicalPackage.loadFromBytes(dataSource.bytes, {signal}); - } catch (err) { - throw new Error(t.text('genealogical_tree.init_failed_to_load_package'), {cause: err}); - } - } else { - sourcePackage = GenealogicalPackage.createEmpty(); + let sourcePackage: GenealogicalPackage; + if (dataSource) { + try { + sourcePackage = await GenealogicalPackage.loadFromBytes(dataSource.bytes, {signal}); + } catch (err) { + throw new Error(t.text('genealogical_tree.init_failed_to_load_package'), {cause: err}); } + } else { + sourcePackage = GenealogicalPackage.createEmpty(); + } - const uploader = new Forms.MemoryFileUploader({ - factory: Reactodia.Rdf.DefaultDataFactory, - disposeSignal: signal, - }); - const mainProvider = new GenealogicalDataProvider(sourcePackage, {uploader, signal}); - const dataProvider = new Reactodia.CompositeDataProvider({ - providers: [ - { - provider: schemaProvider, - origin: schemaProvider.factory.namedNode(genealogy.SchemaOrigin), - }, - { - provider: mainProvider, - origin: mainProvider.factory.namedNode(genealogy.DataOrigin), - }, - ], - }); + const uploader = new Forms.MemoryFileUploader({ + factory: Reactodia.Rdf.DefaultDataFactory, + disposeSignal: signal, + }); + const mainProvider = new GenealogicalDataProvider(sourcePackage, {uploader, signal}); + const dataProvider = new Reactodia.CompositeDataProvider({ + providers: [ + { + provider: schemaProvider, + origin: schemaProvider.factory.namedNode(genealogy.SchemaOrigin), + }, + { + provider: mainProvider, + origin: mainProvider.factory.namedNode(genealogy.DataOrigin), + }, + ], + }); - await metadataProvider.loadSettings({mainProvider, signal}); - metadataProvider.updateSettings(editor.authoringState); - validationProvider.setProvider(mainProvider); + await metadataProvider.loadSettings({mainProvider, signal}); + metadataProvider.updateSettings(editor.authoringState); + validationProvider.setProvider(mainProvider); - const initialSettings = metadataProvider.getSettings(); - const defaultLanguage = termAsString(getSinglePropertyValue( - initialSettings, genealogy.defaultLanguage - )); + const initialSettings = metadataProvider.getSettings(); + const defaultLanguage = termAsString(getSinglePropertyValue( + initialSettings, genealogy.defaultLanguage + )); if (defaultLanguage) { model.setLanguage(defaultLanguage); } @@ -163,51 +189,31 @@ export function ToolGenealogicalTree() { }, [dataSource]); return ( - { - if (types.includes(schema.Male)) { - return {color: '#01baef', icon: PERSON_ICON, iconMonochrome: true}; - } else if (types.includes(schema.Female)) { - return {color: '#f48da7ff', icon: PERSON_ICON, iconMonochrome: true}; - } else if (types.includes(schema.Person)) { - return {color: '#b5d2cb', icon: PERSON_ICON, iconMonochrome: true}; - } - }} - translations={[ - { - ...enTranslation, - 'visual_authoring': { - 'property.load_error.text': '⚠ Failed to load variants', - }, - } - ]}> + setDataSource({bytes})} - onSave={async () => { - const {model, editor} = getContext(); - const mainProvider = findGenealogicalProvider(model.dataProvider); + onOpen={bytes => setDataSource({bytes})} + onSave={async () => { + const {model, editor} = workspace; + const mainProvider = findGenealogicalProvider(model.dataProvider); - // Capture authoring state before finalizing the diagram - const authoringState = editor.authoringState; - editor.applyAuthoringChanges(); + // Capture authoring state before finalizing the diagram + const authoringState = editor.authoringState; + editor.applyAuthoringChanges(); - const updatedPackage = await mainProvider.sourcePackage.exportWith({ - dataProvider: mainProvider, - authoringState: mainProvider.cleanupAuthoring(authoringState), - diagram: model.exportLayout(), - uploader: mainProvider.uploader, - }); - setDataSource({bytes: await updatedPackage.bytes()}); - return updatedPackage; - }} + const updatedPackage = await mainProvider.sourcePackage.exportWith({ + dataProvider: mainProvider, + authoringState: mainProvider.cleanupAuthoring(authoringState), + diagram: model.exportLayout(), + uploader: mainProvider.uploader, + }); + setDataSource({bytes: await updatedPackage.bytes()}); + return updatedPackage; + }} /> } @@ -252,7 +258,7 @@ export function ToolGenealogicalTree() { - + ); }