@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.
Files changed (48) hide show
  1. package/dist/lib/chunk-ImportSpaceDialog.mjs +2 -2
  2. package/dist/lib/chunk-ImportSpaceDialog.mjs.map +1 -1
  3. package/dist/lib/chunk-MembersContainer.mjs +8 -5
  4. package/dist/lib/chunk-MembersContainer.mjs.map +1 -1
  5. package/dist/lib/chunk-ObjectMasonry.mjs +1 -1
  6. package/dist/lib/chunk-ObjectMasonry.mjs.map +1 -1
  7. package/dist/lib/chunk-RecordArticle.mjs +4 -3
  8. package/dist/lib/chunk-RecordArticle.mjs.map +1 -1
  9. package/dist/lib/chunk-SpaceHomeDashboard.mjs +1 -1
  10. package/dist/lib/chunk-SpaceHomeDashboard.mjs.map +1 -1
  11. package/dist/lib/chunk-SpaceHomeRecent.mjs +41 -19
  12. package/dist/lib/chunk-SpaceHomeRecent.mjs.map +1 -1
  13. package/dist/lib/chunk-SyncStatus.mjs +2 -2
  14. package/dist/lib/chunk-SyncStatus.mjs.map +1 -1
  15. package/dist/lib/chunk-TypeArticle.mjs +1 -1
  16. package/dist/lib/chunk-TypeArticle.mjs.map +1 -1
  17. package/dist/lib/chunk-app-graph-builder.mjs +8 -4
  18. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -1
  19. package/dist/lib/components.mjs +3 -2
  20. package/dist/lib/components.mjs.map +1 -1
  21. package/dist/lib/dashboard.mjs +3 -1
  22. package/dist/lib/dashboard.mjs.map +1 -1
  23. package/dist/types/src/capabilities/app-graph-builder/extensions/database.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts +2 -0
  25. package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts.map +1 -1
  26. package/dist/types/src/containers/MembersContainer/MembersContainer.d.ts.map +1 -1
  27. package/dist/types/src/containers/RecordArticle/RecordArticle.d.ts.map +1 -1
  28. package/dist/types/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.d.ts.map +1 -1
  29. package/dist/types/src/containers/SpaceHomeRecent/SpaceHomeRecent.d.ts.map +1 -1
  30. package/dist/types/src/dashboard/stats.d.ts +2 -0
  31. package/dist/types/src/dashboard/stats.d.ts.map +1 -1
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +62 -62
  34. package/src/capabilities/app-graph-builder/extensions/database.ts +5 -3
  35. package/src/capabilities/app-graph-builder/extensions/shared.test.ts +25 -1
  36. package/src/capabilities/app-graph-builder/extensions/shared.ts +6 -0
  37. package/src/components/CreateObjectPanel/CreateObjectPanel.tsx +2 -2
  38. package/src/containers/ImportSpaceDialog/ImportSpaceDialog.tsx +2 -2
  39. package/src/containers/MembersContainer/MembersContainer.tsx +3 -2
  40. package/src/containers/ObjectMasonry/ObjectMasonryArticle.tsx +1 -1
  41. package/src/containers/RecordArticle/RecordArticle.tsx +2 -5
  42. package/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.tsx +6 -1
  43. package/src/containers/SpaceHomeDashboard/SpaceHomeDashboard.tsx +1 -1
  44. package/src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx +71 -28
  45. package/src/containers/SyncStatus/SyncStatus.tsx +2 -2
  46. package/src/containers/TypeArticle/TypeArticle.tsx +1 -1
  47. package/src/dashboard/stats.test.ts +12 -2
  48. package/src/dashboard/stats.ts +4 -0
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-SyncStatus.mjs","names":[],"sources":["../../src/containers/SyncStatus/SyncStatus.tsx"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport React, { useEffect, useState } from 'react';\n\nimport * as StatusBar from '@dxos/plugin-status-bar/StatusBar';\nimport { type EdgeStatus, EdgeStatus_ConnectionState } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { useClient } from '@dxos/react-client';\nimport { type SpaceSyncStateMap, getSyncSummary, useSyncState } from '@dxos/react-client/echo';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Popover from '@dxos/react-ui/Popover';\nimport { iconSize, mx } from '@dxos/ui-theme';\nimport { Unit, type UnitFormat } from '@dxos/util';\n\nimport { createClientSaveTracker, getIcon, getStatus } from '#components';\nimport { useEdgeStatus, useStalled } from '#hooks';\nimport { meta } from '#meta';\n\nconst SYNC_COLS = ['min', 'fill', 'min', 'min'] as const;\n\nexport const SyncStatus = () => {\n const client = useClient();\n const state = useSyncState();\n const edgeStatus = useEdgeStatus();\n const [saved, setSaved] = useState(true);\n useEffect(() => createClientSaveTracker(client, (state) => setSaved(state === 'saved')), []);\n\n return <SyncStatusIndicator state={state} saved={saved} edgeStatus={edgeStatus} />;\n};\n\nexport const SyncStatusIndicator = ({\n state,\n saved,\n edgeStatus,\n}: {\n state: SpaceSyncStateMap;\n saved: boolean;\n edgeStatus: EdgeStatus;\n}) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const summary = getSyncSummary(state);\n // Absent peer sync state is indistinguishable from having no connection.\n const offline = edgeStatus.state !== EdgeStatus_ConnectionState.CONNECTED || Object.values(state).length === 0;\n const needsToUpload = summary.differentDocuments > 0 || summary.missingOnRemote > 0;\n const needsToDownload = summary.differentDocuments > 0 || summary.missingOnLocal > 0;\n\n // Documents left to reconcile; a change means replication advanced, so the stall timer restarts.\n const outstanding = summary.differentDocuments + summary.missingOnLocal + summary.missingOnRemote;\n // Bytes on the wire prove liveness while the document counts hold steady (e.g. one large document).\n const transferring = edgeStatus.rateBytesUp + edgeStatus.rateBytesDown > 0;\n const stalled = useStalled({ active: !offline && outstanding > 0 && !transferring, progress: outstanding });\n\n const status = getStatus({ offline, saved, stalled, needsToUpload, needsToDownload });\n const icon = getIcon(status);\n\n return (\n <Popover.Root positioning={{ placement: 'left' }}>\n <Popover.Trigger asChild>\n <StatusBar.Item>\n {/* The icon and label carry the status; the indicator keeps a single colour in every state. */}\n <Button.Root variant='ghost' icon={icon} iconOnly label={t(`${status}.label`)} />\n </StatusBar.Item>\n </Popover.Trigger>\n <Popover.Content>\n <EdgeConnectionPopover status={edgeStatus} />\n </Popover.Content>\n </Popover.Root>\n );\n};\n\nconst EdgeConnectionPopover = ({ status }: { status: EdgeStatus }) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const client = useClient();\n\n const isConnected = status.state === EdgeStatus_ConnectionState.CONNECTED;\n const edgeUrl = client.config.get('runtime.services.edge.url');\n\n return (\n <Layout.Flex column gap='sm' classNames='w-popover-min-width p-2' style={iconSize(4)}>\n {/* Connection Status Header */}\n <Layout.Flex gap='sm' align='center' classNames='mb-2'>\n <Icon.Icon\n icon={isConnected ? 'ph--check-circle--regular' : 'ph--warning-circle--regular'}\n classNames={mx(isConnected ? 'text-success-text' : 'text-error-text animate-pulse')}\n />\n <span className='font-medium text-sm truncate' title={edgeUrl}>\n {isConnected ? (edgeUrl ?? t('sync-edge-connected.label')) : t('sync-edge-disconnected.label')}\n </span>\n </Layout.Flex>\n\n {/* Connection Details */}\n {!isConnected && (\n <Layout.Grid cols={SYNC_COLS} gap='sm'>\n <Icon.Icon icon='ph--cloud-x--regular' />\n <span className='text-fg-muted'>{t('sync-no-connection.label')}</span>\n </Layout.Grid>\n )}\n\n {isConnected && (\n <Layout.Grid cols={SYNC_COLS} gap='sm' classNames='gap-y-1'>\n {/* Latency */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--timer--regular' />\n <span className='text-fg-muted'>{t('sync-latency.label')}</span>\n <div />\n <UnitValue value={status.rtt} format={Unit.Millisecond} />\n </Layout.Grid>\n\n {/* Upload Speed */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--arrow-up--regular' classNames='text-green-500' />\n <span className='text-fg-muted'>{t('sync-upload.label')}</span>\n <UnitValue value={status.messagesSent} format={Unit.Thousand} />\n <UnitValue value={status.rateBytesUp} format={Unit.Kilobyte} suffix='/s' />\n </Layout.Grid>\n\n {/* Download Speed */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--arrow-down--regular' classNames='text-orange-500' />\n <span className='text-fg-muted'>{t('sync-download.label')}</span>\n <UnitValue value={status.messagesReceived} format={Unit.Thousand} />\n <UnitValue value={status.rateBytesDown} format={Unit.Kilobyte} suffix='/s' />\n </Layout.Grid>\n </Layout.Grid>\n )}\n </Layout.Flex>\n );\n};\n\n// TODO(burdon): Factor out.\nconst UnitValue = ({ value: input, format, suffix }: { value: number; format: UnitFormat; suffix?: string }) => {\n const { formattedValue, unit } = format(input);\n return (\n <span className='font-mono'>\n {formattedValue}\n <span className='ms-1 text-fg-subtle'>\n {unit.symbol}\n {suffix}\n </span>\n </span>\n );\n};\n\nSyncStatus.displayName = 'SyncStatus';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-SyncStatus.mjs","names":[],"sources":["../../src/containers/SyncStatus/SyncStatus.tsx"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport React, { useEffect, useState } from 'react';\n\nimport * as StatusBar from '@dxos/plugin-status-bar/StatusBar';\nimport { type EdgeStatus, EdgeStatus_ConnectionState } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { useClient } from '@dxos/react-client';\nimport { type SpaceSyncStateMap, getSyncSummary, useSyncState } from '@dxos/react-client/echo';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Popover from '@dxos/react-ui/Popover';\nimport { iconSize, mx } from '@dxos/ui-theme';\nimport { Unit, type UnitFormat } from '@dxos/util';\n\nimport { createClientSaveTracker, getIcon, getStatus } from '#components';\nimport { useEdgeStatus, useStalled } from '#hooks';\nimport { meta } from '#meta';\n\nconst SYNC_COLS = ['min', 'fill', 'min', 'min'] as const;\n\nexport const SyncStatus = () => {\n const client = useClient();\n const state = useSyncState();\n const edgeStatus = useEdgeStatus();\n const [saved, setSaved] = useState(true);\n useEffect(() => createClientSaveTracker(client, (state) => setSaved(state === 'saved')), []);\n\n return <SyncStatusIndicator state={state} saved={saved} edgeStatus={edgeStatus} />;\n};\n\nexport const SyncStatusIndicator = ({\n state,\n saved,\n edgeStatus,\n}: {\n state: SpaceSyncStateMap;\n saved: boolean;\n edgeStatus: EdgeStatus;\n}) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const summary = getSyncSummary(state);\n // Absent peer sync state is indistinguishable from having no connection.\n const offline = edgeStatus.state !== EdgeStatus_ConnectionState.CONNECTED || Object.values(state).length === 0;\n const needsToUpload = summary.differentDocuments > 0 || summary.missingOnRemote > 0;\n const needsToDownload = summary.differentDocuments > 0 || summary.missingOnLocal > 0;\n\n // Documents left to reconcile; a change means replication advanced, so the stall timer restarts.\n const outstanding = summary.differentDocuments + summary.missingOnLocal + summary.missingOnRemote;\n // Bytes on the wire prove liveness while the document counts hold steady (e.g. one large document).\n const transferring = edgeStatus.rateBytesUp + edgeStatus.rateBytesDown > 0;\n const stalled = useStalled({ active: !offline && outstanding > 0 && !transferring, progress: outstanding });\n\n const status = getStatus({ offline, saved, stalled, needsToUpload, needsToDownload });\n const icon = getIcon(status);\n\n return (\n <Popover.Root positioning={{ placement: 'left' }}>\n <Popover.Trigger asChild>\n <StatusBar.Item>\n {/* The icon and label carry the status; the indicator keeps a single colour in every state. */}\n <Button.Root variant='ghost' icon={icon} iconOnly label={t(`${status}.label`)} />\n </StatusBar.Item>\n </Popover.Trigger>\n <Popover.Content>\n <EdgeConnectionPopover status={edgeStatus} />\n </Popover.Content>\n </Popover.Root>\n );\n};\n\nconst EdgeConnectionPopover = ({ status }: { status: EdgeStatus }) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const client = useClient();\n\n const isConnected = status.state === EdgeStatus_ConnectionState.CONNECTED;\n const edgeUrl = client.config.get('runtime.services.edge.url');\n\n return (\n <Layout.Flex column gap='sm' classNames='w-popover-min-width p-2' style={iconSize(4)}>\n {/* Connection Status Header */}\n <Layout.Flex gap='sm' align='center' classNames='mb-2'>\n <Icon.Icon\n icon={isConnected ? 'ph--check-circle--regular' : 'ph--warning-circle--regular'}\n classNames={mx(isConnected ? 'text-success-text' : 'text-error-text animate-pulse')}\n />\n <span className='font-medium text-sm truncate' title={edgeUrl}>\n {isConnected ? (edgeUrl ?? t('sync-edge-connected.label')) : t('sync-edge-disconnected.label')}\n </span>\n </Layout.Flex>\n\n {/* Connection Details */}\n {!isConnected && (\n <Layout.Grid cols={SYNC_COLS} gap='sm'>\n <Icon.Icon icon='ph--cloud-x--regular' />\n <span className='text-fg-muted'>{t('sync-no-connection.label')}</span>\n </Layout.Grid>\n )}\n\n {isConnected && (\n <Layout.Grid cols={SYNC_COLS} gap='sm' classNames='gap-y-1'>\n {/* Latency */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--timer--regular' />\n <span className='text-fg-muted'>{t('sync-latency.label')}</span>\n <div />\n <UnitValue value={status.rtt} format={Unit.Millisecond} />\n </Layout.Grid>\n\n {/* Upload Speed */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--arrow-up--regular' classNames='text-green-text' />\n <span className='text-fg-muted'>{t('sync-upload.label')}</span>\n <UnitValue value={status.messagesSent} format={Unit.Thousand} />\n <UnitValue value={status.rateBytesUp} format={Unit.Kilobyte} suffix='/s' />\n </Layout.Grid>\n\n {/* Download Speed */}\n <Layout.Grid cols='subgrid' gap='sm' align='center' classNames='text-sm'>\n <Icon.Icon icon='ph--arrow-down--regular' classNames='text-orange-text' />\n <span className='text-fg-muted'>{t('sync-download.label')}</span>\n <UnitValue value={status.messagesReceived} format={Unit.Thousand} />\n <UnitValue value={status.rateBytesDown} format={Unit.Kilobyte} suffix='/s' />\n </Layout.Grid>\n </Layout.Grid>\n )}\n </Layout.Flex>\n );\n};\n\n// TODO(burdon): Factor out.\nconst UnitValue = ({ value: input, format, suffix }: { value: number; format: UnitFormat; suffix?: string }) => {\n const { formattedValue, unit } = format(input);\n return (\n <span className='font-mono'>\n {formattedValue}\n <span className='ms-1 text-fg-subtle'>\n {unit.symbol}\n {suffix}\n </span>\n </span>\n );\n};\n\nSyncStatus.displayName = 'SyncStatus';\n"],"mappings":"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"}
@@ -469,7 +469,7 @@ var TypeArticle = ({ role, space, type, attendableId }) => {
469
469
  })
470
470
  ] }),
471
471
  /* @__PURE__ */ jsx(Panel.Footer, {
472
- classNames: "flex items-center p-1 border-t border-separator-subtle",
472
+ classNames: "items-center p-1 border-t border-separator-subtle",
473
473
  children: t("item-count.label", { count: tileItems.length })
474
474
  })
475
475
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-TypeArticle.mjs","names":[],"sources":["../../src/containers/TypeArticle/useDuplicates.ts","../../src/containers/TypeArticle/duplicatesGroup.tsx","../../src/containers/TypeArticle/TypeArticle.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport { Obj, Type } from '@dxos/echo';\nimport { type IdentitySpec, planMerge } from '@dxos/extractor';\nimport { type Space } from '@dxos/react-client/echo';\n\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nexport type UseDuplicatesProps = {\n space: Space;\n type: Type.AnyEntity;\n /** Live query results, so a group resolves to reactive objects rather than the scan's snapshots. */\n objects: readonly Obj.Unknown[];\n /** Scan only while the duplicates layout is showing. */\n enabled: boolean;\n};\n\nexport type UseDuplicatesResult = {\n /** `undefined` when no plugin registered an identity rule for this type — the tab is then hidden. */\n spec: IdentitySpec<any> | undefined;\n /** Members of the group under review, or `[]` when the review is finished. */\n current: Obj.Unknown[];\n /** 1-based position of the current group, for the toolbar counter. */\n position: number;\n total: number;\n scanning: boolean;\n next: () => void;\n previous: () => void;\n /** Re-runs the scan (after a merge, or when re-entering the tab). */\n refresh: () => void;\n};\n\n/**\n * Drives the duplicates review: runs `FindDuplicates` for the type, then walks the returned groups\n * one at a time. Groups are held as ids and resolved against the live query on every render, so a\n * merged-away member disappears from the current group without a rescan.\n */\nexport const useDuplicates = ({ space, type, objects, enabled }: UseDuplicatesProps): UseDuplicatesResult => {\n const { invokePromise } = Hooks.useOperationInvoker();\n const specs = Hooks.useCapabilities(SpaceCapabilities.IdentitySpec);\n const typename = Type.getTypename(type);\n const spec = useMemo(\n () => specs.find((candidate) => Type.getTypename(candidate.type) === typename),\n [specs, typename],\n );\n\n const [groups, setGroups] = useState<readonly SpaceOperation.DuplicateGroupResult[]>([]);\n const [index, setIndex] = useState(0);\n const [scanning, setScanning] = useState(false);\n // Rescan is a button a user can hit repeatedly, and a merge triggers its own refresh, so responses\n // can land out of order; only the newest request is allowed to write.\n const request = useRef(0);\n const { lastMergeAt } = Hooks.useAtomCapability(SpaceCapabilities.EphemeralState);\n\n const refresh = useCallback(() => {\n if (!spec) {\n return;\n }\n setScanning(true);\n const current = ++request.current;\n void invokePromise(SpaceOperation.FindDuplicates, { typename }, { spaceId: space.id })\n .then(({ data }) => {\n if (current !== request.current) {\n return;\n }\n setGroups(data?.groups ?? []);\n setIndex(0);\n })\n .finally(() => {\n if (current === request.current) {\n setScanning(false);\n }\n });\n }, [invokePromise, spec, typename, space.id]);\n\n // `lastMergeAt` is a dependency, not a read: a committed merge invalidates the scan.\n useEffect(() => {\n if (enabled) {\n refresh();\n }\n }, [enabled, refresh, lastMergeAt]);\n\n const byId = useMemo(() => new Map(objects.map((object) => [object.id, object])), [objects]);\n const current = useMemo(() => {\n const group = groups[index];\n return group ? group.objectIds.flatMap((id) => byId.get(id) ?? []) : [];\n }, [groups, index, byId]);\n\n return {\n spec,\n current,\n position: groups.length === 0 ? 0 : index + 1,\n total: groups.length,\n scanning,\n // Clamp at the last valid index, not at `groups.length`: `position` is `index + 1`, so one past\n // the end rendered an impossible counter (\"4 / 3\") when Skip was pressed on the last group.\n next: useCallback(() => setIndex((value) => Math.min(value + 1, Math.max(groups.length - 1, 0))), [groups.length]),\n previous: useCallback(() => setIndex((value) => Math.max(value - 1, 0)), []),\n refresh,\n };\n};\n\n/** Builds the detached preview for the group under review; `undefined` when there is nothing to merge. */\nexport const buildMergePreview = (\n spec: IdentitySpec<any> | undefined,\n objects: readonly Obj.Unknown[],\n): Obj.Unknown | undefined => {\n if (!spec || objects.length < 2) {\n return undefined;\n }\n const { preview } = planMerge(spec, { keys: [], objects });\n return Obj.isObject(preview) ? preview : undefined;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport { Ref } from '@dxos/echo';\nimport { type SpaceId } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport type { ActionGroupBuilderFn } from '@dxos/react-ui-menu';\n\nimport { meta } from '#meta';\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nimport { type UseDuplicatesResult, buildMergePreview } from './useDuplicates.ts';\n\nexport type UseDuplicatesGroupOptions = {\n /** URI of the type under review; scopes the staged preview to this article. */\n typeUri: string;\n typename: string;\n spaceId: SpaceId;\n /** Shared selection; a ≥2-member subset of the current group narrows the merge to it. */\n selectedIds: string[];\n duplicates: UseDuplicatesResult;\n /** Called after a confirmed merge with the ids merged away, so the caller can drop them from the selection. */\n onConfirmed?: (objectIds: string[]) => void;\n};\n\n/**\n * Toolbar subgraph for the duplicates review: Merge stages a preview, Skip and the arrows walk the\n * groups, and the trailing gap separator pushes whatever follows (the layout toggle) to the far end.\n * While a preview is staged, Confirm/Cancel replace Merge/Skip so the whole review completes within\n * the article; the companion only mirrors the staged preview. Merge never writes — nothing touches\n * the space until Confirm.\n *\n * Compose via `MenuBuilder.subgraph(...)` so the whole toolbar is one action graph rather than\n * several nested `Toolbar.Root`s.\n */\nexport const useDuplicatesGroup = ({\n typeUri,\n typename,\n spaceId,\n selectedIds,\n duplicates,\n onConfirmed,\n}: UseDuplicatesGroupOptions): ActionGroupBuilderFn => {\n const [ephemeral, updateEphemeral] = Hooks.useAtomCapabilityState(SpaceCapabilities.EphemeralState);\n const { invokePromise } = Hooks.useOperationInvoker();\n const { spec, current, position, total, scanning, next, previous, refresh } = duplicates;\n const staged = ephemeral.mergePreview?.typeUri === typeUri ? ephemeral.mergePreview : undefined;\n\n // The merge removes objects, so a second click would re-run it against ids the first call has\n // already deleted. Confirm is disabled for the duration.\n const [merging, setMerging] = useState(false);\n\n // Every control that changes which group is under review drops the staged preview: it belongs to\n // the group it was raised from, and leaving it up would show the companion previewing one group\n // while the article shows another.\n const clearPreview = useCallback(\n () => updateEphemeral((state) => ({ ...state, mergePreview: undefined })),\n [updateEphemeral],\n );\n\n const handleMerge = useCallback(() => {\n const selected = current.filter((object) => selectedIds.includes(object.id));\n const participants = selected.length >= 2 ? selected : current;\n const preview = buildMergePreview(spec, participants);\n if (!preview) {\n return;\n }\n updateEphemeral((state) => ({\n ...state,\n mergePreview: { typeUri, typename, objectIds: participants.map((object) => object.id), preview },\n }));\n }, [updateEphemeral, spec, current, selectedIds, typeUri, typename]);\n\n const handleConfirm = useCallback(() => {\n if (!staged) {\n return;\n }\n // `staged.objectIds` is the ephemeral preview's own key (bare ids, matched against selections);\n // the operation wants the live members themselves, resolved from the group still under review.\n const members = current.filter((object) => staged.objectIds.includes(object.id));\n if (members.length !== staged.objectIds.length) {\n log.warn('merge preview member missing from current group', { typeUri: staged.typeUri });\n return;\n }\n setMerging(true);\n void invokePromise(\n SpaceOperation.MergeDuplicates,\n { typename: staged.typename, objectIds: members.map((object) => Ref.make(object)), overrides: staged.preview },\n { spaceId },\n )\n .then(({ error }) => {\n if (error) {\n // Leave the preview up: the merge did not happen, and clearing it would look like it did.\n log.warn('merge failed', { error });\n return;\n }\n // Bumping the timestamp is what tells the open review to rescan; without it the group the\n // user just merged stays on screen as a one-member \"duplicate\". Only the preview this\n // operation was raised from is cleared — a preview staged elsewhere mid-flight survives.\n updateEphemeral((state) => ({\n ...state,\n mergePreview: state.mergePreview === staged ? undefined : state.mergePreview,\n lastMergeAt: Date.now(),\n }));\n onConfirmed?.(staged.objectIds);\n })\n .finally(() => setMerging(false));\n }, [staged, current, invokePromise, spaceId, updateEphemeral, onConfirmed]);\n\n // Skip and the next arrow are the same move — advance past the group without writing.\n const handleAdvance = useCallback(() => {\n clearPreview();\n next();\n }, [clearPreview, next]);\n\n const handlePrevious = useCallback(() => {\n clearPreview();\n previous();\n }, [clearPreview, previous]);\n\n const handleRefresh = useCallback(() => {\n clearPreview();\n refresh();\n }, [clearPreview, refresh]);\n\n return useMemo(\n () => (builder) => {\n if (staged) {\n builder\n .action(\n 'confirm-merge',\n {\n label: ['confirm-merge.label', { ns: meta.profile.key }],\n icon: 'ph--check--regular',\n variant: 'primary',\n iconOnly: false,\n disabled: merging,\n },\n handleConfirm,\n )\n .action(\n 'cancel-merge',\n {\n label: ['cancel-merge.label', { ns: meta.profile.key }],\n iconOnly: false,\n disabled: merging,\n },\n clearPreview,\n );\n } else {\n builder\n .action(\n 'merge',\n {\n label: ['merge-duplicates.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-merge--regular',\n variant: 'primary',\n iconOnly: false,\n disabled: current.length < 2 || merging,\n },\n handleMerge,\n )\n .action(\n 'skip',\n {\n label: ['skip-duplicates.label', { ns: meta.profile.key }],\n iconOnly: false,\n // Same bound as the next arrow: skipping past the last group left the counter out of range.\n disabled: position >= total || merging,\n },\n handleAdvance,\n );\n }\n // Every control below clears the staged preview, so all of them freeze while the merge\n // operation is in flight — otherwise a mid-flight clear/restage races the completion handler.\n builder\n .action(\n 'rescan',\n {\n label: ['rescan-duplicates.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-clockwise--regular',\n disabled: scanning || merging,\n spin: scanning,\n },\n handleRefresh,\n )\n .subgraph((builder) =>\n total === 0\n ? []\n : builder\n .action(\n 'previous',\n {\n label: ['previous-duplicate.label', { ns: meta.profile.key }],\n icon: 'ph--caret-left--regular',\n disabled: position <= 1 || merging,\n },\n handlePrevious,\n )\n .action(\n 'position',\n {\n // The group counter is not an action; `custom` lets the graph carry it so the toolbar stays\n // a single menu rather than a menu plus a hand-rolled toolbar for one label.\n variant: 'custom',\n label: ['duplicates-position.label', { ns: meta.profile.key }],\n render: () => (\n <span className='text-fg-muted text-sm tabular-nums'>\n {position} / {total}\n </span>\n ),\n },\n () => {},\n )\n .action(\n 'next',\n {\n label: ['next-duplicate.label', { ns: meta.profile.key }],\n icon: 'ph--caret-right--regular',\n disabled: position >= total || merging,\n },\n handleAdvance,\n ),\n )\n .separator();\n },\n [\n staged,\n merging,\n current.length,\n position,\n total,\n scanning,\n handleMerge,\n handleConfirm,\n clearPreview,\n handleAdvance,\n handlePrevious,\n handleRefresh,\n ],\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type PropsWithChildren, useCallback, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as CollectionOperation from '@dxos/app-toolkit/CollectionOperation';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Filter, Obj, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space } from '@dxos/react-client/echo';\nimport { Selection, useSelection, useSelectionActions, useViewStateActions } from '@dxos/react-ui-attention';\nimport { ActionToolbar, MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';\nimport { SearchList, useSearchListResults } from '@dxos/react-ui-search';\nimport { DynamicTable, type TableRowAction } from '@dxos/react-ui-table';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as Status from '@dxos/react-ui/Status';\nimport * as Tabs from '@dxos/react-ui/Tabs';\nimport * as Toolbar from '@dxos/react-ui/Toolbar';\nimport { mx, osTranslations } from '@dxos/ui-theme';\n\nimport { meta } from '#meta';\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nimport { ObjectMasonry, type TileData } from '../ObjectMasonry/index.ts';\nimport { useDuplicatesGroup } from './duplicatesGroup.tsx';\nimport { useDuplicates } from './useDuplicates.ts';\n\n/** Sidebar layout modes for a type article. */\ntype Layout = 'masonry' | 'table' | 'duplicates';\n\nconst LAYOUTS: { value: Layout; icon: string }[] = [\n {\n value: 'masonry',\n icon: 'ph--squares-four--regular',\n },\n {\n value: 'table',\n icon: 'ph--table--regular',\n },\n {\n value: 'duplicates',\n icon: 'ph--copy--regular',\n },\n];\n\nexport type TypeArticleProps = {\n role?: string;\n space: Space;\n type: Type.AnyEntity;\n attendableId: string;\n};\n\n/**\n * List view rendered when a type node is selected: a toolbar with a Masonry/Table layout switch and a\n * text filter over every object of the type. Selecting an item opens it as a sibling plank.\n *\n * Objects are not enumerated in the nav tree; each navigated object becomes a hidden child of the\n * type node resolved on demand.\n */\nexport const TypeArticle = ({ role, space, type, attendableId }: TypeArticleProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n const [layout, setLayout] = useState<Layout>('masonry');\n const typeUri = Type.getURI(type);\n const objects = useQuery(space.db, Filter.type(typeUri));\n\n // Ordered by label: the query returns index order, which reads as arbitrary to someone scanning a\n // directory of cards. Sorted here rather than in the query because a label is DERIVED (`Obj.getLabel`\n // resolves a different property per type), so there is no single property to order on. Sorting the\n // INPUT leaves the search below free to rank by match score while a filter is active.\n const ordered = useMemo(\n () =>\n [...objects].sort((a, b) =>\n (Obj.getLabel(a) ?? '').localeCompare(Obj.getLabel(b) ?? '', undefined, { sensitivity: 'base' }),\n ),\n [objects],\n );\n\n // Text filter over the object labels; feeds both the masonry tiles and the table rows.\n const { results, handleSearch } = useSearchListResults<Obj.Unknown>({\n items: ordered,\n extract: (object) => Obj.getLabel(object) ?? '',\n });\n\n // Selection is keyed by the type's own URI — the same id the 'selected-objects' companion resolves\n // from its `companionTo` (see react-surface.tsx), so cards, table rows and the companion agree.\n const selectedIds = useSelection(typeUri, 'multi');\n const { multi: setSelectedObjects, toggle: toggleSelected } = useSelectionActions(typeUri);\n\n // TODO(burdon): Factor out as an aspect?\n const duplicates = useDuplicates({ space, type, objects, enabled: layout === 'duplicates' });\n const { mergePreview } = Hooks.useAtomCapability(SpaceCapabilities.EphemeralState);\n const stagedPreview = mergePreview?.typeUri === typeUri ? mergePreview : undefined;\n\n // Merged-away ids would otherwise linger in the shared selection and the companion's card stack.\n // Functional update against the live selection: the merge operation is async, so a snapshot of\n // `selectedIds` taken at confirm time would drop any selection change made while it ran.\n const { update: updateSelection } = useViewStateActions(Selection.aspect, typeUri);\n const handleConfirmed = useCallback(\n (objectIds: string[]) =>\n updateSelection((previous) => ({\n mode: 'multi',\n ids: Selection.resolve(previous, 'multi').filter((id) => !objectIds.includes(id)),\n })),\n [updateSelection],\n );\n\n const duplicatesGroup = useDuplicatesGroup({\n typeUri,\n typename: Type.getTypename(type),\n spaceId: space.id,\n selectedIds,\n duplicates,\n onConfirmed: handleConfirmed,\n });\n\n // Duplicates is offered only for types some plugin registered an identity rule for:\n // there is nothing to scan otherwise.\n const layouts = useMemo(\n () => LAYOUTS.filter(({ value }) => value !== 'duplicates' || duplicates.spec),\n [duplicates.spec],\n );\n\n const handleOpen = useCallback(\n (object: Obj.Unknown) => {\n const id = Obj.getURI(object);\n void invokePromise(LayoutOperation.Select, { contextId: attendableId, subject: { mode: 'single', id } });\n void invokePromise(LayoutOperation.Open, {\n // Open under the node being viewed rather than the object's canonical type path, so an object\n // reached from a type section opens within that section. Identical for a database type node,\n // whose object path already is `<typeNode>/<id>`.\n subject: [GraphPath.getCollectionObjectPath(attendableId, object.id)],\n pivotId: attendableId,\n disposition: 'add',\n navigation: 'immediate',\n });\n },\n [attendableId, invokePromise],\n );\n\n // Both deletes go through the operation, whose undo mapping puts the objects back (a direct\n // `db.remove` would be silent and final); a removed object also leaves the selection.\n const removeObjects = useCallback(\n (removed: Obj.Unknown[]) => {\n if (removed.length === 0) {\n return;\n }\n void invokePromise(SpaceOperation.RemoveObjects, { objects: removed }, { spaceId: space.id });\n const removedIds = new Set(removed.map((object) => object.id));\n setSelectedObjects(selectedIds.filter((id) => !removedIds.has(id)));\n },\n [selectedIds, invokePromise, space.id, setSelectedObjects],\n );\n\n const handleDelete = useCallback((object: Obj.Unknown) => removeObjects([object]), [removeObjects]);\n\n const handleDeleteSelected = useCallback(\n () => removeObjects(objects.filter((object) => selectedIds.includes(object.id))),\n [objects, selectedIds, removeObjects],\n );\n\n // Stable identity: `DynamicTable` rebuilds its model whenever `features` changes, so an inline\n // literal here would discard and re-seed the table's selection on every render.\n const tableFeatures = useMemo(\n () => ({\n selection: { enabled: true, mode: 'multiple' as const },\n dataEditable: true,\n schemaEditable: false,\n pinColumns: 1,\n }),\n [],\n );\n\n // Table rows are editable, so opening a row is a deliberate row action rather than `onRowClick`\n // (which would fire on every cell click and fight with in-cell editing).\n const rowActions = useMemo(\n (): TableRowAction[] => [\n { id: 'open', label: ['open-object.label', { ns: meta.profile.key }] },\n ...(TypeOptions.isUserType(type)\n ? [\n {\n id: 'addToCollection',\n label: ['add-to-collection.label', { ns: osTranslations }],\n } satisfies TableRowAction,\n ]\n : []),\n ],\n [type],\n );\n\n const handleRowAction = useCallback(\n (actionId: string, object: Obj.Unknown) => {\n if (actionId === 'open') {\n handleOpen(object);\n } else if (actionId === 'addToCollection') {\n void invokePromise(CollectionOperation.OpenAddToCollection, { object });\n }\n },\n [handleOpen, invokePromise],\n );\n\n // One action graph for the whole toolbar: the mode-specific actions first, then the layout toggle.\n // Composing several `Toolbar.Root`s instead would each need their own roving-focus group and would\n // fight over the row's width.\n const menuActions = useMenuBuilder(\n () =>\n MenuBuilder.make()\n .subgraph(layout === 'duplicates' && duplicatesGroup)\n .subgraph(\n layout === 'table' &&\n selectedIds.length > 0 &&\n ((builder) => {\n builder.action(\n 'delete-selected',\n {\n label: ['delete-selected.label', { ns: meta.profile.key, count: selectedIds.length }],\n icon: 'ph--trash--regular',\n },\n handleDeleteSelected,\n );\n }),\n )\n .separator()\n .group(\n 'layout',\n {\n variant: 'toggleGroup',\n selectCardinality: 'single',\n value: layout,\n label: ['layout.label', { ns: meta.profile.key }],\n },\n (group) => {\n layouts.forEach(({ value, icon }) => {\n group.action(value, { label: [`layout-${value}.label`, { ns: meta.profile.key }], icon }, () =>\n setLayout(value),\n );\n });\n },\n )\n .build(),\n [layout, layouts, selectedIds.length, duplicatesGroup, handleDeleteSelected],\n );\n\n // The card grid backs two layouts — every object for `masonry`, only the group under review for\n // `duplicates` — so the items are resolved here and the grid itself is declared once.\n const tiles = layout === 'duplicates' ? duplicates.current : results;\n const tileItems = useMemo<TileData[]>(() => {\n const toTile = (object: Obj.Unknown): TileData => ({\n object,\n current: selectedIds.includes(object.id),\n onSelect: toggleSelected,\n onOpen: handleOpen,\n onDelete: Obj.getParent(object) ? undefined : handleDelete,\n });\n // While a merge is staged the participants fold into one read-only result card; members the\n // selection excluded stay behind as normal cards.\n if (layout === 'duplicates' && stagedPreview) {\n const participants = new Set(stagedPreview.objectIds);\n return [\n { object: stagedPreview.preview, current: true },\n ...tiles.filter((object) => !participants.has(object.id)).map(toTile),\n ];\n }\n return tiles.map(toTile);\n }, [layout, stagedPreview, tiles, selectedIds, toggleSelected, handleOpen, handleDelete]);\n\n // A type with no objects at all has nothing for any layout to show; past that each layout says\n // when it is empty for its own reason.\n const noObjects = objects.length === 0 ? t('type-collection-empty.message') : undefined;\n const noResults = noObjects ?? (results.length === 0 ? t('search-no-results.message') : undefined);\n const noDuplicates =\n noObjects ??\n (duplicates.current.length === 0\n ? duplicates.scanning\n ? t('duplicates-scanning.message')\n : t('duplicates-none.message')\n : undefined);\n\n return (\n <SearchList.Root onSearch={handleSearch}>\n <Tabs.Root asChild value={layout} onValueChange={(value) => setLayout(value as Layout)} orientation='vertical'>\n <Panel.Root role={role}>\n <Panel.Header classNames={mx('grid', layout !== 'duplicates' && 'grid-cols-[1fr_auto]')}>\n {layout !== 'duplicates' && (\n <Toolbar.Root>\n <SearchList.Input placeholder={t('search-placeholder.label')} />\n </Toolbar.Root>\n )}\n <ActionToolbar {...menuActions} attendableId={attendableId} alwaysActive />\n </Panel.Header>\n <Panel.Body>\n <LayoutPanel value='masonry' empty={noResults}>\n <ObjectMasonry cacheKey={typeUri} items={tileItems} />\n </LayoutPanel>\n <LayoutPanel value='table' empty={noResults}>\n <DynamicTable\n type={type}\n rows={results}\n features={tableFeatures}\n rowActions={rowActions}\n // Seed from the shared selection so switching card → table keeps what was selected;\n // the table otherwise starts from its own empty model.\n selection={selectedIds}\n onRowAction={handleRowAction}\n onSelectionChanged={setSelectedObjects}\n />\n </LayoutPanel>\n {duplicates.spec && (\n <LayoutPanel value='duplicates' empty={noDuplicates}>\n <ObjectMasonry cacheKey={typeUri} items={tileItems} />\n </LayoutPanel>\n )}\n </Panel.Body>\n <Panel.Footer classNames='flex items-center p-1 border-t border-separator-subtle'>\n {t('item-count.label', { count: tileItems.length })}\n </Panel.Footer>\n </Panel.Root>\n </Tabs.Root>\n </SearchList.Root>\n );\n};\n\n/** One layout's content, or the message standing in for it when the layout has nothing to show. */\nconst LayoutPanel = ({ value, empty, children }: PropsWithChildren<{ value: Layout; empty?: string }>) => (\n <Tabs.Content value={value} classNames='contents'>\n {empty ? <Status.Empty classNames='h-full'>{empty}</Status.Empty> : children}\n </Tabs.Content>\n);\n\nTypeArticle.displayName = 'TypeArticle';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-TypeArticle.mjs","names":[],"sources":["../../src/containers/TypeArticle/useDuplicates.ts","../../src/containers/TypeArticle/duplicatesGroup.tsx","../../src/containers/TypeArticle/TypeArticle.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport { Obj, Type } from '@dxos/echo';\nimport { type IdentitySpec, planMerge } from '@dxos/extractor';\nimport { type Space } from '@dxos/react-client/echo';\n\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nexport type UseDuplicatesProps = {\n space: Space;\n type: Type.AnyEntity;\n /** Live query results, so a group resolves to reactive objects rather than the scan's snapshots. */\n objects: readonly Obj.Unknown[];\n /** Scan only while the duplicates layout is showing. */\n enabled: boolean;\n};\n\nexport type UseDuplicatesResult = {\n /** `undefined` when no plugin registered an identity rule for this type — the tab is then hidden. */\n spec: IdentitySpec<any> | undefined;\n /** Members of the group under review, or `[]` when the review is finished. */\n current: Obj.Unknown[];\n /** 1-based position of the current group, for the toolbar counter. */\n position: number;\n total: number;\n scanning: boolean;\n next: () => void;\n previous: () => void;\n /** Re-runs the scan (after a merge, or when re-entering the tab). */\n refresh: () => void;\n};\n\n/**\n * Drives the duplicates review: runs `FindDuplicates` for the type, then walks the returned groups\n * one at a time. Groups are held as ids and resolved against the live query on every render, so a\n * merged-away member disappears from the current group without a rescan.\n */\nexport const useDuplicates = ({ space, type, objects, enabled }: UseDuplicatesProps): UseDuplicatesResult => {\n const { invokePromise } = Hooks.useOperationInvoker();\n const specs = Hooks.useCapabilities(SpaceCapabilities.IdentitySpec);\n const typename = Type.getTypename(type);\n const spec = useMemo(\n () => specs.find((candidate) => Type.getTypename(candidate.type) === typename),\n [specs, typename],\n );\n\n const [groups, setGroups] = useState<readonly SpaceOperation.DuplicateGroupResult[]>([]);\n const [index, setIndex] = useState(0);\n const [scanning, setScanning] = useState(false);\n // Rescan is a button a user can hit repeatedly, and a merge triggers its own refresh, so responses\n // can land out of order; only the newest request is allowed to write.\n const request = useRef(0);\n const { lastMergeAt } = Hooks.useAtomCapability(SpaceCapabilities.EphemeralState);\n\n const refresh = useCallback(() => {\n if (!spec) {\n return;\n }\n setScanning(true);\n const current = ++request.current;\n void invokePromise(SpaceOperation.FindDuplicates, { typename }, { spaceId: space.id })\n .then(({ data }) => {\n if (current !== request.current) {\n return;\n }\n setGroups(data?.groups ?? []);\n setIndex(0);\n })\n .finally(() => {\n if (current === request.current) {\n setScanning(false);\n }\n });\n }, [invokePromise, spec, typename, space.id]);\n\n // `lastMergeAt` is a dependency, not a read: a committed merge invalidates the scan.\n useEffect(() => {\n if (enabled) {\n refresh();\n }\n }, [enabled, refresh, lastMergeAt]);\n\n const byId = useMemo(() => new Map(objects.map((object) => [object.id, object])), [objects]);\n const current = useMemo(() => {\n const group = groups[index];\n return group ? group.objectIds.flatMap((id) => byId.get(id) ?? []) : [];\n }, [groups, index, byId]);\n\n return {\n spec,\n current,\n position: groups.length === 0 ? 0 : index + 1,\n total: groups.length,\n scanning,\n // Clamp at the last valid index, not at `groups.length`: `position` is `index + 1`, so one past\n // the end rendered an impossible counter (\"4 / 3\") when Skip was pressed on the last group.\n next: useCallback(() => setIndex((value) => Math.min(value + 1, Math.max(groups.length - 1, 0))), [groups.length]),\n previous: useCallback(() => setIndex((value) => Math.max(value - 1, 0)), []),\n refresh,\n };\n};\n\n/** Builds the detached preview for the group under review; `undefined` when there is nothing to merge. */\nexport const buildMergePreview = (\n spec: IdentitySpec<any> | undefined,\n objects: readonly Obj.Unknown[],\n): Obj.Unknown | undefined => {\n if (!spec || objects.length < 2) {\n return undefined;\n }\n const { preview } = planMerge(spec, { keys: [], objects });\n return Obj.isObject(preview) ? preview : undefined;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport { Ref } from '@dxos/echo';\nimport { type SpaceId } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport type { ActionGroupBuilderFn } from '@dxos/react-ui-menu';\n\nimport { meta } from '#meta';\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nimport { type UseDuplicatesResult, buildMergePreview } from './useDuplicates.ts';\n\nexport type UseDuplicatesGroupOptions = {\n /** URI of the type under review; scopes the staged preview to this article. */\n typeUri: string;\n typename: string;\n spaceId: SpaceId;\n /** Shared selection; a ≥2-member subset of the current group narrows the merge to it. */\n selectedIds: string[];\n duplicates: UseDuplicatesResult;\n /** Called after a confirmed merge with the ids merged away, so the caller can drop them from the selection. */\n onConfirmed?: (objectIds: string[]) => void;\n};\n\n/**\n * Toolbar subgraph for the duplicates review: Merge stages a preview, Skip and the arrows walk the\n * groups, and the trailing gap separator pushes whatever follows (the layout toggle) to the far end.\n * While a preview is staged, Confirm/Cancel replace Merge/Skip so the whole review completes within\n * the article; the companion only mirrors the staged preview. Merge never writes — nothing touches\n * the space until Confirm.\n *\n * Compose via `MenuBuilder.subgraph(...)` so the whole toolbar is one action graph rather than\n * several nested `Toolbar.Root`s.\n */\nexport const useDuplicatesGroup = ({\n typeUri,\n typename,\n spaceId,\n selectedIds,\n duplicates,\n onConfirmed,\n}: UseDuplicatesGroupOptions): ActionGroupBuilderFn => {\n const [ephemeral, updateEphemeral] = Hooks.useAtomCapabilityState(SpaceCapabilities.EphemeralState);\n const { invokePromise } = Hooks.useOperationInvoker();\n const { spec, current, position, total, scanning, next, previous, refresh } = duplicates;\n const staged = ephemeral.mergePreview?.typeUri === typeUri ? ephemeral.mergePreview : undefined;\n\n // The merge removes objects, so a second click would re-run it against ids the first call has\n // already deleted. Confirm is disabled for the duration.\n const [merging, setMerging] = useState(false);\n\n // Every control that changes which group is under review drops the staged preview: it belongs to\n // the group it was raised from, and leaving it up would show the companion previewing one group\n // while the article shows another.\n const clearPreview = useCallback(\n () => updateEphemeral((state) => ({ ...state, mergePreview: undefined })),\n [updateEphemeral],\n );\n\n const handleMerge = useCallback(() => {\n const selected = current.filter((object) => selectedIds.includes(object.id));\n const participants = selected.length >= 2 ? selected : current;\n const preview = buildMergePreview(spec, participants);\n if (!preview) {\n return;\n }\n updateEphemeral((state) => ({\n ...state,\n mergePreview: { typeUri, typename, objectIds: participants.map((object) => object.id), preview },\n }));\n }, [updateEphemeral, spec, current, selectedIds, typeUri, typename]);\n\n const handleConfirm = useCallback(() => {\n if (!staged) {\n return;\n }\n // `staged.objectIds` is the ephemeral preview's own key (bare ids, matched against selections);\n // the operation wants the live members themselves, resolved from the group still under review.\n const members = current.filter((object) => staged.objectIds.includes(object.id));\n if (members.length !== staged.objectIds.length) {\n log.warn('merge preview member missing from current group', { typeUri: staged.typeUri });\n return;\n }\n setMerging(true);\n void invokePromise(\n SpaceOperation.MergeDuplicates,\n { typename: staged.typename, objectIds: members.map((object) => Ref.make(object)), overrides: staged.preview },\n { spaceId },\n )\n .then(({ error }) => {\n if (error) {\n // Leave the preview up: the merge did not happen, and clearing it would look like it did.\n log.warn('merge failed', { error });\n return;\n }\n // Bumping the timestamp is what tells the open review to rescan; without it the group the\n // user just merged stays on screen as a one-member \"duplicate\". Only the preview this\n // operation was raised from is cleared — a preview staged elsewhere mid-flight survives.\n updateEphemeral((state) => ({\n ...state,\n mergePreview: state.mergePreview === staged ? undefined : state.mergePreview,\n lastMergeAt: Date.now(),\n }));\n onConfirmed?.(staged.objectIds);\n })\n .finally(() => setMerging(false));\n }, [staged, current, invokePromise, spaceId, updateEphemeral, onConfirmed]);\n\n // Skip and the next arrow are the same move — advance past the group without writing.\n const handleAdvance = useCallback(() => {\n clearPreview();\n next();\n }, [clearPreview, next]);\n\n const handlePrevious = useCallback(() => {\n clearPreview();\n previous();\n }, [clearPreview, previous]);\n\n const handleRefresh = useCallback(() => {\n clearPreview();\n refresh();\n }, [clearPreview, refresh]);\n\n return useMemo(\n () => (builder) => {\n if (staged) {\n builder\n .action(\n 'confirm-merge',\n {\n label: ['confirm-merge.label', { ns: meta.profile.key }],\n icon: 'ph--check--regular',\n variant: 'primary',\n iconOnly: false,\n disabled: merging,\n },\n handleConfirm,\n )\n .action(\n 'cancel-merge',\n {\n label: ['cancel-merge.label', { ns: meta.profile.key }],\n iconOnly: false,\n disabled: merging,\n },\n clearPreview,\n );\n } else {\n builder\n .action(\n 'merge',\n {\n label: ['merge-duplicates.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-merge--regular',\n variant: 'primary',\n iconOnly: false,\n disabled: current.length < 2 || merging,\n },\n handleMerge,\n )\n .action(\n 'skip',\n {\n label: ['skip-duplicates.label', { ns: meta.profile.key }],\n iconOnly: false,\n // Same bound as the next arrow: skipping past the last group left the counter out of range.\n disabled: position >= total || merging,\n },\n handleAdvance,\n );\n }\n // Every control below clears the staged preview, so all of them freeze while the merge\n // operation is in flight — otherwise a mid-flight clear/restage races the completion handler.\n builder\n .action(\n 'rescan',\n {\n label: ['rescan-duplicates.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-clockwise--regular',\n disabled: scanning || merging,\n spin: scanning,\n },\n handleRefresh,\n )\n .subgraph((builder) =>\n total === 0\n ? []\n : builder\n .action(\n 'previous',\n {\n label: ['previous-duplicate.label', { ns: meta.profile.key }],\n icon: 'ph--caret-left--regular',\n disabled: position <= 1 || merging,\n },\n handlePrevious,\n )\n .action(\n 'position',\n {\n // The group counter is not an action; `custom` lets the graph carry it so the toolbar stays\n // a single menu rather than a menu plus a hand-rolled toolbar for one label.\n variant: 'custom',\n label: ['duplicates-position.label', { ns: meta.profile.key }],\n render: () => (\n <span className='text-fg-muted text-sm tabular-nums'>\n {position} / {total}\n </span>\n ),\n },\n () => {},\n )\n .action(\n 'next',\n {\n label: ['next-duplicate.label', { ns: meta.profile.key }],\n icon: 'ph--caret-right--regular',\n disabled: position >= total || merging,\n },\n handleAdvance,\n ),\n )\n .separator();\n },\n [\n staged,\n merging,\n current.length,\n position,\n total,\n scanning,\n handleMerge,\n handleConfirm,\n clearPreview,\n handleAdvance,\n handlePrevious,\n handleRefresh,\n ],\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type PropsWithChildren, useCallback, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as CollectionOperation from '@dxos/app-toolkit/CollectionOperation';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Filter, Obj, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space } from '@dxos/react-client/echo';\nimport { Selection, useSelection, useSelectionActions, useViewStateActions } from '@dxos/react-ui-attention';\nimport { ActionToolbar, MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';\nimport { SearchList, useSearchListResults } from '@dxos/react-ui-search';\nimport { DynamicTable, type TableRowAction } from '@dxos/react-ui-table';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as Status from '@dxos/react-ui/Status';\nimport * as Tabs from '@dxos/react-ui/Tabs';\nimport * as Toolbar from '@dxos/react-ui/Toolbar';\nimport { mx, osTranslations } from '@dxos/ui-theme';\n\nimport { meta } from '#meta';\nimport { SpaceCapabilities, SpaceOperation } from '#types';\n\nimport { ObjectMasonry, type TileData } from '../ObjectMasonry/index.ts';\nimport { useDuplicatesGroup } from './duplicatesGroup.tsx';\nimport { useDuplicates } from './useDuplicates.ts';\n\n/** Sidebar layout modes for a type article. */\ntype Layout = 'masonry' | 'table' | 'duplicates';\n\nconst LAYOUTS: { value: Layout; icon: string }[] = [\n {\n value: 'masonry',\n icon: 'ph--squares-four--regular',\n },\n {\n value: 'table',\n icon: 'ph--table--regular',\n },\n {\n value: 'duplicates',\n icon: 'ph--copy--regular',\n },\n];\n\nexport type TypeArticleProps = {\n role?: string;\n space: Space;\n type: Type.AnyEntity;\n attendableId: string;\n};\n\n/**\n * List view rendered when a type node is selected: a toolbar with a Masonry/Table layout switch and a\n * text filter over every object of the type. Selecting an item opens it as a sibling plank.\n *\n * Objects are not enumerated in the nav tree; each navigated object becomes a hidden child of the\n * type node resolved on demand.\n */\nexport const TypeArticle = ({ role, space, type, attendableId }: TypeArticleProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n const [layout, setLayout] = useState<Layout>('masonry');\n const typeUri = Type.getURI(type);\n const objects = useQuery(space.db, Filter.type(typeUri));\n\n // Ordered by label: the query returns index order, which reads as arbitrary to someone scanning a\n // directory of cards. Sorted here rather than in the query because a label is DERIVED (`Obj.getLabel`\n // resolves a different property per type), so there is no single property to order on. Sorting the\n // INPUT leaves the search below free to rank by match score while a filter is active.\n const ordered = useMemo(\n () =>\n [...objects].sort((a, b) =>\n (Obj.getLabel(a) ?? '').localeCompare(Obj.getLabel(b) ?? '', undefined, { sensitivity: 'base' }),\n ),\n [objects],\n );\n\n // Text filter over the object labels; feeds both the masonry tiles and the table rows.\n const { results, handleSearch } = useSearchListResults<Obj.Unknown>({\n items: ordered,\n extract: (object) => Obj.getLabel(object) ?? '',\n });\n\n // Selection is keyed by the type's own URI — the same id the 'selected-objects' companion resolves\n // from its `companionTo` (see react-surface.tsx), so cards, table rows and the companion agree.\n const selectedIds = useSelection(typeUri, 'multi');\n const { multi: setSelectedObjects, toggle: toggleSelected } = useSelectionActions(typeUri);\n\n // TODO(burdon): Factor out as an aspect?\n const duplicates = useDuplicates({ space, type, objects, enabled: layout === 'duplicates' });\n const { mergePreview } = Hooks.useAtomCapability(SpaceCapabilities.EphemeralState);\n const stagedPreview = mergePreview?.typeUri === typeUri ? mergePreview : undefined;\n\n // Merged-away ids would otherwise linger in the shared selection and the companion's card stack.\n // Functional update against the live selection: the merge operation is async, so a snapshot of\n // `selectedIds` taken at confirm time would drop any selection change made while it ran.\n const { update: updateSelection } = useViewStateActions(Selection.aspect, typeUri);\n const handleConfirmed = useCallback(\n (objectIds: string[]) =>\n updateSelection((previous) => ({\n mode: 'multi',\n ids: Selection.resolve(previous, 'multi').filter((id) => !objectIds.includes(id)),\n })),\n [updateSelection],\n );\n\n const duplicatesGroup = useDuplicatesGroup({\n typeUri,\n typename: Type.getTypename(type),\n spaceId: space.id,\n selectedIds,\n duplicates,\n onConfirmed: handleConfirmed,\n });\n\n // Duplicates is offered only for types some plugin registered an identity rule for:\n // there is nothing to scan otherwise.\n const layouts = useMemo(\n () => LAYOUTS.filter(({ value }) => value !== 'duplicates' || duplicates.spec),\n [duplicates.spec],\n );\n\n const handleOpen = useCallback(\n (object: Obj.Unknown) => {\n const id = Obj.getURI(object);\n void invokePromise(LayoutOperation.Select, { contextId: attendableId, subject: { mode: 'single', id } });\n void invokePromise(LayoutOperation.Open, {\n // Open under the node being viewed rather than the object's canonical type path, so an object\n // reached from a type section opens within that section. Identical for a database type node,\n // whose object path already is `<typeNode>/<id>`.\n subject: [GraphPath.getCollectionObjectPath(attendableId, object.id)],\n pivotId: attendableId,\n disposition: 'add',\n navigation: 'immediate',\n });\n },\n [attendableId, invokePromise],\n );\n\n // Both deletes go through the operation, whose undo mapping puts the objects back (a direct\n // `db.remove` would be silent and final); a removed object also leaves the selection.\n const removeObjects = useCallback(\n (removed: Obj.Unknown[]) => {\n if (removed.length === 0) {\n return;\n }\n void invokePromise(SpaceOperation.RemoveObjects, { objects: removed }, { spaceId: space.id });\n const removedIds = new Set(removed.map((object) => object.id));\n setSelectedObjects(selectedIds.filter((id) => !removedIds.has(id)));\n },\n [selectedIds, invokePromise, space.id, setSelectedObjects],\n );\n\n const handleDelete = useCallback((object: Obj.Unknown) => removeObjects([object]), [removeObjects]);\n\n const handleDeleteSelected = useCallback(\n () => removeObjects(objects.filter((object) => selectedIds.includes(object.id))),\n [objects, selectedIds, removeObjects],\n );\n\n // Stable identity: `DynamicTable` rebuilds its model whenever `features` changes, so an inline\n // literal here would discard and re-seed the table's selection on every render.\n const tableFeatures = useMemo(\n () => ({\n selection: { enabled: true, mode: 'multiple' as const },\n dataEditable: true,\n schemaEditable: false,\n pinColumns: 1,\n }),\n [],\n );\n\n // Table rows are editable, so opening a row is a deliberate row action rather than `onRowClick`\n // (which would fire on every cell click and fight with in-cell editing).\n const rowActions = useMemo(\n (): TableRowAction[] => [\n { id: 'open', label: ['open-object.label', { ns: meta.profile.key }] },\n ...(TypeOptions.isUserType(type)\n ? [\n {\n id: 'addToCollection',\n label: ['add-to-collection.label', { ns: osTranslations }],\n } satisfies TableRowAction,\n ]\n : []),\n ],\n [type],\n );\n\n const handleRowAction = useCallback(\n (actionId: string, object: Obj.Unknown) => {\n if (actionId === 'open') {\n handleOpen(object);\n } else if (actionId === 'addToCollection') {\n void invokePromise(CollectionOperation.OpenAddToCollection, { object });\n }\n },\n [handleOpen, invokePromise],\n );\n\n // One action graph for the whole toolbar: the mode-specific actions first, then the layout toggle.\n // Composing several `Toolbar.Root`s instead would each need their own roving-focus group and would\n // fight over the row's width.\n const menuActions = useMenuBuilder(\n () =>\n MenuBuilder.make()\n .subgraph(layout === 'duplicates' && duplicatesGroup)\n .subgraph(\n layout === 'table' &&\n selectedIds.length > 0 &&\n ((builder) => {\n builder.action(\n 'delete-selected',\n {\n label: ['delete-selected.label', { ns: meta.profile.key, count: selectedIds.length }],\n icon: 'ph--trash--regular',\n },\n handleDeleteSelected,\n );\n }),\n )\n .separator()\n .group(\n 'layout',\n {\n variant: 'toggleGroup',\n selectCardinality: 'single',\n value: layout,\n label: ['layout.label', { ns: meta.profile.key }],\n },\n (group) => {\n layouts.forEach(({ value, icon }) => {\n group.action(value, { label: [`layout-${value}.label`, { ns: meta.profile.key }], icon }, () =>\n setLayout(value),\n );\n });\n },\n )\n .build(),\n [layout, layouts, selectedIds.length, duplicatesGroup, handleDeleteSelected],\n );\n\n // The card grid backs two layouts — every object for `masonry`, only the group under review for\n // `duplicates` — so the items are resolved here and the grid itself is declared once.\n const tiles = layout === 'duplicates' ? duplicates.current : results;\n const tileItems = useMemo<TileData[]>(() => {\n const toTile = (object: Obj.Unknown): TileData => ({\n object,\n current: selectedIds.includes(object.id),\n onSelect: toggleSelected,\n onOpen: handleOpen,\n onDelete: Obj.getParent(object) ? undefined : handleDelete,\n });\n // While a merge is staged the participants fold into one read-only result card; members the\n // selection excluded stay behind as normal cards.\n if (layout === 'duplicates' && stagedPreview) {\n const participants = new Set(stagedPreview.objectIds);\n return [\n { object: stagedPreview.preview, current: true },\n ...tiles.filter((object) => !participants.has(object.id)).map(toTile),\n ];\n }\n return tiles.map(toTile);\n }, [layout, stagedPreview, tiles, selectedIds, toggleSelected, handleOpen, handleDelete]);\n\n // A type with no objects at all has nothing for any layout to show; past that each layout says\n // when it is empty for its own reason.\n const noObjects = objects.length === 0 ? t('type-collection-empty.message') : undefined;\n const noResults = noObjects ?? (results.length === 0 ? t('search-no-results.message') : undefined);\n const noDuplicates =\n noObjects ??\n (duplicates.current.length === 0\n ? duplicates.scanning\n ? t('duplicates-scanning.message')\n : t('duplicates-none.message')\n : undefined);\n\n return (\n <SearchList.Root onSearch={handleSearch}>\n <Tabs.Root asChild value={layout} onValueChange={(value) => setLayout(value as Layout)} orientation='vertical'>\n <Panel.Root role={role}>\n <Panel.Header classNames={mx('grid', layout !== 'duplicates' && 'grid-cols-[1fr_auto]')}>\n {layout !== 'duplicates' && (\n <Toolbar.Root>\n <SearchList.Input placeholder={t('search-placeholder.label')} />\n </Toolbar.Root>\n )}\n <ActionToolbar {...menuActions} attendableId={attendableId} alwaysActive />\n </Panel.Header>\n <Panel.Body>\n <LayoutPanel value='masonry' empty={noResults}>\n <ObjectMasonry cacheKey={typeUri} items={tileItems} />\n </LayoutPanel>\n <LayoutPanel value='table' empty={noResults}>\n <DynamicTable\n type={type}\n rows={results}\n features={tableFeatures}\n rowActions={rowActions}\n // Seed from the shared selection so switching card → table keeps what was selected;\n // the table otherwise starts from its own empty model.\n selection={selectedIds}\n onRowAction={handleRowAction}\n onSelectionChanged={setSelectedObjects}\n />\n </LayoutPanel>\n {duplicates.spec && (\n <LayoutPanel value='duplicates' empty={noDuplicates}>\n <ObjectMasonry cacheKey={typeUri} items={tileItems} />\n </LayoutPanel>\n )}\n </Panel.Body>\n <Panel.Footer classNames='items-center p-1 border-t border-separator-subtle'>\n {t('item-count.label', { count: tileItems.length })}\n </Panel.Footer>\n </Panel.Root>\n </Tabs.Root>\n </SearchList.Root>\n );\n};\n\n/** One layout's content, or the message standing in for it when the layout has nothing to show. */\nconst LayoutPanel = ({ value, empty, children }: PropsWithChildren<{ value: Layout; empty?: string }>) => (\n <Tabs.Content value={value} classNames='contents'>\n {empty ? <Status.Empty classNames='h-full'>{empty}</Status.Empty> : children}\n </Tabs.Content>\n);\n\nTypeArticle.displayName = 'TypeArticle';\n"],"mappings":"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"}
@@ -9,6 +9,7 @@ import { Annotation, Collection, Database, Entity, Filter, Obj, Query, Scope, Ty
9
9
  import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
10
10
  import { SpaceCapabilities, SpaceEvents, SpaceOperation, SpaceSchema } from "#types";
11
11
  import { meta } from "#meta";
12
+ import { SPACE_STATS_QUERY, typeUrisWithObjects } from "#dashboard";
12
13
  import * as Atom from "effect/reactivity/Atom";
13
14
  import { createFilename, downloadBlob, isNonNullable } from "@dxos/util";
14
15
  import * as Plugin from "@dxos/app-framework/Plugin";
@@ -80,6 +81,7 @@ var buildViewIndex = (get, space, schemas) => {
80
81
  const viewSchemas = schemas.filter((schema) => ViewAnnotation.has(schema));
81
82
  const viewsByTypeUri = /* @__PURE__ */ new Map();
82
83
  const viewObjectIds = /* @__PURE__ */ new Set();
84
+ const typeUrisWithNonViewObjects = /* @__PURE__ */ new Set();
83
85
  if (viewSchemas.length > 0) {
84
86
  const filter = Filter.or(...viewSchemas.map((schema) => Filter.type(schema)));
85
87
  const viewObjects = get(space.db.query(filter).atom);
@@ -91,6 +93,7 @@ var buildViewIndex = (get, space, schemas) => {
91
93
  let holder = get(Obj.atom(viewObject));
92
94
  for (const segment of path) holder = holder == null || typeof holder !== "object" ? void 0 : holder[segment];
93
95
  if (holder != null) viewObjectIds.add(viewObject.id);
96
+ else if (holderType) typeUrisWithNonViewObjects.add(Type.getURI(holderType));
94
97
  const viewTarget = holder != null ? get(Obj.atom(holder)) : void 0;
95
98
  const typeUri = getTypeURIFromQuery(viewTarget?.query?.ast);
96
99
  if (typeUri) {
@@ -103,7 +106,8 @@ var buildViewIndex = (get, space, schemas) => {
103
106
  return {
104
107
  typeUrisWithViews: new Set(viewsByTypeUri.keys()),
105
108
  getViewsForTypeUri: (typeUri) => viewsByTypeUri.get(typeUri) ?? [],
106
- isView: (object) => viewObjectIds.has(object.id)
109
+ isView: (object) => viewObjectIds.has(object.id),
110
+ typeUrisWithNonViewObjects
107
111
  };
108
112
  };
109
113
  //#endregion
@@ -510,12 +514,12 @@ var createDatabaseExtensions = Effect.fnUntraced(function* () {
510
514
  return true;
511
515
  });
512
516
  const viewIndex = buildViewIndex(get, space, allSchemas);
517
+ const withObjects = typeUrisWithObjects(get(space.db.query(SPACE_STATS_QUERY).atom));
513
518
  const visibleSchemas = userSchemas.filter((schema) => {
514
519
  if (Type.getDatabase(schema) != null) return true;
515
520
  const typeUri = Type.getURI(schema);
516
- const objects = get(space.db.query(Filter.type(typeUri)).atom);
517
- if (ViewAnnotation.has(schema)) return objects.some((obj) => !viewIndex.isView(obj));
518
- return objects.length > 0 || viewIndex.typeUrisWithViews.has(typeUri);
521
+ if (ViewAnnotation.has(schema)) return viewIndex.typeUrisWithNonViewObjects.has(typeUri);
522
+ return withObjects.has(typeUri) || viewIndex.typeUrisWithViews.has(typeUri);
519
523
  });
520
524
  const translator = get(capabilities.atom(AppCapabilities.Translator)).at(0);
521
525
  const labelOf = (node) => {