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.
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 } });
},
};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 the current link
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.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
enableViews: true,
allowViewSave: true,
syncUrl: true,
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue/config";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
enableViews: true,
allowViewSave: true,
syncUrl: true,
},
});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.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
enableViews: false,
enableAdvancedFilters: true,
enableSorting: true,
density: "medium",
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue/config";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
enableViews: false,
enableAdvancedFilters: true,
enableSorting: true,
density: "medium",
},
});