import { generateId, type ObjectScope, type SortOrder } from './_helpers'; import type { CustomObject, CustomObjectFieldInput } from './custom-object'; import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { Field } from './field'; import type { ManifestComponentDeletion } from './manifest-builder'; import { optionalStringComponentField, requireArrayComponentField, requireBooleanComponentField, requireStringComponentField, } from './_existing-component-fields'; export interface ListViewDeletionIdentifier { viewId: string; objectRqlName: string; } /** * Per-column display configuration for a list view. */ export interface ListViewColumnConfig { /** * Relative display width for this column. */ widthRatio: number; } /** * Initialization properties for {@link ListViewDef}. */ export interface ListViewDefProps { /** * Display name of this list view shown in the UI (e.g. `'All members'`). * * Required. */ name: string; /** * Columns to display, as live {@link Field} references or standard field names. At least one field is required. * * Required. */ fields: CustomObjectFieldInput[]; /** * Whether this view is visible to all users (`true`) or only the record owner (`false`). * * Required. */ isPublic: boolean; /** * Stable identifier for this view. Pass a human-readable slug * (e.g. `'view_gym_all_members'`) to keep it stable across regenerations. * * Optional. Auto-generated when omitted. @default generateId('view') */ viewId?: string; /** * Field to sort by. Required when `sortOrder` is set. * * Optional. */ orderBy?: CustomObjectFieldInput; /** * Sort direction. One of `'ASC'` | `'DESC'`. Requires `orderBy` to be set. * * Optional. */ sortOrder?: SortOrder; /** * Record scope for this view. One of `'all'` | `'my_records'`. * * Optional. @default `'all'` */ objectScope?: ObjectScope; /** * Human-readable description of this view. * * Required. */ description: string; /** * Per-column configuration overrides keyed by field API name. * * Only `widthRatio` is currently supported. Presentation hints such as * badge/status rendering are not list-view column config options. * * Optional. @default `{}` */ columnConfig?: Record; /** * RQL filter expression to pre-filter records shown in this view * (e.g. `'(session_date__c == TODAY())'`). * * Optional. @default null */ rqlFilter?: string; } export interface ListViewDefLoadFromExistingOptions extends ExistingManifestContextOptions { customObjectApiName?: string; } /** * Defines a list view and registers it with the manifest. * * A list view defines which fields appear as columns, their sort order, and an * optional RQL filter. You can define multiple views per custom object; each * appears as a selectable view in the object's list page. * * @example * ```ts * new ListViewDef(memberObj, { * viewId: 'view_gym_all_members', * name: 'All members', * description: 'Directory with membership status', * fields: ['name', memberEmail, memberPhone, memberStatusField, memberJoinDate], * orderBy: 'name', * sortOrder: 'ASC', * isPublic: true, * objectScope: 'all', * }); * * // Filtered view example: * new ListViewDef(sessionObj, { * viewId: 'view_gym_today_sessions', * name: 'Today\'s class sessions', * fields: [sessionTrainerRef, sessionStartField, sessionClassTypeField], * orderBy: sessionStartField, * sortOrder: 'ASC', * isPublic: true, * objectScope: 'all', * rqlFilter: '(session_date__c == TODAY())', * }); * ``` */ export class ListViewDef { static readonly componentType = 'CUSTOM_OBJECT_LIST_VIEW' as const; static toDeletionIdentifier(identifier: ListViewDeletionIdentifier): ManifestComponentDeletion { return { type: ListViewDef.componentType, view_id: identifier.viewId, object_rql_name: identifier.objectRqlName, }; } private readonly _viewId: string; private readonly _customObjectApiName: string; private readonly _name: string; private readonly _fields: Field[]; private readonly _isPublic: boolean; private readonly _orderBy: Field | undefined; private readonly _sortOrder: SortOrder | undefined; private readonly _objectScope: ObjectScope | undefined; private readonly _description: string; private readonly _columnConfig: Record | undefined; private readonly _rqlFilter: string | undefined; /** * @param customObject - The custom object this view belongs to. * @param props - Initialization properties. * @throws {Error} If `name` is empty, `fields` is empty, or `sortOrder` is set without `orderBy`. */ constructor(customObject: CustomObject, props: ListViewDefProps) { if (!props.name) throw new Error('ListViewDef: name cannot be empty'); if (!props.fields || props.fields.length === 0) { throw new Error('ListViewDef: must have at least one field'); } if (props.sortOrder && !props.orderBy) { throw new Error('ListViewDef: sortOrder requires orderBy to be set'); } this._viewId = props.viewId ?? generateId('view'); this._customObjectApiName = customObject.getApiName(); this._name = props.name; this._fields = props.fields.map((field) => customObject._resolveField(field)); this._isPublic = props.isPublic; this._orderBy = props.orderBy == null ? undefined : customObject._resolveField(props.orderBy); this._sortOrder = props.sortOrder; this._objectScope = props.objectScope ?? 'all'; this._description = props.description; this._columnConfig = props.columnConfig; this._rqlFilter = props.rqlFilter; customObject._register(this); } /** * Loads this list view from the active existing-manifest JSON context. */ static loadFromExisting(viewId: string, options: ListViewDefLoadFromExistingOptions = {}): ListViewDef { return getExistingManifestContext(options).loadListView(viewId, options); } /** @internal Hydrates a list view from existing manifest wire JSON. */ static _fromExistingComponent( customObject: CustomObject, component: Record, resolveField: (apiName: string) => Field, ): ListViewDef { const props: ListViewDefProps = { viewId: requireStringComponentField(component, 'view_id', ListViewDef.componentType), name: requireStringComponentField(component, 'name', ListViewDef.componentType), description: optionalStringComponentField(component, 'description'), fields: requireArrayComponentField(component, 'fields', ListViewDef.componentType).map( (fieldApiName: string) => resolveField(fieldApiName), ), isPublic: requireBooleanComponentField(component, 'public', ListViewDef.componentType), }; if (component['object_scope'] !== undefined) props.objectScope = component['object_scope']; if (component['sort_order'] !== undefined) props.sortOrder = component['sort_order']; if (component['column_config'] !== undefined) props.columnConfig = component['column_config']; if (component['rql_filter'] !== undefined) props.rqlFilter = component['rql_filter']; if (component['order_by'] != null) props.orderBy = resolveField(component['order_by']); return new ListViewDef(customObject, props); } /** * Returns the stable identifier for this view (e.g. `'view_gym_all_members'`). */ getViewId(): string { return this._viewId; } /** * Returns the api_name of the custom object this view belongs to. */ getCustomObjectApiName(): string { return this._customObjectApiName; } /** * Serializes this list view to the wire format consumed by the manifest install endpoint. * * @returns A plain object with `type: 'CUSTOM_OBJECT_LIST_VIEW'`. * Optional fields are omitted when unset. */ toDict(): Record { const d: Record = { type: ListViewDef.componentType, object_rql_name: this._customObjectApiName, view_id: this._viewId, name: this._name, public: this._isPublic, fields: this._fields.map((f) => f.getApiName()), description: this._description, }; if (this._objectScope != null) d['object_scope'] = this._objectScope; if (this._sortOrder != null) d['sort_order'] = this._sortOrder; if (this._orderBy != null) d['order_by'] = this._orderBy.getApiName(); if (this._columnConfig != null) d['column_config'] = this._columnConfig; if (this._rqlFilter != null) d['rql_filter'] = this._rqlFilter; return d; } }