Skip to content

Replace selectComponent with a view registry #1899

Description

@michielbdejong

Problem

Which component renders something is decided in several unrelated places, each with its own hard-coded mapping:

  • Page: selectComponent in browser/data-browser/src/views/ResourcePage.tsx:276, a switch from class URL to page component (Table, Document, Canvas, Folder, Dashboard, …), falling back to ResourcePageDefault.
  • Fallbacks after that switch (ResourcePage.tsx:185-255): Website / WebsiteExport, drive apps, plugin scripts, and getPluginForClass (iframe PluginView). Apps and plugins can't be cases because their classes are minted per drive, so they are special-cased around the switch.
  • The generic edit form: a separate route, /app/edit (routes/EditRoute.tsx → ResourceForm), reached through editURL() (helpers/navigation.tsx:80) from the Edit action and its shortcut (actions/resourceActions.tsx:127) and five other places.
  • Card, Inline and row views (views/Card/, views/ResourceInline/, ResourceRow.tsx) each have their own class → component mapping.
  • Folder grid tiles: gridItemMap in views/FolderPage/GridItem/ResourceGridItem.tsx:33 (Bookmark, Class, Property, ChatRoom, Document, DocumentV2, File, Article → tile component).
  • Resources embedded in a document: chunks/RTE/ResourceExtension/ResourceComponent.tsx:19.
  • Table view tabs: chunks/TablePage/tableViewKinds.ts (VIEW_KINDS: table, kanban, calendar, timer, dashboard, issues) plus drive apps offered through appsForClass(useDriveApps(drive), rowClass) in TableViewTabs.tsx:111. This is a second registry, and plugin views plug in here today, not in ResourcePage.
  • Folder list / grid: viewMap in views/FolderPage/index.tsx, keyed by DisplayStyle URL.
  • Ontology read / edit: one page with a local editMode state and an Edit/Read button (views/OntologyPage/OntologyPage.tsx).

Choosing between views is also done differently in each place:

URL ?view= Local override Stored value on the resource Default
Table tabs (useTableView.ts:220) yes no default-view first tab
Folder list / grid no yes (folderDisplayPrefs in localStorage) display-style List
Ontology read / edit no no no edit if the ontology is empty
Everything else no no no only one view is reachable

Other problems:

  • Only the first class is considered (isA = isAList[0], ResourcePage.tsx:93), so a resource with several classes gets the view of whichever class happens to be listed first, and the views of its other classes can't be reached at all.
  • Adding a view means editing ResourcePage.tsx or tableViewKinds.ts and importing the view there. Views can't be added from outside the data browser, which blocks moving them into their own package (see "Why" below).
  • A plugin view has to choose which mechanism it plugs into, so it can't be offered both as a table tab and as a page view.

A registry pattern already exists for new-resource dialogs (registerNewResourceDialog, components/forms/NewForm/CustomCreateActions/CustomForms/index.ts).

Proposal: one registry

One registry keyed by class URL and view type. The view types are page, card, grid-item, inline, row, embed, and rows: a view of a collection of resources (a table's rows, a folder's children), looked up by the row class.

registerView({
  id: 'table',
  type: 'page',
  class: dataBrowser.classes.table,
  component: lazy(() => import('@chunks/TablePage').then(m => ({ default: m.TablePage }))),
});

// Table tab kinds are `rows` views. Their ids are today's `view-kind` strings.
registerView({
  id: 'calendar',
  type: 'rows',
  match: ({ columns }) => columns.some(isDateProperty), // e.g. calendar needs a date column
  component: lazy(() => import('@chunks/TablePage/Calendar/CalendarView')),
});

// Per-drive classes (drive apps, plugins) register through a matcher instead of a fixed URL,
// replacing both the ResourcePage special cases and `appsForClass`.
registerView({ id: app.subject, type: 'rows', match: ({ rowClass }) => app.classes.includes(rowClass), component: AppView });

// The generic edit form: every resource, for anyone who can write it, lowest priority.
registerView({ id: 'resource-form', type: 'page', match: ({ canWrite }) => canWrite, priority: -Infinity, component: ResourceFormView });

resolveViews(resource, 'page');          // every applicable page view, in priority order
resolveViews({ rowClass, columns }, 'rows'); // every applicable table tab kind

Choosing the active view: one rule everywhere

Decided by Michiel. Wherever more than one view applies (a resource page, a table's tabs, a folder), one hook, e.g. useActiveView(resource, type), picks the active one in this order:

  1. ?view= in the URL. A link shows what its sender saw, a reload keeps it, back/forward moves between views. Tables already work this way; ShowRoute already has the parameter.
  2. Local override, per resource, in localStorage. This is the folder behaviour, now for everything. The old folderDisplayPrefs key is read once so existing choices survive.
  3. Stored value on the resource, shared with everyone: a table's default-view, a folder's display-style.
  4. Class default: the first applicable view from resolveViews, with an optional per-view default rule (e.g. an empty ontology opens in its edit view).

Rules:

  • Every step is checked against resolveViews. A deleted tab, an uninstalled app, lost write access or an old link falls through to the next step; it never shows an empty page.
  • Switching views sets the URL and the local override, never the resource. Only an explicit "Set as default" action writes the stored value. Tables already have this; folders get the same action (today their toggle writes only localStorage and the shared display-style can't be changed from the page).
  • The value in steps 1–3 identifies a view within that resource's options: for a table, the View resource's subject (tabs are resources, as today); for a folder, the DisplayStyle URL; otherwise the registry id.

Ontology: two views instead of a mode

OntologyPage becomes two registered page views for the Ontology class, ontology-read (class and property cards plus the graph) and ontology-edit (the *Write cards and New buttons, requires write access). The Edit/Read button becomes the generic view switcher, so edit mode is linkable (?view=ontology-edit) and remembered per browser. OntologyContextProvider wraps both. The "empty ontology opens in edit" behaviour stays, as the edit view's default rule in step 4.

The generic edit form becomes a view

Decided by Michiel. ResourceForm is registered as the page view resource-form: it applies to every resource the viewer can write, with the lowest priority, so it is never the default but is always reachable from the view switcher. An ontology then offers three views: read, the ontology editor and the generic form.

  • editURL() returns the resource's own URL with ?view=resource-form, so the Edit action, its keyboard shortcut and the five other callers change in one place.
  • /app/edit?subject=… keeps working: the route redirects to ?view=resource-form, so existing links and bookmarks still open the form.
  • Opening the form this way sets only the URL (step 1), not the local override, so a quick edit doesn't make the form the remembered view for that resource. Worth confirming in review; if it should behave like any other switch, it just follows the general rule.

Storage is unchanged

A table tab is still a View resource that stores its kind as the view-kind string, plus its own configuration (filters, grouping, …). That string is now a registry id. A folder's display-style keeps its DisplayStyle URLs, which are the registry ids of the folder's list and grid views. Existing resources keep working with no migration.

Registry ids are stable

Ids are stored in View resources and folders, appear in URLs, and are referenced by app-view grants (#1788 / #1849 / #1888, scoped to "tables where this view is installed"). An id can never be renamed or reused once released; renaming it would orphan saved tabs, grants and links. Built-in table ids are exactly today's view-kind values, and a drive app's id stays its subject, as it is today (appViewOf in tableViewKinds.ts).

Steps

  1. Registry + page views. Add the registry and resolveViews, and move selectComponent and the Website/app/plugin special cases in ResourcePage.tsx onto it. No behaviour change.
  2. Active view selection. Add useActiveView with the four-step order; use it for page views and folders (list/grid as rows views, with the local override migrated), split the ontology into its two views, and register resource-form (with editURL() and the /app/edit redirect).
  3. Card, grid-item, inline, row and embed views onto the same registry.
  4. Table tab kinds, after Changing a view's kind replaces the only table view without warning #1806 lands. Replace VIEW_KINDS and appsForClass with rows registrations, make TableViewTabs list resolveViews(…, 'rows'), and move useTableView's selection onto useActiveView (which adds the local override for tables). Changing a view's kind replaces the only table view without warning #1806 only changes the menu's add-vs-convert logic, not the kind list, so it can land first.

Related

Why

This is the first step toward moving the built-in views (table, kanban, calendar, document, canvas, …) out of data-browser into their own package. A views package can export a single registerViews(registry) instead of ResourcePage.tsx and tableViewKinds.ts importing each view. It also lets apps and plugins register views the same way as built-ins, in one place, and makes switching between views behave the same everywhere.

Done when

  • selectComponent and the Website/app/plugin special cases in ResourcePage.tsx go through the registry.
  • useActiveView applies ?view= → local override → stored value → class default for page views, folders and table tabs, with each step checked against resolveViews; unit tests cover every step and the fall-through for a view that no longer applies.
  • Switching views never writes the resource; an explicit "Set as default" does, for tables and folders.
  • Existing folderDisplayPrefs choices are kept.
  • The ontology has separate read and edit views; readers never get the edit view, and an empty ontology still opens in edit.
  • ResourceForm is the resource-form page view for every writable resource; editURL() and the Edit shortcut open it via ?view=, and /app/edit links redirect to it.
  • Card, grid-item, Inline, row and RTE-embed views go through the same registry.
  • VIEW_KINDS and appsForClass are replaced by rows registrations; the tab menu lists resolveViews(…, 'rows') (after Changing a view's kind replaces the only table view without warning #1806).
  • resolveViews considers every class of a resource and returns all applicable views, with a test for a multi-class resource.
  • Existing View resources, folders and app-view grants keep working unchanged, with a test that loads a View saved under each current view-kind.
  • Unit tests for registration, resolution and fallback; the existing e2e suite stays green.
  • views/README.md documents how to register a view, the selection order, and the id-stability rule.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions