Documentation

List facet example

Build a typed backoffice list facet with columns, filters, sorts, and Relay data.

A list facet describes how a backoffice entity appears in list views: its query, fragment, rows, columns, filters, sorts, and default state.

import {
  createListFacet,
  type BackofficeColumnSpec,
  type BackofficeFilterSpec,
  type BackofficeListFacetConfig,
  type BackofficeSortSpec,
} from '@plumile/backoffice-core';

import { ProjectListPageQuery } from '../queries/ProjectListPageQuery.js';
import { ProjectListFragment } from '../queries/ProjectListFragment.js';

type ProjectWhere = {
  archived?: boolean;
  owner?: { id?: string };
};

type ProjectSort = 'CREATED_AT_DESC' | 'CREATED_AT_ASC';

type ProjectRowRef = {
  id: string;
  name: string;
  owner: { id: string; name: string } | null;
  createdAt: string;
};

type ProjectRow = {
  id: string;
  name: string;
  ownerName: string | null;
  createdAt: string;
};

const columns: readonly BackofficeColumnSpec<ProjectRow>[] = [
  {
    key: 'name',
    header: (t) => t('project.columns.name'),
    size: 'lg',
    cell: {
      type: 'link',
      value: (row) => row.name,
      to: (row) => `/projects/${row.id}`,
    },
  },
  {
    key: 'owner',
    header: (t) => t('project.columns.owner'),
    size: 'md',
    cell: { type: 'text', value: (row) => row.ownerName },
  },
];

const filters: readonly BackofficeFilterSpec<ProjectWhere>[] = [
  {
    id: 'ownerId',
    kind: 'entityId',
    entity: 'users',
    label: (t) => t('project.filters.owner'),
    toGraphQL: (value) => ({ owner: { id: value } }),
    fromGraphQL: (where) => where.owner?.id ?? null,
  },
];

const sorts: readonly BackofficeSortSpec<ProjectSort>[] = [
  { id: 'CREATED_AT_DESC', label: (t) => t('sort.newest') },
  { id: 'CREATED_AT_ASC', label: (t) => t('sort.oldest') },
];

export const projectListFacet = createListFacet({
  kind: 'list',
  id: 'projects',
  label: (t) => t('project.entity.plural'),
  list: {
    title: (t) => t('project.entity.plural'),
    query: ProjectListPageQuery,
    fragment: ProjectListFragment,
    getConnection: (data) => data.projects,
    toRow: (node: ProjectRowRef): ProjectRow => ({
      id: node.id,
      name: node.name,
      ownerName: node.owner?.name ?? null,
      createdAt: node.createdAt,
    }),
    getRowId: (row) => row.id,
    columns,
    filters,
    sorts,
    defaultState: { sort: 'CREATED_AT_DESC' },
  },
} satisfies BackofficeListFacetConfig<ProjectWhere, ProjectSort>);

Keep GraphQL-specific mapping in the facet module or a nearby helper. This keeps visual columns, filters, and URL state aligned with the list query.

For standalone URL-to-GraphQL filter mapping, see GraphQL where.