Docs
Display and explore

Saved views

Save arrangements, share team views, and choose a personal favorite.

A saved view is a serializable snapshot of the table’s arrangement. Keep it separate from the configuration catalog and from the records it displays.

Per-view permissions

TableView.canEdit and TableView.canDelete let the host describe independent permissions for the current actor. A value of false disables the corresponding saved-view action in React and Vue. Omitted flags preserve the existing non-system behavior. System views and allowViewSave: false remain read-only.

A shared view can stay selectable, copyable and eligible as a personal favorite without granting write access. Return these flags from the list/create/update actions using the current owner and workspace. The local adapter preserves and respects explicit flags; remote actions must enforce their own current authorization and concurrent-update checks. UI flags are not an authorization boundary.

Keep the favorite in a per-user preference, separate from the shared view definition. A favorite must not silently rename, replace or become the organization-wide default of a colleague's view.

Consistent display controls

Density choices use normal-weight text, including the selected choice, with selection indicated by its background and accessible pressed state. The Display mode control is a labelled dropdown (React Base UI Select, Vue TableSelect); compact toolbars and touch drawers keep the wrapping buttons instead. Gallery, Kanban and Gantt settings use compact fields and a single drawer on mobile, including nested choices. On touch layouts, layout and density are rows showing their current value that open the same list-of-choices screen as properties, filter, sort and group. Share lives in the Data menu next to Export and Connect, matches its 32px desktop height, and keeps a minimum 44px touch target on compact layouts.

Configuration example

Start from the React quick start or Vue quick start. The following file extends their product catalog. Pass this configuration through getTableConfig in React or config in Vue.

view-actions.ts
import type {
  TableView,
  TableViewActions,
} from "@/components/ui/yayaw-table/types/view-types";

/** Demo persistence lasts for this module's lifetime. Scope server storage by user and organization. */
const views = new Map<string, TableView>();
const favorites = new Map<string, string | null>();
export const viewActions: TableViewActions = {
  list: ({ tableId }) =>
    Promise.resolve({
      data: [...views.values()].filter((view) => view.tableId === tableId),
    }),
  create: (input) => {
    const view = {
      ...input,
      id: crypto.randomUUID(),
      createdById: "demo-user",
    };
    views.set(view.id, structuredClone(view));
    return Promise.resolve({ success: true, data: view });
  },
  update: (id, input) => {
    const current = views.get(id);
    if (!current || (input.tableId && input.tableId !== current.tableId)) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    const view = { ...current, ...input };
    views.set(id, structuredClone(view));
    return Promise.resolve({ success: true, data: view });
  },
  delete: (id, { tableId }) => {
    if (views.get(id)?.tableId !== tableId) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    views.delete(id);
    if (favorites.get(tableId) === id) {
      favorites.delete(tableId);
    }
    return Promise.resolve({ success: true, data: { id } });
  },
  getFavorite: ({ tableId }) =>
    Promise.resolve({
      success: true,
      data: { viewId: favorites.get(tableId) ?? null },
    }),
  setFavorite: (viewId, { tableId }) => {
    if (viewId && views.get(viewId)?.tableId !== tableId) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    favorites.set(tableId, viewId);
    return Promise.resolve({ success: true, data: { viewId } });
  },
};
view-actions.ts
import type {
  TableView,
  TableViewActions,
} from "@/components/ui/yayaw-table-vue/types";

/** Demo persistence lasts for this module's lifetime. Scope server storage by user and organization. */
const views = new Map<string, TableView>();
const favorites = new Map<string, string | null>();
export const viewActions: TableViewActions = {
  list: ({ tableId }) =>
    Promise.resolve({
      data: [...views.values()].filter((view) => view.tableId === tableId),
    }),
  create: (input) => {
    const view = {
      ...input,
      id: crypto.randomUUID(),
      createdById: "demo-user",
    };
    views.set(view.id, structuredClone(view));
    return Promise.resolve({ success: true, data: view });
  },
  update: (id, input) => {
    const current = views.get(id);
    if (!current || (input.tableId && input.tableId !== current.tableId)) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    const view = { ...current, ...input };
    views.set(id, structuredClone(view));
    return Promise.resolve({ success: true, data: view });
  },
  delete: (id, { tableId }) => {
    if (views.get(id)?.tableId !== tableId) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    views.delete(id);
    if (favorites.get(tableId) === id) {
      favorites.delete(tableId);
    }
    return Promise.resolve({ success: true });
  },
  getFavorite: ({ tableId }) =>
    Promise.resolve({
      success: true,
      data: { viewId: favorites.get(tableId) ?? null },
    }),
  setFavorite: (viewId, { tableId }) => {
    if (viewId && views.get(viewId)?.tableId !== tableId) {
      return Promise.resolve({ success: false, error: "View not found." });
    }
    favorites.set(tableId, viewId);
    return Promise.resolve({ success: true, data: { viewId } });
  },
};

Connect the actions

Attach viewActions as getTableActions(tableType).views. Enable enableViews and allowViewSave; enable allowViewSharing only when the application supports organization-scoped sharing. The example stores views in memory and resets on reload. Use a database for persistent production views. The record recipe wires the complete flow.

What a view stores

A view (TableViewConfig) can restore the search (globalSearch), column and advanced filters, sorting, grouping, column visibility, order, sizing and pinning, density, whether footer calculations are shown, page size, the display mode and the settings of each display mode: kanban, gallery, list, filetree, calendar, chart, feed, map, form and gantt. Store values and stable column IDs. Do not serialize callbacks, renderers, permissions or query clients.

Date rules are saved and shared as calendar days (YYYY-MM-DD). Views saved by versions before v3.8.0 keep the instants they were saved with until they are saved again, and still open on the same days: the table reads those instants as the viewer's days, without marking the view modified.

Favorites and sharing

isGlobal describes a shared view within the host organization scope. getFavorite and setFavorite manage one personal arrival view without changing the shared view. Clear a dangling favorite when its view is deleted or becomes inaccessible. URL state can represent a temporary arrangement; see URL state.

Edit or restore a view

Open the view menu to select a view, choose a favorite, change settings, save, reset or delete. Mode and the six table densities are direct controls. Filters, sort, grouping and properties have dedicated screens with Back navigation. Disabling enableViews keeps a View button for settings.

The blue dot indicates only a difference from the active saved snapshot. A saved filtered view opens without a dot. Density, mode, search, filters, sorting, grouping, columns, card settings, page size and footer visibility participate in the same comparison. Selection and the current page do not. Returning manually to the saved configuration clears the dot.

Save changes stays visible but disabled when unchanged. Desktop explains this on hover and keyboard focus; mobile displays the explanation beneath the action. Save as new view creates a separate snapshot. A temporary view offers Save this view… and has no saved-view dot. Save errors retain the draft; favorites remain available without write permission.

Reset view, identified by Lucide ListRestart, restores the saved snapshot or the application's initial configuration for a temporary view. It never changes records or deletes the view. showClearFilters and its historical alias showResetFilters still clear only filtering inputs inside the Filters screen.

The shared snapshot now includes footerCalculationsVisible?: boolean. Omitted values in older views inherit the initial visible setting; enableCalculations still gates the feature. Keep settings for inactive presentation modes when persisting snapshots.

For example, open an ungrouped saved view filtered to Open, group it by Status, and change its density. Reset view removes that added group, restores the saved density (or the configured default for an older view), keeps the Open filter, and clears the blue dot. A Kanban lane stored in an inactive presentation does not introduce grouping into a table view. Resetting a temporary view also removes added groups.

View tabs

The toolbar separates the view switcher, on the left, from view settings, on the right. Saved views show as tabs in the switcher on wide screens, as soon as the table has at least one saved view. The default view is always the first tab; each tab shows the icon of its layout (display mode) and a dot when the active view has unsaved changes. Clicking a tab applies the view.

When there are more views than table.viewTabs.maxVisible (default 4), the rest move under …, an icon button named "More views" (with the same tooltip) whose menu shows their full names; the active view always stays visible, taking the place of the last visible tab. + ("New view") opens the save dialog and only shows when views can be created (allowViewSave and a create action). The save dialog offers a Layout choice — the display modes the table offers, the current one by default; creating a view in another layout switches to it.

With tabs, a View actions chevron next to them holds save changes, save as new view, favorite, Move left and Move right (see Order of views), reset and delete. Compact toolbars and touch layouts replace the tabs with an icon-only trigger — no visible label or chevron, so search keeps the room on the row — with an accessible name built from views.current ("Current view" by default) followed by the view name, and a small dot for unsaved changes. Its menu lists the views — scrollable, with a Find a view filter beyond seven — followed by the same actions. Set table.viewTabs: false to keep the named trigger (icon, view name and chevron) even on wide screens.

Order of views

Each person orders their own saved views. The view menu moves the current saved view one step: Move left and Move right next to the tabs, Move up and Move down where the menu lists the views (phones and viewTabs: false), right after the favorite. At either end the action stays focusable but inactive (aria-disabled), so the focus stays on it, and each move is announced to screen readers, for example "View “Sales” moved to position 3 of 6" (the position counts the built-in default view first). The order applies to the tabs, the … list and the menu's list of views; the view a table opens on does not depend on it.

  • System views (isSystem) and the default view (isDefault, such as a dashboard screen's own view) stay first, in their list order, and have no move actions.

  • Views the order does not name, such as new ones, come last in their list order. Unknown ids are ignored, and with fewer than two views to order there is nothing to move.

Keep the order on your server with the optional views.setOrder action. It receives the person's complete new order, first to last, without system and default views; store it as it comes, per user, organization, table type and table id, and answer it back from list as order. The table sorts the views with it, with the rules above, so your server does not sort (a server may instead list the views already in that order and leave order out). orderViews(views, order) from utils/view-order.ts sorts the same way when your server needs it. A refused write ({ success: false, error }) keeps the previous order and shows the error in the view manager.

// Beside list, create, update, delete, getFavorite and setFavorite.
const orders = new Map<string, string[]>(); // per user, organization, table type and table id

export const viewActions: TableViewActions = {
  list: async ({ tableId, tableType }) => ({
    data: await listViews(tableId),
    order: orders.get(orderKey(tableType, tableId)),
  }),
  setOrder: ({ tableId, tableType, viewIds }) => {
    orders.set(orderKey(tableType, tableId), viewIds);
    return Promise.resolve({ success: true, data: { viewIds } });
  },
};

The action and the list answer (TableViewListResult, { data, order? }) are the same in React and Vue; Vue's list may still answer a bare array. Without setOrder, the order stays in the browser's localStorage under yayaw-table-view-order:<JSON [tableType, tableId]>, the favorite's scope, shared by every manager of that table on the page. createLocalTableViewActions() has no setOrder (its type is LocalTableViewActions), so tables using it keep this fallback.

Share lives in the Data menu, opened from the database-icon button next to View settings, alongside Export and Connect. Without custom share destinations it copies the exact current URL directly on desktop (mobile invokes native sharing when available and falls back to copying; cancellation is silent). This action does not grant permissions or change isGlobal. Organization sharing remains a separate save-dialog option. Set table.share: false to hide the Share row.

Custom connect and share destinations declared in actions.destinations (webhooks, n8n, connectors) get their own rows in the same Data menu: connect destinations under Connect › (hidden when there are none), share destinations under Share › after the built-in "Copy link" — with share destinations declared, Share opens that screen instead of copying directly. See Custom destinations.

Enable saved arrangements

Wire the viewActions adapter shown above into your table actions as views. Save a filtered view, reopen it, change density and reset: the saved filter remains and the modified indicator disappears.

Copy this complete configuration beside product-config.ts. Use () => exampleConfig as React’s getTableConfig, or :config="exampleConfig" in Vue.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.

Keep settings without saved views

Use the View button for filters, sort, properties and density without exposing a saved-view catalog. Reset restores the application’s initial arrangement.

Copy this complete configuration beside product-config.ts. Use () => exampleConfig as React’s getTableConfig, or :config="exampleConfig" in Vue.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.