@dxos/plugin-space 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/chunk-ImportSpaceDialog.mjs +2 -2
- package/dist/lib/chunk-ImportSpaceDialog.mjs.map +1 -1
- package/dist/lib/chunk-MembersContainer.mjs +8 -5
- package/dist/lib/chunk-MembersContainer.mjs.map +1 -1
- package/dist/lib/chunk-ObjectMasonry.mjs +1 -1
- package/dist/lib/chunk-ObjectMasonry.mjs.map +1 -1
- package/dist/lib/chunk-RecordArticle.mjs +4 -3
- package/dist/lib/chunk-RecordArticle.mjs.map +1 -1
- package/dist/lib/chunk-SpaceHomeDashboard.mjs +1 -1
- package/dist/lib/chunk-SpaceHomeDashboard.mjs.map +1 -1
- package/dist/lib/chunk-SpaceHomeRecent.mjs +41 -19
- package/dist/lib/chunk-SpaceHomeRecent.mjs.map +1 -1
- package/dist/lib/chunk-SyncStatus.mjs +2 -2
- package/dist/lib/chunk-SyncStatus.mjs.map +1 -1
- package/dist/lib/chunk-TypeArticle.mjs +1 -1
- package/dist/lib/chunk-TypeArticle.mjs.map +1 -1
- package/dist/lib/chunk-app-graph-builder.mjs +8 -4
- package/dist/lib/chunk-app-graph-builder.mjs.map +1 -1
- package/dist/lib/components.mjs +3 -2
- package/dist/lib/components.mjs.map +1 -1
- package/dist/lib/dashboard.mjs +3 -1
- package/dist/lib/dashboard.mjs.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder/extensions/database.d.ts.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts +2 -0
- package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts.map +1 -1
- package/dist/types/src/containers/MembersContainer/MembersContainer.d.ts.map +1 -1
- package/dist/types/src/containers/RecordArticle/RecordArticle.d.ts.map +1 -1
- package/dist/types/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.d.ts.map +1 -1
- package/dist/types/src/containers/SpaceHomeRecent/SpaceHomeRecent.d.ts.map +1 -1
- package/dist/types/src/dashboard/stats.d.ts +2 -0
- package/dist/types/src/dashboard/stats.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +62 -62
- package/src/capabilities/app-graph-builder/extensions/database.ts +5 -3
- package/src/capabilities/app-graph-builder/extensions/shared.test.ts +25 -1
- package/src/capabilities/app-graph-builder/extensions/shared.ts +6 -0
- package/src/components/CreateObjectPanel/CreateObjectPanel.tsx +2 -2
- package/src/containers/ImportSpaceDialog/ImportSpaceDialog.tsx +2 -2
- package/src/containers/MembersContainer/MembersContainer.tsx +3 -2
- package/src/containers/ObjectMasonry/ObjectMasonryArticle.tsx +1 -1
- package/src/containers/RecordArticle/RecordArticle.tsx +2 -5
- package/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.tsx +6 -1
- package/src/containers/SpaceHomeDashboard/SpaceHomeDashboard.tsx +1 -1
- package/src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx +71 -28
- package/src/containers/SyncStatus/SyncStatus.tsx +2 -2
- package/src/containers/TypeArticle/TypeArticle.tsx +1 -1
- package/src/dashboard/stats.test.ts +12 -2
- package/src/dashboard/stats.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/plugin-space",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "DXOS Surface plugin for DXOS ECHO Spaces",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -208,58 +208,58 @@
|
|
|
208
208
|
"dependencies": {
|
|
209
209
|
"@effect/atom-react": "4.0.0",
|
|
210
210
|
"react-drag-drop-files": "^2.3.8",
|
|
211
|
-
"@dxos/app-
|
|
212
|
-
"@dxos/app-toolkit": "0.
|
|
213
|
-
"@dxos/app-
|
|
214
|
-
"@dxos/async": "0.
|
|
215
|
-
"@dxos/cli-util": "0.
|
|
216
|
-
"@dxos/client": "0.
|
|
217
|
-
"@dxos/client-protocol": "0.
|
|
218
|
-
"@dxos/
|
|
219
|
-
"@dxos/
|
|
220
|
-
"@dxos/
|
|
221
|
-
"@dxos/echo-client": "0.
|
|
222
|
-
"@dxos/echo
|
|
223
|
-
"@dxos/echo-protocol": "0.
|
|
224
|
-
"@dxos/
|
|
225
|
-
"@dxos/
|
|
226
|
-
"@dxos/
|
|
227
|
-
"@dxos/extractor": "0.
|
|
228
|
-
"@dxos/
|
|
229
|
-
"@dxos/halo": "0.
|
|
230
|
-
"@dxos/
|
|
231
|
-
"@dxos/keys": "0.
|
|
232
|
-
"@dxos/
|
|
233
|
-
"@dxos/
|
|
234
|
-
"@dxos/
|
|
235
|
-
"@dxos/
|
|
236
|
-
"@dxos/plugin-
|
|
237
|
-
"@dxos/plugin-settings": "0.
|
|
238
|
-
"@dxos/
|
|
239
|
-
"@dxos/
|
|
240
|
-
"@dxos/
|
|
241
|
-
"@dxos/
|
|
242
|
-
"@dxos/
|
|
243
|
-
"@dxos/react-ui": "0.
|
|
244
|
-
"@dxos/react-ui-attention": "0.
|
|
245
|
-
"@dxos/react-ui-components": "0.
|
|
246
|
-
"@dxos/react-ui-dashboard": "0.
|
|
247
|
-
"@dxos/react-ui-dnd": "0.
|
|
248
|
-
"@dxos/react-ui-form": "0.
|
|
249
|
-
"@dxos/react-ui-list": "0.
|
|
250
|
-
"@dxos/react-ui-masonry": "0.
|
|
251
|
-
"@dxos/react-ui-mosaic": "0.
|
|
252
|
-
"@dxos/react-ui-
|
|
253
|
-
"@dxos/react-ui-
|
|
254
|
-
"@dxos/react-ui-
|
|
255
|
-
"@dxos/react-ui-
|
|
256
|
-
"@dxos/react-ui-table": "0.
|
|
257
|
-
"@dxos/types": "0.
|
|
258
|
-
"@dxos/schema": "0.
|
|
259
|
-
"@dxos/ui-theme": "0.
|
|
260
|
-
"@dxos/util": "0.
|
|
261
|
-
"@dxos/ui-types": "0.
|
|
262
|
-
"@dxos/shell": "0.
|
|
211
|
+
"@dxos/app-graph": "0.14.0",
|
|
212
|
+
"@dxos/app-toolkit": "0.14.0",
|
|
213
|
+
"@dxos/app-framework": "0.14.0",
|
|
214
|
+
"@dxos/async": "0.14.0",
|
|
215
|
+
"@dxos/cli-util": "0.14.0",
|
|
216
|
+
"@dxos/client": "0.14.0",
|
|
217
|
+
"@dxos/client-protocol": "0.14.0",
|
|
218
|
+
"@dxos/context": "0.14.0",
|
|
219
|
+
"@dxos/compute": "0.14.0",
|
|
220
|
+
"@dxos/display-name": "0.14.0",
|
|
221
|
+
"@dxos/echo-client": "0.14.0",
|
|
222
|
+
"@dxos/echo": "0.14.0",
|
|
223
|
+
"@dxos/echo-protocol": "0.14.0",
|
|
224
|
+
"@dxos/echo-react": "0.14.0",
|
|
225
|
+
"@dxos/effect": "0.14.0",
|
|
226
|
+
"@dxos/errors": "0.14.0",
|
|
227
|
+
"@dxos/extractor": "0.14.0",
|
|
228
|
+
"@dxos/halo": "0.14.0",
|
|
229
|
+
"@dxos/halo-react": "0.14.0",
|
|
230
|
+
"@dxos/log": "0.14.0",
|
|
231
|
+
"@dxos/keys": "0.14.0",
|
|
232
|
+
"@dxos/graph": "0.14.0",
|
|
233
|
+
"@dxos/migrations": "0.14.0",
|
|
234
|
+
"@dxos/plugin-attention": "0.14.0",
|
|
235
|
+
"@dxos/plugin-client": "0.14.0",
|
|
236
|
+
"@dxos/plugin-graph": "0.14.0",
|
|
237
|
+
"@dxos/plugin-settings": "0.14.0",
|
|
238
|
+
"@dxos/progress": "0.14.0",
|
|
239
|
+
"@dxos/invariant": "0.14.0",
|
|
240
|
+
"@dxos/plugin-status-bar": "0.14.0",
|
|
241
|
+
"@dxos/react-client": "0.14.0",
|
|
242
|
+
"@dxos/protocols": "0.14.0",
|
|
243
|
+
"@dxos/react-ui": "0.14.0",
|
|
244
|
+
"@dxos/react-ui-attention": "0.14.0",
|
|
245
|
+
"@dxos/react-ui-components": "0.14.0",
|
|
246
|
+
"@dxos/react-ui-dashboard": "0.14.0",
|
|
247
|
+
"@dxos/react-ui-dnd": "0.14.0",
|
|
248
|
+
"@dxos/react-ui-form": "0.14.0",
|
|
249
|
+
"@dxos/react-ui-list": "0.14.0",
|
|
250
|
+
"@dxos/react-ui-masonry": "0.14.0",
|
|
251
|
+
"@dxos/react-ui-mosaic": "0.14.0",
|
|
252
|
+
"@dxos/react-ui-pickers": "0.14.0",
|
|
253
|
+
"@dxos/react-ui-query": "0.14.0",
|
|
254
|
+
"@dxos/react-ui-search": "0.14.0",
|
|
255
|
+
"@dxos/react-ui-menu": "0.14.0",
|
|
256
|
+
"@dxos/react-ui-table": "0.14.0",
|
|
257
|
+
"@dxos/types": "0.14.0",
|
|
258
|
+
"@dxos/schema": "0.14.0",
|
|
259
|
+
"@dxos/ui-theme": "0.14.0",
|
|
260
|
+
"@dxos/util": "0.14.0",
|
|
261
|
+
"@dxos/ui-types": "0.14.0",
|
|
262
|
+
"@dxos/shell": "0.14.0"
|
|
263
263
|
},
|
|
264
264
|
"devDependencies": {
|
|
265
265
|
"@effect/atom-react": "4.0.0",
|
|
@@ -271,21 +271,21 @@
|
|
|
271
271
|
"react": "~19.2.7",
|
|
272
272
|
"react-dom": "~19.2.7",
|
|
273
273
|
"vite": "^8.2.2",
|
|
274
|
-
"@dxos/
|
|
275
|
-
"@dxos/
|
|
276
|
-
"@dxos/plugin-
|
|
277
|
-
"@dxos/
|
|
278
|
-
"@dxos/
|
|
279
|
-
"@dxos/ui
|
|
280
|
-
"@dxos/
|
|
274
|
+
"@dxos/agent-runtime": "0.14.0",
|
|
275
|
+
"@dxos/extractor-lib": "0.14.0",
|
|
276
|
+
"@dxos/plugin-preview": "0.14.0",
|
|
277
|
+
"@dxos/plugin-testing": "0.14.0",
|
|
278
|
+
"@dxos/random": "0.14.0",
|
|
279
|
+
"@dxos/react-ui": "0.14.0",
|
|
280
|
+
"@dxos/ui-theme": "0.14.0"
|
|
281
281
|
},
|
|
282
282
|
"peerDependencies": {
|
|
283
283
|
"@effect/atom-react": "4.0.0",
|
|
284
284
|
"effect": "4.0.0",
|
|
285
285
|
"react": "~19.2.7",
|
|
286
286
|
"react-dom": "~19.2.7",
|
|
287
|
-
"@dxos/react-ui": "^0.
|
|
288
|
-
"@dxos/ui-theme": "^0.
|
|
287
|
+
"@dxos/react-ui": "^0.14.0",
|
|
288
|
+
"@dxos/ui-theme": "^0.14.0"
|
|
289
289
|
},
|
|
290
290
|
"publishConfig": {
|
|
291
291
|
"access": "public"
|
|
@@ -28,6 +28,7 @@ import { ViewAnnotation } from '@dxos/schema';
|
|
|
28
28
|
import { isLabel, toLocalizedString } from '@dxos/ui-types/translations';
|
|
29
29
|
import { createFilename, downloadBlob, isNonNullable } from '@dxos/util';
|
|
30
30
|
|
|
31
|
+
import { SPACE_STATS_QUERY, typeUrisWithObjects } from '#dashboard';
|
|
31
32
|
import { meta } from '#meta';
|
|
32
33
|
import { SpaceCapabilities, SpaceEvents, SpaceOperation } from '#types';
|
|
33
34
|
|
|
@@ -121,17 +122,18 @@ export const createDatabaseExtensions = Effect.fnUntraced(function* () {
|
|
|
121
122
|
});
|
|
122
123
|
|
|
123
124
|
const viewIndex = buildViewIndex(get, space, allSchemas);
|
|
125
|
+
// Index-only per-type counts; the same query as the space dashboard, so both share one result.
|
|
126
|
+
const withObjects = typeUrisWithObjects(get(space.db.query(SPACE_STATS_QUERY).atom));
|
|
124
127
|
|
|
125
128
|
const visibleSchemas = userSchemas.filter((schema) => {
|
|
126
129
|
if (Type.getDatabase(schema) != null) {
|
|
127
130
|
return true;
|
|
128
131
|
}
|
|
129
132
|
const typeUri = Type.getURI(schema);
|
|
130
|
-
const objects = get(space.db.query(Filter.type(typeUri)).atom);
|
|
131
133
|
if (ViewAnnotation.has(schema)) {
|
|
132
|
-
return
|
|
134
|
+
return viewIndex.typeUrisWithNonViewObjects.has(typeUri);
|
|
133
135
|
}
|
|
134
|
-
return
|
|
136
|
+
return withObjects.has(typeUri) || viewIndex.typeUrisWithViews.has(typeUri);
|
|
135
137
|
});
|
|
136
138
|
|
|
137
139
|
// Sort alphabetically by display name. Static types' labels are `typename.label` translation
|
|
@@ -28,6 +28,12 @@ const TestViewWrapper = Type.makeObject(DXN.make('com.example.type.viewWrapper',
|
|
|
28
28
|
}).pipe(ViewAnnotation.set(['view'])),
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
+
const TestOptionalViewWrapper = Type.makeObject(DXN.make('com.example.type.optionalViewWrapper', '0.1.0'))(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
view: Schema.optional(Ref.Ref(View.View)),
|
|
34
|
+
}).pipe(ViewAnnotation.set(['view'])),
|
|
35
|
+
);
|
|
36
|
+
|
|
31
37
|
describe('buildViewIndex', () => {
|
|
32
38
|
let testBuilder: EchoTestBuilder;
|
|
33
39
|
let db: EchoDatabase;
|
|
@@ -35,7 +41,9 @@ describe('buildViewIndex', () => {
|
|
|
35
41
|
|
|
36
42
|
beforeEach(async () => {
|
|
37
43
|
testBuilder = await new EchoTestBuilder().open();
|
|
38
|
-
const result = await testBuilder.createDatabase({
|
|
44
|
+
const result = await testBuilder.createDatabase({
|
|
45
|
+
types: [TestContact, TestViewWrapper, TestOptionalViewWrapper, View.View],
|
|
46
|
+
});
|
|
39
47
|
db = result.db;
|
|
40
48
|
registry = AtomRegistry.make();
|
|
41
49
|
});
|
|
@@ -101,6 +109,22 @@ describe('buildViewIndex', () => {
|
|
|
101
109
|
expect(views[0].id).toBe(tableView.id);
|
|
102
110
|
});
|
|
103
111
|
|
|
112
|
+
test('typeUrisWithNonViewObjects lists view types with an instance that is not a view', async ({ expect }) => {
|
|
113
|
+
const viewObj = db.add(View.make({ query: { ast: Query.select(Filter.type(TestContact)).ast } }));
|
|
114
|
+
db.add(Obj.make(TestViewWrapper, { view: Ref.make(viewObj) }));
|
|
115
|
+
db.add(Obj.make(TestOptionalViewWrapper, {}));
|
|
116
|
+
await db.flush();
|
|
117
|
+
|
|
118
|
+
const allSchemas = db.graph.registry
|
|
119
|
+
.list()
|
|
120
|
+
.filter(Type.isType)
|
|
121
|
+
.filter((t) => !Type.isTypeKind(t));
|
|
122
|
+
const viewIndex = buildViewIndex(registry.get.bind(registry) as any, { db } as any, allSchemas);
|
|
123
|
+
|
|
124
|
+
expect(viewIndex.typeUrisWithNonViewObjects.has(Type.getURI(TestOptionalViewWrapper))).toBe(true);
|
|
125
|
+
expect(viewIndex.typeUrisWithNonViewObjects.has(Type.getURI(TestViewWrapper))).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
104
128
|
test('returns empty index when no view schemas exist', async ({ expect }) => {
|
|
105
129
|
await db.flush();
|
|
106
130
|
|
|
@@ -104,6 +104,8 @@ export type ViewIndex = {
|
|
|
104
104
|
getViewsForTypeUri: (typeUri: string) => Obj.Any[];
|
|
105
105
|
/** True when the schema has `ViewAnnotation` and that path resolves non-null on `object`; false otherwise (no annotation, or null/undefined path — e.g. Kanban with unset `view`). */
|
|
106
106
|
isView: (object: Obj.Any) => boolean;
|
|
107
|
+
/** View-annotated type URIs with at least one instance that is not a view. */
|
|
108
|
+
typeUrisWithNonViewObjects: Set<string>;
|
|
107
109
|
};
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -120,6 +122,7 @@ export const buildViewIndex = (get: Atom.AtomContext, space: Space, schemas: Typ
|
|
|
120
122
|
// Used by `isView` to distinguish view instances from regular instances of
|
|
121
123
|
// the same schema (e.g. items-variant Kanban vs view-variant Kanban).
|
|
122
124
|
const viewObjectIds = new Set<string>();
|
|
125
|
+
const typeUrisWithNonViewObjects = new Set<string>();
|
|
123
126
|
|
|
124
127
|
if (viewSchemas.length > 0) {
|
|
125
128
|
const filter = Filter.or(...viewSchemas.map((schema) => Filter.type(schema)));
|
|
@@ -149,6 +152,8 @@ export const buildViewIndex = (get: Atom.AtomContext, space: Space, schemas: Typ
|
|
|
149
152
|
// be of a view-annotated schema.
|
|
150
153
|
if (holder != null) {
|
|
151
154
|
viewObjectIds.add(viewObject.id);
|
|
155
|
+
} else if (holderType) {
|
|
156
|
+
typeUrisWithNonViewObjects.add(Type.getURI(holderType));
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
const viewTarget = holder != null ? get(Obj.atom(holder as Obj.Any)) : undefined;
|
|
@@ -165,5 +170,6 @@ export const buildViewIndex = (get: Atom.AtomContext, space: Space, schemas: Typ
|
|
|
165
170
|
typeUrisWithViews: new Set(viewsByTypeUri.keys()),
|
|
166
171
|
getViewsForTypeUri: (typeUri) => viewsByTypeUri.get(typeUri) ?? [],
|
|
167
172
|
isView: (object) => viewObjectIds.has(object.id),
|
|
173
|
+
typeUrisWithNonViewObjects,
|
|
168
174
|
};
|
|
169
175
|
};
|
|
@@ -274,14 +274,14 @@ const SelectType = ({ options, onChange }: SelectTypeProps) => {
|
|
|
274
274
|
size='xl'
|
|
275
275
|
classNames={getIconHueStyles(option.iconHue)}
|
|
276
276
|
/>
|
|
277
|
-
<
|
|
277
|
+
<Layout.Flex column classNames='min-w-0 grow gap-0.5'>
|
|
278
278
|
<span className='truncate'>{option.label}</span>
|
|
279
279
|
{(option.plugin || option.description) && (
|
|
280
280
|
<span className='truncate text-fg-muted text-xs'>
|
|
281
281
|
{option.plugin ? t('plugin-subtitle.label', { plugin: option.plugin }) : option.description}
|
|
282
282
|
</span>
|
|
283
283
|
)}
|
|
284
|
-
</
|
|
284
|
+
</Layout.Flex>
|
|
285
285
|
</Picker.Item>
|
|
286
286
|
))}
|
|
287
287
|
</SearchList.Viewport>
|
|
@@ -78,7 +78,7 @@ export const ImportSpaceDialog = () => {
|
|
|
78
78
|
gap='sm'
|
|
79
79
|
asChild
|
|
80
80
|
role='status'
|
|
81
|
-
classNames='my-4 p-8 border-2 border-dashed border-
|
|
81
|
+
classNames='my-4 p-8 border-2 border-dashed border-separator rounded-sm'
|
|
82
82
|
>
|
|
83
83
|
<div>
|
|
84
84
|
<Icon.Icon icon='ph--spinner-gap--regular' size='xl' spin />
|
|
@@ -88,7 +88,7 @@ export const ImportSpaceDialog = () => {
|
|
|
88
88
|
) : (
|
|
89
89
|
<FileUploader
|
|
90
90
|
types={['json', 'tar']}
|
|
91
|
-
classes='block my-4 p-8 border-2 border-dashed border-
|
|
91
|
+
classes='block my-4 p-8 border-2 border-dashed border-separator rounded-sm flex items-center justify-center gap-2 cursor-pointer'
|
|
92
92
|
dropMessageStyle={{ border: 'none', backgroundColor: '#EEE' }}
|
|
93
93
|
handleChange={(file: File) => {
|
|
94
94
|
void handleFile(file);
|
|
@@ -27,6 +27,7 @@ import { Form } from '@dxos/react-ui-form';
|
|
|
27
27
|
import * as Button from '@dxos/react-ui/Button';
|
|
28
28
|
import * as UiHooks from '@dxos/react-ui/Hooks';
|
|
29
29
|
import * as Icon from '@dxos/react-ui/Icon';
|
|
30
|
+
import * as Layout from '@dxos/react-ui/Layout';
|
|
30
31
|
import * as QrCode from '@dxos/react-ui/QrCode';
|
|
31
32
|
import * as SystemButton from '@dxos/react-ui/SystemButton';
|
|
32
33
|
import {
|
|
@@ -296,7 +297,7 @@ const InvitationQR = ({ id, url, onCancel }: { id: string; url: string; onCancel
|
|
|
296
297
|
return (
|
|
297
298
|
<>
|
|
298
299
|
<p className='text-fg-muted'>{t('qr-code.description', { ns: meta.profile.key })}</p>
|
|
299
|
-
<
|
|
300
|
+
<Layout.Grid role='group' cols={['fill', 'min']} gap='sm' classNames='my-2'>
|
|
300
301
|
<div className='w-full aspect-square relative text-fg-muted'>
|
|
301
302
|
<QrCode.QrCode aria-labelledby={qrLabel} errorCorrection='Q' value={url ?? 'never'} />
|
|
302
303
|
<Centered>
|
|
@@ -307,7 +308,7 @@ const InvitationQR = ({ id, url, onCancel }: { id: string; url: string; onCancel
|
|
|
307
308
|
{t('qr.label')}
|
|
308
309
|
</span>
|
|
309
310
|
<SystemButton.Clipboard value={url ?? 'never'} />
|
|
310
|
-
</
|
|
311
|
+
</Layout.Grid>
|
|
311
312
|
<Button.Root variant='ghost' onClick={onCancel}>
|
|
312
313
|
{t('cancel.label')}
|
|
313
314
|
</Button.Root>
|
|
@@ -110,7 +110,7 @@ export const ObjectMasonryArticle = ({ role, attendableId, objects, emptyMessage
|
|
|
110
110
|
<ObjectMasonry cacheKey={attendableId} items={items} />
|
|
111
111
|
)}
|
|
112
112
|
</Panel.Body>
|
|
113
|
-
<Panel.Footer classNames='
|
|
113
|
+
<Panel.Footer classNames='items-center p-1 border-t border-separator-subtle'>
|
|
114
114
|
{t('item-count.label', { count: items.length })}
|
|
115
115
|
</Panel.Footer>
|
|
116
116
|
</Panel.Root>
|
|
@@ -27,7 +27,6 @@ import * as Hooks from '@dxos/react-ui/Hooks';
|
|
|
27
27
|
import * as Layout from '@dxos/react-ui/Layout';
|
|
28
28
|
import * as Panel from '@dxos/react-ui/Panel';
|
|
29
29
|
import * as ScrollArea from '@dxos/react-ui/ScrollArea';
|
|
30
|
-
import { mx } from '@dxos/ui-theme';
|
|
31
30
|
|
|
32
31
|
import { ObjectCard, RelatedTypeFilter } from '#components';
|
|
33
32
|
import { useRelatedObjects, useRelatedTypeFilter } from '#hooks';
|
|
@@ -83,9 +82,7 @@ export const RecordArticle = ({ role, subject, attendableId }: AppSurface.Object
|
|
|
83
82
|
|
|
84
83
|
{/* Gated on the unfiltered set so hiding every type does not remove the filter itself. */}
|
|
85
84
|
{relatedObjects.length > 0 && (
|
|
86
|
-
<
|
|
87
|
-
className={mx('dx-expand flex flex-col gap-form-gap', singleColumn ? 'dx-card-max-width' : 'w-full')}
|
|
88
|
-
>
|
|
85
|
+
<Layout.Flex column gap='form' classNames={['dx-expand', singleColumn ? 'dx-card-max-width' : 'w-full']}>
|
|
89
86
|
<Field.Root>
|
|
90
87
|
<Field.Label>{t('related-objects.label')}</Field.Label>
|
|
91
88
|
</Field.Root>
|
|
@@ -101,7 +98,7 @@ export const RecordArticle = ({ role, subject, attendableId }: AppSurface.Object
|
|
|
101
98
|
<Masonry.Viewport items={related} />
|
|
102
99
|
</Masonry.Content>
|
|
103
100
|
</Masonry.Root>
|
|
104
|
-
</
|
|
101
|
+
</Layout.Flex>
|
|
105
102
|
)}
|
|
106
103
|
</ScrollArea.Viewport>
|
|
107
104
|
</ScrollArea.Root>
|
|
@@ -32,13 +32,18 @@ class Task extends Type.makeObject<Task>(DXN.make('org.dxos.type.test.task', '0.
|
|
|
32
32
|
}).pipe(
|
|
33
33
|
Annotation.LabelAnnotation.set(['name']),
|
|
34
34
|
Annotation.IconAnnotation.set({ icon: 'ph--check-square--regular' }),
|
|
35
|
+
Annotation.UserType.set(),
|
|
35
36
|
),
|
|
36
37
|
) {}
|
|
37
38
|
|
|
38
39
|
class Note extends Type.makeObject<Note>(DXN.make('org.dxos.type.test.note', '0.1.0'))(
|
|
39
40
|
Schema.Struct({
|
|
40
41
|
content: Schema.optional(Schema.String),
|
|
41
|
-
}).pipe(
|
|
42
|
+
}).pipe(
|
|
43
|
+
Annotation.LabelAnnotation.set(['content']),
|
|
44
|
+
Annotation.IconAnnotation.set({ icon: 'ph--note--regular' }),
|
|
45
|
+
Annotation.UserType.set(),
|
|
46
|
+
),
|
|
42
47
|
) {}
|
|
43
48
|
|
|
44
49
|
const OBJECT_COUNT = 24;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Array from 'effect/Array';
|
|
5
6
|
import React, { useCallback, useMemo } from 'react';
|
|
6
7
|
|
|
7
8
|
import * as Hooks from '@dxos/app-framework/Hooks';
|
|
@@ -10,7 +11,7 @@ import * as GraphPath from '@dxos/app-toolkit/GraphPath';
|
|
|
10
11
|
import * as HomeSection from '@dxos/app-toolkit/HomeSection';
|
|
11
12
|
import * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';
|
|
12
13
|
import * as TypeOptions from '@dxos/app-toolkit/TypeOptions';
|
|
13
|
-
import { Collection, Filter, Obj, Order, Query, Type } from '@dxos/echo';
|
|
14
|
+
import { Aggregate, Collection, Filter, Obj, Order, Query, Type } from '@dxos/echo';
|
|
14
15
|
import { useQuery } from '@dxos/echo-react';
|
|
15
16
|
import { type Space } from '@dxos/react-client/echo';
|
|
16
17
|
import { Masonry } from '@dxos/react-ui-masonry';
|
|
@@ -18,6 +19,7 @@ import * as Card from '@dxos/react-ui/Card';
|
|
|
18
19
|
import * as UiHooks from '@dxos/react-ui/Hooks';
|
|
19
20
|
import * as Icon from '@dxos/react-ui/Icon';
|
|
20
21
|
import * as Layout from '@dxos/react-ui/Layout';
|
|
22
|
+
import * as Status from '@dxos/react-ui/Status';
|
|
21
23
|
import * as Theme from '@dxos/react-ui/Theme';
|
|
22
24
|
import { getStyles } from '@dxos/ui-theme';
|
|
23
25
|
|
|
@@ -31,51 +33,92 @@ type SpaceScopedProps = {
|
|
|
31
33
|
onClose?: () => void;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
/**
|
|
35
|
-
* Recent-objects region for the Home article. Queries the most-recently-modified objects of
|
|
36
|
-
* registered, non-hidden, non-relation, non-collection types and renders them as a Masonry of
|
|
37
|
-
* tiles. Renders nothing (no heading) when the space has no recent objects — the starter-prompt
|
|
38
|
-
* contributor (plugin-assistant) fills the empty state instead.
|
|
39
|
-
*/
|
|
40
36
|
export const SpaceHomeRecent = ({ space, onClose }: SpaceScopedProps) => {
|
|
41
37
|
const { t } = UiHooks.useTranslation(meta.profile.key);
|
|
42
38
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const collectionTypename = Type.getTypename(Collection.Collection);
|
|
46
|
-
const types = schemas
|
|
47
|
-
.flat()
|
|
48
|
-
.filter(Type.isType)
|
|
49
|
-
.filter((type) => TypeOptions.isUserType(type))
|
|
50
|
-
.filter((type) => Type.getTypename(type) !== collectionTypename);
|
|
51
|
-
return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : undefined;
|
|
52
|
-
}, [schemas]);
|
|
53
|
-
|
|
54
|
-
const query = useMemo(
|
|
39
|
+
const { recent, placeholderCount } = useRecentObjects(space);
|
|
40
|
+
const items = useMemo<RecentItem[]>(
|
|
55
41
|
() =>
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
[
|
|
42
|
+
placeholderCount > 0
|
|
43
|
+
? Array.makeBy(placeholderCount, (index) => new Placeholder(`placeholder-${index}`))
|
|
44
|
+
: recent,
|
|
45
|
+
[placeholderCount, recent],
|
|
60
46
|
);
|
|
61
|
-
|
|
62
|
-
const recent = useQuery(filter && space ? space.db : undefined, query);
|
|
63
|
-
if (recent.length === 0) {
|
|
47
|
+
if (recent.length === 0 && placeholderCount === 0) {
|
|
64
48
|
return null;
|
|
65
49
|
}
|
|
66
50
|
|
|
67
51
|
return (
|
|
68
52
|
<HomeSection.Root>
|
|
69
53
|
<HomeSection.Header title={t('space-home.recent.heading')} onClose={onClose} />
|
|
70
|
-
<Masonry.Root Tile={
|
|
54
|
+
<Masonry.Root Tile={RecentTile}>
|
|
71
55
|
<Masonry.Content padding={false} scrollbars={false}>
|
|
72
|
-
<Masonry.Viewport items={
|
|
56
|
+
<Masonry.Viewport items={items} getId={(item) => item.id} cacheKey={space && `${space.id}/recent`} />
|
|
73
57
|
</Masonry.Content>
|
|
74
58
|
</Masonry.Root>
|
|
75
59
|
</HomeSection.Root>
|
|
76
60
|
);
|
|
77
61
|
};
|
|
78
62
|
|
|
63
|
+
const useRecentObjects = (space?: Space): { recent: Obj.Unknown[]; placeholderCount: number } => {
|
|
64
|
+
const schemas = Hooks.useCapabilities(AppCapabilities.Schema);
|
|
65
|
+
const filter = useMemo(() => recentObjectsFilter(schemas.flat()), [schemas]);
|
|
66
|
+
const query = useMemo(
|
|
67
|
+
() =>
|
|
68
|
+
Query.select(filter ?? Filter.everything())
|
|
69
|
+
.orderBy(Order.updated('desc'))
|
|
70
|
+
.limit(RECENT_LIMIT),
|
|
71
|
+
[filter],
|
|
72
|
+
);
|
|
73
|
+
const countQuery = useMemo(
|
|
74
|
+
() => Query.select(filter ?? Filter.everything()).aggregate({ count: Aggregate.count() }),
|
|
75
|
+
[filter],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const db = filter && space ? space.db : undefined;
|
|
79
|
+
const recent = useQuery(db, query);
|
|
80
|
+
const [total] = useQuery(db, countQuery);
|
|
81
|
+
return { recent, placeholderCount: recent.length === 0 ? Math.min(total?.count ?? 0, RECENT_LIMIT) : 0 };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const recentObjectsFilter = (schemas: readonly unknown[]) => {
|
|
85
|
+
const collectionTypename = Type.getTypename(Collection.Collection);
|
|
86
|
+
const types = Array.dedupeWith(
|
|
87
|
+
schemas
|
|
88
|
+
.filter(Type.isType)
|
|
89
|
+
.filter((type) => TypeOptions.isUserType(type))
|
|
90
|
+
.filter((type) => Type.getTypename(type) !== collectionTypename),
|
|
91
|
+
(a, b) => Type.getURI(a) === Type.getURI(b),
|
|
92
|
+
);
|
|
93
|
+
return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
class Placeholder {
|
|
97
|
+
constructor(readonly id: string) {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type RecentItem = Obj.Unknown | Placeholder;
|
|
101
|
+
|
|
102
|
+
const RecentTile = ({ data, index }: { data: RecentItem; index: number }) =>
|
|
103
|
+
data instanceof Placeholder ? <PlaceholderTile /> : <RecentObjectTile data={data} index={index} />;
|
|
104
|
+
|
|
105
|
+
RecentTile.displayName = 'RecentTile';
|
|
106
|
+
|
|
107
|
+
const PlaceholderTile = () => (
|
|
108
|
+
<Card.Root>
|
|
109
|
+
<Card.Header>
|
|
110
|
+
<Layout.Block>
|
|
111
|
+
<Status.Skeleton variant='circle' />
|
|
112
|
+
</Layout.Block>
|
|
113
|
+
<Card.Title>
|
|
114
|
+
<Status.Skeleton variant='text' />
|
|
115
|
+
</Card.Title>
|
|
116
|
+
</Card.Header>
|
|
117
|
+
</Card.Root>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
PlaceholderTile.displayName = 'PlaceholderTile';
|
|
121
|
+
|
|
79
122
|
const RecentObjectTile = ({ data }: { data: Obj.Unknown; index: number }) => {
|
|
80
123
|
const { invokePromise } = Hooks.useOperationInvoker();
|
|
81
124
|
const { t } = UiHooks.useTranslation(meta.profile.key);
|
|
@@ -112,7 +112,7 @@ const EdgeConnectionPopover = ({ status }: { status: EdgeStatus }) => {
|
|
|
112
112
|
|
|
113
113
|
{/* Upload Speed */}
|
|
114
114
|
<Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>
|
|
115
|
-
<Icon.Icon icon='ph--arrow-up--regular' classNames='text-green-
|
|
115
|
+
<Icon.Icon icon='ph--arrow-up--regular' classNames='text-green-text' />
|
|
116
116
|
<span className='text-fg-muted'>{t('sync-upload.label')}</span>
|
|
117
117
|
<UnitValue value={status.messagesSent} format={Unit.Thousand} />
|
|
118
118
|
<UnitValue value={status.rateBytesUp} format={Unit.Kilobyte} suffix='/s' />
|
|
@@ -120,7 +120,7 @@ const EdgeConnectionPopover = ({ status }: { status: EdgeStatus }) => {
|
|
|
120
120
|
|
|
121
121
|
{/* Download Speed */}
|
|
122
122
|
<Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>
|
|
123
|
-
<Icon.Icon icon='ph--arrow-down--regular' classNames='text-orange-
|
|
123
|
+
<Icon.Icon icon='ph--arrow-down--regular' classNames='text-orange-text' />
|
|
124
124
|
<span className='text-fg-muted'>{t('sync-download.label')}</span>
|
|
125
125
|
<UnitValue value={status.messagesReceived} format={Unit.Thousand} />
|
|
126
126
|
<UnitValue value={status.rateBytesDown} format={Unit.Kilobyte} suffix='/s' />
|
|
@@ -316,7 +316,7 @@ export const TypeArticle = ({ role, space, type, attendableId }: TypeArticleProp
|
|
|
316
316
|
</LayoutPanel>
|
|
317
317
|
)}
|
|
318
318
|
</Panel.Body>
|
|
319
|
-
<Panel.Footer classNames='
|
|
319
|
+
<Panel.Footer classNames='items-center p-1 border-t border-separator-subtle'>
|
|
320
320
|
{t('item-count.label', { count: tileItems.length })}
|
|
321
321
|
</Panel.Footer>
|
|
322
322
|
</Panel.Root>
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { Feed, Obj } from '@dxos/echo';
|
|
7
|
+
import { Feed, Obj, Type } from '@dxos/echo';
|
|
8
8
|
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
9
9
|
import { TestSchema } from '@dxos/echo/testing';
|
|
10
10
|
|
|
11
|
-
import { SPACE_STATS_QUERY, toSpaceStats } from './stats.ts';
|
|
11
|
+
import { SPACE_STATS_QUERY, toSpaceStats, typeUrisWithObjects } from './stats.ts';
|
|
12
12
|
|
|
13
13
|
describe('toSpaceStats', () => {
|
|
14
14
|
let builder: EchoTestBuilder;
|
|
@@ -32,6 +32,16 @@ describe('toSpaceStats', () => {
|
|
|
32
32
|
expect(toSpaceStats(rows, 7)).toEqual({ objects: 3, feeds: 2, types: 2, plugins: 7 });
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
test('typeUrisWithObjects names each type with objects by its Type.getURI', async ({ expect }) => {
|
|
36
|
+
const { db } = await builder.createDatabase({ types: [Feed.Feed, TestSchema.Expando] });
|
|
37
|
+
db.add(Obj.make(TestSchema.Expando, { value: 1 }));
|
|
38
|
+
await db.flush({ indexes: true });
|
|
39
|
+
|
|
40
|
+
const uris = typeUrisWithObjects(await db.query(SPACE_STATS_QUERY).run());
|
|
41
|
+
expect(uris.has(Type.getURI(TestSchema.Expando))).toBe(true);
|
|
42
|
+
expect(uris.has(Type.getURI(Feed.Feed))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
35
45
|
test('two schema versions of one type count as one type', ({ expect }) => {
|
|
36
46
|
const rows = [
|
|
37
47
|
{ type: 'dxn:example.com.type.task:0.1.0', count: 2 },
|
package/src/dashboard/stats.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const SPACE_STATS_QUERY = Query.select(Filter.everything()).aggregate({
|
|
|
24
24
|
count: Aggregate.count(),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
/** Type URIs with at least one live object, in the form `Type.getURI` returns. */
|
|
28
|
+
export const typeUrisWithObjects = (rows: readonly TypeCount[]): Set<string> =>
|
|
29
|
+
new Set(rows.flatMap((row) => (row.type !== null && row.count > 0 ? [row.type] : [])));
|
|
30
|
+
|
|
27
31
|
/** The versionless typename a stored type URI names, or the URI itself when it is not a DXN. */
|
|
28
32
|
export const typenameOf = (type: string): string => {
|
|
29
33
|
const dxn = DXN.tryMake(type);
|