@dxos/app-framework 0.3.3-main.3806cc2
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/.eslintrc.js +9 -0
- package/.storybook/main.cjs +36 -0
- package/.storybook/preview-head.html +3 -0
- package/.storybook/preview.cjs +45 -0
- package/LICENSE +8 -0
- package/README.md +29 -0
- package/README.yml +5 -0
- package/dist/lib/browser/index.mjs +453 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/index.cjs +522 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/types/src/App.d.ts +19 -0
- package/dist/types/src/App.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/plugins/IntentPlugin/IntentContext.d.ts +8 -0
- package/dist/types/src/plugins/IntentPlugin/IntentContext.d.ts.map +1 -0
- package/dist/types/src/plugins/IntentPlugin/IntentPlugin.d.ts +19 -0
- package/dist/types/src/plugins/IntentPlugin/IntentPlugin.d.ts.map +1 -0
- package/dist/types/src/plugins/IntentPlugin/index.d.ts +4 -0
- package/dist/types/src/plugins/IntentPlugin/index.d.ts.map +1 -0
- package/dist/types/src/plugins/IntentPlugin/intent.d.ts +28 -0
- package/dist/types/src/plugins/IntentPlugin/intent.d.ts.map +1 -0
- package/dist/types/src/plugins/PluginHost/PluginContext.d.ts +24 -0
- package/dist/types/src/plugins/PluginHost/PluginContext.d.ts.map +1 -0
- package/dist/types/src/plugins/PluginHost/PluginHost.d.ts +20 -0
- package/dist/types/src/plugins/PluginHost/PluginHost.d.ts.map +1 -0
- package/dist/types/src/plugins/PluginHost/index.d.ts +4 -0
- package/dist/types/src/plugins/PluginHost/index.d.ts.map +1 -0
- package/dist/types/src/plugins/PluginHost/plugin.d.ts +66 -0
- package/dist/types/src/plugins/PluginHost/plugin.d.ts.map +1 -0
- package/dist/types/src/plugins/PluginHost/plugin.test.d.ts +35 -0
- package/dist/types/src/plugins/PluginHost/plugin.test.d.ts.map +1 -0
- package/dist/types/src/plugins/SurfacePlugin/ErrorBoundary.d.ts +30 -0
- package/dist/types/src/plugins/SurfacePlugin/ErrorBoundary.d.ts.map +1 -0
- package/dist/types/src/plugins/SurfacePlugin/Surface.d.ts +45 -0
- package/dist/types/src/plugins/SurfacePlugin/Surface.d.ts.map +1 -0
- package/dist/types/src/plugins/SurfacePlugin/SurfacePlugin.d.ts +20 -0
- package/dist/types/src/plugins/SurfacePlugin/SurfacePlugin.d.ts.map +1 -0
- package/dist/types/src/plugins/SurfacePlugin/SurfaceRootContext.d.ts +13 -0
- package/dist/types/src/plugins/SurfacePlugin/SurfaceRootContext.d.ts.map +1 -0
- package/dist/types/src/plugins/SurfacePlugin/index.d.ts +5 -0
- package/dist/types/src/plugins/SurfacePlugin/index.d.ts.map +1 -0
- package/dist/types/src/plugins/common/graph.d.ts +26 -0
- package/dist/types/src/plugins/common/graph.d.ts.map +1 -0
- package/dist/types/src/plugins/common/index.d.ts +4 -0
- package/dist/types/src/plugins/common/index.d.ts.map +1 -0
- package/dist/types/src/plugins/common/layout.d.ts +89 -0
- package/dist/types/src/plugins/common/layout.d.ts.map +1 -0
- package/dist/types/src/plugins/common/translations.d.ts +22 -0
- package/dist/types/src/plugins/common/translations.d.ts.map +1 -0
- package/dist/types/src/plugins/helpers.d.ts +22 -0
- package/dist/types/src/plugins/helpers.d.ts.map +1 -0
- package/dist/types/src/plugins/index.d.ts +6 -0
- package/dist/types/src/plugins/index.d.ts.map +1 -0
- package/package.json +34 -0
- package/project.json +78 -0
- package/src/App.tsx +43 -0
- package/src/index.ts +7 -0
- package/src/plugins/IntentPlugin/IntentContext.tsx +17 -0
- package/src/plugins/IntentPlugin/IntentPlugin.tsx +69 -0
- package/src/plugins/IntentPlugin/index.ts +7 -0
- package/src/plugins/IntentPlugin/intent.ts +34 -0
- package/src/plugins/PluginHost/PluginContext.tsx +45 -0
- package/src/plugins/PluginHost/PluginHost.tsx +200 -0
- package/src/plugins/PluginHost/index.ts +7 -0
- package/src/plugins/PluginHost/plugin.test.ts +159 -0
- package/src/plugins/PluginHost/plugin.ts +82 -0
- package/src/plugins/SurfacePlugin/ErrorBoundary.tsx +45 -0
- package/src/plugins/SurfacePlugin/Surface.tsx +116 -0
- package/src/plugins/SurfacePlugin/SurfacePlugin.tsx +53 -0
- package/src/plugins/SurfacePlugin/SurfaceRootContext.tsx +22 -0
- package/src/plugins/SurfacePlugin/index.ts +8 -0
- package/src/plugins/common/graph.ts +33 -0
- package/src/plugins/common/index.ts +7 -0
- package/src/plugins/common/layout.ts +102 -0
- package/src/plugins/common/translations.ts +34 -0
- package/src/plugins/helpers.ts +43 -0
- package/src/plugins/index.ts +10 -0
- package/tsconfig.json +43 -0
- package/tsconfig.node.json +11 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
import { type FC } from 'react';
|
|
7
|
+
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { describe, test } from '@dxos/test';
|
|
10
|
+
|
|
11
|
+
import { type Plugin } from './plugin';
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
14
|
+
export interface Interface<T> {
|
|
15
|
+
key: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type InterfaceDef<T> = {
|
|
19
|
+
key: Interface<T>;
|
|
20
|
+
impl: T;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const provide = <T>(key: Interface<T>, impl: T): InterfaceDef<T> => ({ key, impl });
|
|
24
|
+
|
|
25
|
+
export type PluginProps = {
|
|
26
|
+
provides?: InterfaceDef<any>[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class TestPlugin {
|
|
30
|
+
readonly _interfaces = new Map<string, any>();
|
|
31
|
+
constructor({ provides }: PluginProps) {
|
|
32
|
+
provides?.forEach(({ key, impl }) => this._interfaces.set(key.key, impl));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getInterface = <T>(key: Interface<T>): T | undefined => this._interfaces.get(key.key);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const getInterfaces = <T>(plugins: TestPlugin[], key: Interface<T>): T[] =>
|
|
39
|
+
plugins.map((plugin) => plugin.getInterface(key)).filter(Boolean) as any;
|
|
40
|
+
|
|
41
|
+
//
|
|
42
|
+
// Interfaces allow shared typesafe contracts between plugins.
|
|
43
|
+
// The system might define common contracts (e.g., Printable); others would be defined by individual plugins.
|
|
44
|
+
// This allows parts of the system to find interfaces across all plugins to enable loosely coupled actions
|
|
45
|
+
// (separate from broadcast events) or requests for information.
|
|
46
|
+
//
|
|
47
|
+
|
|
48
|
+
export interface Printable {
|
|
49
|
+
print: (data: any | undefined) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Printable: Interface<Printable> = { key: 'printable' };
|
|
53
|
+
|
|
54
|
+
export interface Selectable {
|
|
55
|
+
selection: () => string | undefined;
|
|
56
|
+
select: (object?: { id: string | undefined }) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Selectable: Interface<Selectable> = { key: 'selectable' };
|
|
60
|
+
|
|
61
|
+
// NOTE: Would allow Stack to define a contract for plugins that have embeddable content.
|
|
62
|
+
export type Section = FC<{ object: any; onSelect: (object: string | undefined) => void }>;
|
|
63
|
+
|
|
64
|
+
export const Section: Interface<Section> = { key: 'list' };
|
|
65
|
+
|
|
66
|
+
//
|
|
67
|
+
// Plugin defs
|
|
68
|
+
//
|
|
69
|
+
|
|
70
|
+
// TODO(burdon): Tie to declarative manifest.
|
|
71
|
+
// {
|
|
72
|
+
// Printable: { print: (data: any) => console.log(data) }
|
|
73
|
+
// }
|
|
74
|
+
//
|
|
75
|
+
|
|
76
|
+
const plugin1 = new TestPlugin({
|
|
77
|
+
provides: [
|
|
78
|
+
provide(Printable, {
|
|
79
|
+
print: (data: any | undefined) => log(data),
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
provide(Selectable, {
|
|
83
|
+
selection: () => undefined,
|
|
84
|
+
select: (data: any) => {}, // TODO(burdon): Update plugin state.
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
// provide(Section, ({ object }) => <div>{JSON.stringify(object)}</div>),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const plugin2 = new TestPlugin({
|
|
92
|
+
provides: [
|
|
93
|
+
provide(Printable, {
|
|
94
|
+
print: (data: any) => log(data),
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const plugin3 = new TestPlugin({});
|
|
100
|
+
|
|
101
|
+
describe('Plugins', () => {
|
|
102
|
+
const plugins = [plugin1, plugin2, plugin3];
|
|
103
|
+
|
|
104
|
+
test('invocation', () => {
|
|
105
|
+
// Invoke against all plugins that provide interface.
|
|
106
|
+
expect(getInterfaces(plugins, Printable)).to.have.length(2);
|
|
107
|
+
getInterfaces(plugins, Printable).forEach((i) => i.print({}));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('symbols', () => {
|
|
111
|
+
const s1 = Symbol.for('s1');
|
|
112
|
+
const s2 = Symbol.for('s1');
|
|
113
|
+
expect(s1).to.be.eq(s2);
|
|
114
|
+
|
|
115
|
+
const map = new Map();
|
|
116
|
+
map.set(s1, true);
|
|
117
|
+
map.set(s2, false);
|
|
118
|
+
expect(map.get(s1)).to.be.false;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('filter', () => {
|
|
122
|
+
type Provides1 = {
|
|
123
|
+
test1: {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type Provides2 = {
|
|
127
|
+
test2: {};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const p1: Plugin<Provides1> = {
|
|
131
|
+
meta: { id: 'example.com/plugin/plugin-1' },
|
|
132
|
+
provides: {
|
|
133
|
+
test1: {},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const p2: Plugin<Provides2> = {
|
|
138
|
+
meta: { id: 'example.com/plugin/plugin-2' },
|
|
139
|
+
provides: {
|
|
140
|
+
test2: {},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const plugins: Plugin[] = [p1, p2];
|
|
145
|
+
|
|
146
|
+
const matching = plugins.filter((plugin) => true);
|
|
147
|
+
expect(matching).to.have.length(2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
152
|
+
// const StackContainer: FC<{}> = () => {
|
|
153
|
+
// const StackSection = plugin1.getInterface(Section)!;
|
|
154
|
+
// const selected = plugin1.getInterface(Selectable)?.selection();
|
|
155
|
+
|
|
156
|
+
// return (
|
|
157
|
+
// <StackSection object={selected} onSelect={(object) => plugin1.getInterface(Selectable)?.select({ id: object })} />
|
|
158
|
+
// );
|
|
159
|
+
// };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type FC, type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Capabilities provided by a plugin.
|
|
9
|
+
* The base surface capabilities are always included.
|
|
10
|
+
*/
|
|
11
|
+
export type PluginProvides<TProvides> = TProvides & {
|
|
12
|
+
/**
|
|
13
|
+
* React Context which is wrapped around the application to enable any hooks the plugin may provide.
|
|
14
|
+
*/
|
|
15
|
+
context?: FC<PropsWithChildren>;
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* React component which is rendered at the root of the application.
|
|
19
|
+
*/
|
|
20
|
+
root?: FC<PropsWithChildren>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A unit of containment of modular functionality that can be provided to an application.
|
|
25
|
+
* Plugins provide things like components, state, actions, etc. to the application.
|
|
26
|
+
*/
|
|
27
|
+
export type Plugin<TProvides = {}> = {
|
|
28
|
+
meta: {
|
|
29
|
+
/**
|
|
30
|
+
* Globally unique ID.
|
|
31
|
+
*
|
|
32
|
+
* Expected to be in the form of a valid URL.
|
|
33
|
+
*
|
|
34
|
+
* @example dxos.org/plugin/example
|
|
35
|
+
*/
|
|
36
|
+
id: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Short ID for use in URLs.
|
|
40
|
+
*
|
|
41
|
+
* NOTE: This is especially experimental and likely to change.
|
|
42
|
+
*/
|
|
43
|
+
// TODO(wittjosiah): How should these be managed?
|
|
44
|
+
shortId?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Capabilities provided by the plugin.
|
|
49
|
+
*/
|
|
50
|
+
provides: PluginProvides<TProvides>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Plugin definitions extend the base `Plugin` interface with additional lifecycle methods.
|
|
55
|
+
*/
|
|
56
|
+
export type PluginDefinition<TProvides = {}, TInitializeProvides = {}> = Omit<Plugin, 'provides'> & {
|
|
57
|
+
/**
|
|
58
|
+
* Capabilities provided by the plugin.
|
|
59
|
+
*/
|
|
60
|
+
provides?: Plugin<TProvides>['provides'];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize any async behavior required by the plugin.
|
|
64
|
+
*
|
|
65
|
+
* @return Capabilities provided by the plugin which are merged with base capabilities.
|
|
66
|
+
*/
|
|
67
|
+
initialize?: () => Promise<PluginProvides<TInitializeProvides>>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Called once all plugins have been initialized.
|
|
71
|
+
* This is the place to do any initialization which requires other plugins to be ready.
|
|
72
|
+
*
|
|
73
|
+
* @param plugins All plugins which successfully initialized.
|
|
74
|
+
*/
|
|
75
|
+
ready?: (plugins: Plugin[]) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Called when the plugin is unloaded.
|
|
79
|
+
* This is the place to do any cleanup required by the plugin.
|
|
80
|
+
*/
|
|
81
|
+
unload?: () => Promise<void>;
|
|
82
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { Component, type FC, type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
type Props = PropsWithChildren<{ data?: any; fallback: FC<{ error: Error; reset: () => void }> }>;
|
|
8
|
+
type State = { error: Error | undefined };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Surface error boundary.
|
|
12
|
+
*
|
|
13
|
+
* For basic usage prefer providing a fallback component to `Surface`.
|
|
14
|
+
*
|
|
15
|
+
* For more information on error boundaries, see:
|
|
16
|
+
* https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
|
17
|
+
*/
|
|
18
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
19
|
+
constructor(props: Props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = { error: undefined };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDerivedStateFromError(error: Error) {
|
|
25
|
+
return { error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override componentDidUpdate(prevProps: Props): void {
|
|
29
|
+
if (prevProps.data !== this.props.data) {
|
|
30
|
+
this.resetError();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override render() {
|
|
35
|
+
if (this.state.error) {
|
|
36
|
+
return <this.props.fallback error={this.state.error} reset={this.resetError} />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.props.children;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private resetError() {
|
|
43
|
+
this.setState({ error: undefined });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren, type ReactNode } from 'react';
|
|
6
|
+
import { createContext, useContext } from 'react';
|
|
7
|
+
|
|
8
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
9
|
+
import { type SurfaceComponent, useSurface } from './SurfaceRootContext';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Direction determines how multiple components are laid out.
|
|
13
|
+
*/
|
|
14
|
+
export type Direction = 'inline' | 'inline-reverse' | 'block' | 'block-reverse';
|
|
15
|
+
|
|
16
|
+
export type SurfaceProps = {
|
|
17
|
+
/**
|
|
18
|
+
* Role defines how the data should be rendered.
|
|
19
|
+
*/
|
|
20
|
+
role?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Names allow nested surfaces to be specified in the parent context, similar to a slot.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to the value of `role` if not specified.
|
|
26
|
+
*/
|
|
27
|
+
name?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The data to be rendered by the surface.
|
|
31
|
+
*/
|
|
32
|
+
data?: Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configure nested surfaces.
|
|
36
|
+
*/
|
|
37
|
+
surfaces?: Record<string, Pick<SurfaceProps, 'data' | 'surfaces'>>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* If specified, the Surface will be wrapped in an error boundary.
|
|
41
|
+
* The fallback component will be rendered if an error occurs.
|
|
42
|
+
*/
|
|
43
|
+
fallback?: ErrorBoundary['props']['fallback'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If more than one component is resolved, the limit determines how many are rendered.
|
|
47
|
+
*/
|
|
48
|
+
limit?: number | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* If more than one component is resolved, the direction determines how they are laid out.
|
|
52
|
+
*
|
|
53
|
+
* NOTE: This is not yet implemented.
|
|
54
|
+
*/
|
|
55
|
+
direction?: Direction;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A surface is a named region of the screen that can be populated by plugins.
|
|
60
|
+
*/
|
|
61
|
+
export const Surface = ({ role, name = role, fallback, ...rest }: SurfaceProps) => {
|
|
62
|
+
const props = { role, name, fallback, ...rest };
|
|
63
|
+
const context = useContext(SurfaceContext);
|
|
64
|
+
const data = props.data ?? ((name && context?.surfaces?.[name]?.data) || {});
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
{fallback ? (
|
|
69
|
+
<ErrorBoundary data={data} fallback={fallback}>
|
|
70
|
+
<SurfaceResolver {...props} />
|
|
71
|
+
</ErrorBoundary>
|
|
72
|
+
) : (
|
|
73
|
+
<SurfaceResolver {...props} />
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const SurfaceContext = createContext<SurfaceProps | null>(null);
|
|
80
|
+
|
|
81
|
+
const SurfaceResolver = (props: SurfaceProps) => {
|
|
82
|
+
const { components } = useSurface();
|
|
83
|
+
const parent = useContext(SurfaceContext);
|
|
84
|
+
const nodes = resolveNodes(components, props, parent);
|
|
85
|
+
const currentContext: SurfaceProps = {
|
|
86
|
+
...props,
|
|
87
|
+
surfaces: {
|
|
88
|
+
...((props.name && parent?.surfaces?.[props.name]?.surfaces) || {}),
|
|
89
|
+
...props.surfaces,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return <SurfaceContext.Provider value={currentContext}>{nodes}</SurfaceContext.Provider>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resolveNodes = (
|
|
97
|
+
components: Record<string, SurfaceComponent>,
|
|
98
|
+
props: SurfaceProps,
|
|
99
|
+
context: SurfaceProps | null,
|
|
100
|
+
): ReactNode[] => {
|
|
101
|
+
const data = {
|
|
102
|
+
...((props.name && context?.surfaces?.[props.name]?.data) || {}),
|
|
103
|
+
...props.data,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const nodes = Object.entries(components)
|
|
107
|
+
.map(([key, component]) => {
|
|
108
|
+
const result = component(data, props.role);
|
|
109
|
+
return result ? <Wrapper key={key}>{result}</Wrapper> : undefined;
|
|
110
|
+
})
|
|
111
|
+
.filter((Component): Component is JSX.Element => Boolean(Component));
|
|
112
|
+
|
|
113
|
+
return props.limit ? nodes.slice(0, props.limit) : nodes;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const Wrapper = ({ children }: PropsWithChildren) => <>{children}</>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { deepSignal, shallow } from 'deepsignal/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { type SurfaceComponent, SurfaceProvider, type SurfaceRootContext } from './SurfaceRootContext';
|
|
9
|
+
import type { Plugin, PluginDefinition } from '../PluginHost';
|
|
10
|
+
import { filterPlugins } from '../helpers';
|
|
11
|
+
|
|
12
|
+
export type SurfaceProvides = {
|
|
13
|
+
surface: {
|
|
14
|
+
/**
|
|
15
|
+
* Used by the `Surface` resolver to find a component to render.
|
|
16
|
+
*/
|
|
17
|
+
component: SurfaceComponent;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SurfacePluginProvides = {
|
|
22
|
+
surface: SurfaceRootContext;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const parseRootSurfacePlugin = (plugin?: Plugin) =>
|
|
26
|
+
(plugin?.provides as any)?.surface?.components ? (plugin as Plugin<SurfacePluginProvides>) : undefined;
|
|
27
|
+
|
|
28
|
+
export const parseSurfacePlugin = (plugin?: Plugin) =>
|
|
29
|
+
(plugin?.provides as any)?.surface?.component ? (plugin as Plugin<SurfaceProvides>) : undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provides a registry of surface components.
|
|
33
|
+
*/
|
|
34
|
+
export const SurfacePlugin = (): PluginDefinition<SurfacePluginProvides> => {
|
|
35
|
+
const state = deepSignal<SurfaceRootContext>({ components: {} });
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
meta: {
|
|
39
|
+
id: 'dxos.org/plugin/surface',
|
|
40
|
+
},
|
|
41
|
+
ready: async (plugins) => {
|
|
42
|
+
state.components = shallow(
|
|
43
|
+
filterPlugins(plugins, parseSurfacePlugin).reduce((acc, plugin) => {
|
|
44
|
+
return { ...acc, [plugin.meta.id]: plugin.provides.surface.component };
|
|
45
|
+
}, {}),
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
provides: {
|
|
49
|
+
surface: state,
|
|
50
|
+
context: ({ children }) => <SurfaceProvider value={state}>{children}</SurfaceProvider>,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext, useContext, type Context, type Provider, type ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Function which resolves a Surface.
|
|
9
|
+
*
|
|
10
|
+
* If a null value is returned, the rendering is deferred to other plugins.
|
|
11
|
+
*/
|
|
12
|
+
export type SurfaceComponent = (data: Record<string, unknown>, role?: string) => ReactNode;
|
|
13
|
+
|
|
14
|
+
export type SurfaceRootContext = {
|
|
15
|
+
components: Record<string, SurfaceComponent>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SurfaceRootContext: Context<SurfaceRootContext> = createContext<SurfaceRootContext>({ components: {} });
|
|
19
|
+
|
|
20
|
+
export const useSurface = () => useContext(SurfaceRootContext);
|
|
21
|
+
|
|
22
|
+
export const SurfaceProvider: Provider<SurfaceRootContext> = SurfaceRootContext.Provider;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Graph, Node } from '@dxos/app-graph';
|
|
6
|
+
import type { UnsubscribeCallback } from '@dxos/async';
|
|
7
|
+
|
|
8
|
+
import type { Plugin } from '../PluginHost';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides for a plugin that exposes the application graph.
|
|
12
|
+
*/
|
|
13
|
+
export type GraphPluginProvides = {
|
|
14
|
+
graph: Graph;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type GraphBuilderProvides = {
|
|
18
|
+
graph: {
|
|
19
|
+
builder: (params: { parent: Node; plugins: Plugin[] }) => UnsubscribeCallback | void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type guard for graph plugins.
|
|
25
|
+
*/
|
|
26
|
+
export const parseGraphPlugin = (plugin: Plugin) =>
|
|
27
|
+
(plugin.provides as any).graph?.root ? (plugin as Plugin<GraphPluginProvides>) : undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Type guard for graph builder plugins.
|
|
31
|
+
*/
|
|
32
|
+
export const parseGraphBuilderPlugin = (plugin: Plugin) =>
|
|
33
|
+
(plugin.provides as any).graph?.builder ? (plugin as Plugin<GraphBuilderProvides>) : undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
import type { Plugin } from '../PluginHost';
|
|
8
|
+
|
|
9
|
+
//
|
|
10
|
+
// Provides
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Basic state provided by a layout plugin.
|
|
15
|
+
*/
|
|
16
|
+
export const Layout = z.object({
|
|
17
|
+
fullscreen: z.boolean(),
|
|
18
|
+
sidebarOpen: z.boolean(),
|
|
19
|
+
complementarySidebarOpen: z.boolean(),
|
|
20
|
+
|
|
21
|
+
dialogContent: z.any().optional().describe('Data to be passed to the dialog Surface.'),
|
|
22
|
+
dialogOpen: z.boolean(),
|
|
23
|
+
|
|
24
|
+
popoverAnchorId: z.string().optional(),
|
|
25
|
+
popoverContent: z.any().optional().describe('Data to be passed to the popover Surface.'),
|
|
26
|
+
popoverOpen: z.boolean(),
|
|
27
|
+
|
|
28
|
+
// TODO(wittjosiah): Array?
|
|
29
|
+
active: z.string().optional().describe('Id of the currently active item.'),
|
|
30
|
+
// TODO(wittjosiah): History?
|
|
31
|
+
previous: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type Layout = z.infer<typeof Layout>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Provides for a plugin that can manage the app layout.
|
|
38
|
+
*/
|
|
39
|
+
export type LayoutProvides = {
|
|
40
|
+
layout: Readonly<Layout>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Type guard for layout plugins.
|
|
45
|
+
*/
|
|
46
|
+
export const parseLayoutPlugin = (plugin: Plugin) => {
|
|
47
|
+
const { success } = Layout.safeParse((plugin.provides as any).layout);
|
|
48
|
+
return success ? (plugin as Plugin<LayoutProvides>) : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//
|
|
52
|
+
// Intents
|
|
53
|
+
//
|
|
54
|
+
|
|
55
|
+
const LAYOUT_ACTION = 'dxos.org/plugin/intent';
|
|
56
|
+
// TODO(wittjosiah): Consider consolidating some action types (e.g. toggle).
|
|
57
|
+
export enum LayoutAction {
|
|
58
|
+
TOGGLE_FULLSCREEN = `${LAYOUT_ACTION}/toggle-fullscreen`,
|
|
59
|
+
TOGGLE_SIDEBAR = `${LAYOUT_ACTION}/toggle-sidebar`,
|
|
60
|
+
TOGGLE_COMPLEMENTARY_SIDEBAR = `${LAYOUT_ACTION}/toggle-complementary-sidebar`,
|
|
61
|
+
OPEN_DIALOG = `${LAYOUT_ACTION}/open-dialog`,
|
|
62
|
+
CLOSE_DIALOG = `${LAYOUT_ACTION}/close-dialog`,
|
|
63
|
+
OPEN_POPOVER = `${LAYOUT_ACTION}/open-popover`,
|
|
64
|
+
CLOSE_POPOVER = `${LAYOUT_ACTION}/close-popover`,
|
|
65
|
+
ACTIVATE = `${LAYOUT_ACTION}/activate`,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Expected payload for layout actions.
|
|
70
|
+
*/
|
|
71
|
+
export namespace LayoutAction {
|
|
72
|
+
export type ToggleFullscreen = {
|
|
73
|
+
state?: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ToggleSidebar = {
|
|
77
|
+
state?: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ToggleComplementarySidebar = {
|
|
81
|
+
state?: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type OpenDialog = {
|
|
85
|
+
component: string;
|
|
86
|
+
subject: any;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type CloseDialog = {};
|
|
90
|
+
|
|
91
|
+
export type OpenPopover = {
|
|
92
|
+
anchorId: string;
|
|
93
|
+
component: string;
|
|
94
|
+
subject: any;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ClosePopover = {};
|
|
98
|
+
|
|
99
|
+
export type Activate = {
|
|
100
|
+
id: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
import type { Plugin } from '../PluginHost';
|
|
8
|
+
|
|
9
|
+
export const ResourceKey = z.union([z.string(), z.record(z.any())]);
|
|
10
|
+
export type ResourceKey = z.infer<typeof ResourceKey>;
|
|
11
|
+
|
|
12
|
+
export const ResourceLanguage = z.record(ResourceKey);
|
|
13
|
+
export type ResourceLanguage = z.infer<typeof ResourceLanguage>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A resource is a collection of translations for a language.
|
|
17
|
+
*/
|
|
18
|
+
export const Resource = z.record(ResourceLanguage);
|
|
19
|
+
export type Resource = z.infer<typeof Resource>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Provides for a plugin that exposes translations.
|
|
23
|
+
*/
|
|
24
|
+
export type TranslationsProvides = {
|
|
25
|
+
translations: Readonly<Resource[]>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Type guard for translation plugins.
|
|
30
|
+
*/
|
|
31
|
+
export const parseTranslationsPlugin = (plugin: Plugin) => {
|
|
32
|
+
const { success } = z.array(Resource).safeParse((plugin.provides as any).translations);
|
|
33
|
+
return success ? (plugin as Plugin<TranslationsProvides>) : undefined;
|
|
34
|
+
};
|