@navette-js/panels 1.0.0-beta.3 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -5,6 +5,10 @@ import { Action } from '@navette-js/actions';
5
5
  import { Step, Wizard } from '@navette-js/forms';
6
6
  import { Component as SchemaComponent, Schema, type SchemaHost } from '@navette-js/schemas';
7
7
  export { Notification, Notifications } from './notifications.ts';
8
+ export { MemorySessionStore, type SessionStore } from './session-store.ts';
9
+ export { csvCell, csvLine, DownloadStore, ExportAction, ExportBulkAction, ExportColumn, Exporter, ImportAction, ImportColumn, Importer, parseCsv, type ExportActionInstance, type ImportActionInstance } from './import-export.ts';
10
+ import { DownloadStore } from './import-export.ts';
11
+ import { type SessionStore } from './session-store.ts';
8
12
  import { type CalendarEvent, type CalendarLabels, type CalendarView } from './calendar.ts';
9
13
  export { addDays, addMonths, CALENDAR_LABELS, dayOf, daysBetween, eventsOn, renderCalendar, renderCalendarEvent, timeOf, visibleRange, type CalendarEvent, type CalendarLabels, type CalendarModel, type CalendarView } from './calendar.ts';
10
14
  import { type DatabaseNotificationStore, type GlobalSearchCategory, type GlobalSearchResult } from './topbar.ts';
@@ -71,6 +75,8 @@ export interface AuthProvider {
71
75
  markEmailAsVerified?(user: User): MaybePromise<void>;
72
76
  /** A value that changes with the stored password, so a used reset link becomes invalid. */
73
77
  getPasswordFingerprint?(user: User): MaybePromise<string>;
78
+ /** Can this user use this panel? Checked on every request and at login (`false`: 403, or "credentials do not match" at login). */
79
+ canAccessPanel?(user: User, panel: Panel): MaybePromise<boolean>;
74
80
  }
75
81
  /**
76
82
  * Panel tenants (teams, organizations…). Every panel page then lives under
@@ -97,7 +103,8 @@ export type PanelContext = {
97
103
  panel: Panel;
98
104
  user: User | null;
99
105
  mailbox: string;
100
- tenant?: unknown;
106
+ tenant?: unknown; /** Signed in through the shared session, but not allowed into this panel. */
107
+ denied?: boolean;
101
108
  };
102
109
  /**
103
110
  * Predefined panel theme: a set of colors (palettes or plain colors), optionally a font and some CSS.
@@ -114,6 +121,19 @@ export type Theme = {
114
121
  /** CSS added to the page for this theme (written by the panel developer, inserted as-is). */
115
122
  css?: string;
116
123
  };
124
+ /** Navigation group: label, icon (then its items have none, as in Filament) and whether it can be collapsed. */
125
+ export declare class NavigationGroup {
126
+ private _label;
127
+ private _icon;
128
+ private _collapsible;
129
+ constructor(label: string);
130
+ static make(label: string): NavigationGroup;
131
+ icon(name: string | null): this;
132
+ collapsible(condition?: boolean): this;
133
+ getLabel(): string;
134
+ getIcon(): string | null;
135
+ isCollapsible(): boolean;
136
+ }
117
137
  export declare class NavigationItem {
118
138
  label: string;
119
139
  _url: string;
@@ -149,10 +169,20 @@ export declare class Panel {
149
169
  private _login;
150
170
  private _auth;
151
171
  private _key;
172
+ private _sessionStore;
173
+ /** Where per-browser data is kept (persisted table filters, search and sort): in memory by default. */
174
+ sessionStore(store: SessionStore): this;
175
+ getSessionStore(): SessionStore;
176
+ private _downloads;
177
+ /** Where files offered for download (exports, failed import rows) are kept: in memory by default. */
178
+ downloads(store: DownloadStore): this;
179
+ getDownloads(): DownloadStore;
180
+ getDownloadUrl(token: string): string;
152
181
  private _pages;
153
182
  private _resources;
154
183
  private _navigationItems;
155
184
  private _navigationGroups;
185
+ private _navigationGroupOptions;
156
186
  private _maxContentWidth;
157
187
  readonly colors: ColorManager;
158
188
  static make(): Panel;
@@ -191,12 +221,38 @@ export declare class Panel {
191
221
  login(page?: typeof Login): this;
192
222
  auth(provider: AuthProvider): this;
193
223
  key(key: string): this;
224
+ private _authGuard;
225
+ private _canAccess;
226
+ /**
227
+ * Session shared with the other panels that use the same guard (`'web'` by default): signing into one signs into all of
228
+ * them, and `canAccess()` decides who may enter each one. `null` gives this panel a session of its own.
229
+ * Panels sharing a guard must use the same key and the same auth provider.
230
+ */
231
+ authGuard(guard: string | null): this;
232
+ getAuthGuard(): string | null;
233
+ /** Who may use this panel, e.g. `panel.canAccess((user) => user.role === 'admin')`. Checked on every request and at login. */
234
+ canAccess(rule: ((user: User, panel: Panel) => MaybePromise<boolean>) | null): this;
235
+ /** True if this user may use this panel: the panel's rule and the provider's `canAccessPanel()`, when set, must both allow it. */
236
+ userCanAccess(user: User): Promise<boolean>;
237
+ /** Name of the session cookie: per guard (shared), or per panel. */
238
+ getSessionCookieName(): string;
194
239
  pages(pages: (typeof Page)[]): this;
195
240
  /** Registers resources: their pages and navigation items. */
196
241
  resources(resources: (typeof Resource)[]): this;
197
242
  navigationItems(items: NavigationItem[]): this;
198
243
  /** Order of navigation groups. */
199
- navigationGroups(groups: string[]): this;
244
+ /** Order of the navigation groups: labels, or `NavigationGroup.make('Shop').icon('o-shopping-cart').collapsible(false)`. */
245
+ private _darkMode;
246
+ private _darkModeForced;
247
+ private _defaultThemeMode;
248
+ /** `false`: light mode only (no switcher); `darkMode(true, true)`: dark mode only. */
249
+ darkMode(condition?: boolean, isForced?: boolean): this;
250
+ /** Mode used until the user picks one in the switcher: `'system'` (default), `'light'` or `'dark'`. */
251
+ defaultThemeMode(mode: 'light' | 'dark' | 'system'): this;
252
+ hasDarkMode(): boolean;
253
+ hasDarkModeForced(): boolean;
254
+ getDefaultThemeMode(): 'light' | 'dark' | 'system';
255
+ navigationGroups(groups: (string | NavigationGroup)[]): this;
200
256
  private _plugins;
201
257
  /** Applies a plugin: an object that configures the panel (resources, pages, render hooks, themes…). */
202
258
  plugin(plugin: Plugin): this;
@@ -324,6 +380,25 @@ export declare class Panel {
324
380
  mailer(mailer: Mailer): this;
325
381
  sendMail(message: MailMessage): Promise<void>;
326
382
  getAuthKey(): string;
383
+ private _authSlugs;
384
+ private authSlug;
385
+ /** URL segment of the login page (`login` by default). */
386
+ loginRouteSlug(slug: string): this;
387
+ /** URL segment of the registration page (`register` by default). */
388
+ registrationRouteSlug(slug: string): this;
389
+ /** URL segments of the password reset pages: `<prefix>/<request>` and `<prefix>/<reset>` (`password-reset/request` and `password-reset/reset` by default). */
390
+ passwordResetRoutePrefix(prefix: string): this;
391
+ passwordResetRequestRouteSlug(slug: string): this;
392
+ passwordResetRouteSlug(slug: string): this;
393
+ /** Paths of the authentication pages, relative to the panel. */
394
+ getAuthPaths(): {
395
+ login: string;
396
+ registration: string;
397
+ passwordResetRequest: string;
398
+ passwordReset: string;
399
+ };
400
+ /** First URL segments used by the panel itself (never page slugs or tenant keys). */
401
+ getReservedSegments(): Set<string>;
327
402
  getRegistrationUrl(): string;
328
403
  getRequestPasswordResetUrl(): string;
329
404
  getResetPasswordUrl(user: User, fingerprint: string): string;
@@ -584,6 +659,8 @@ export declare class Page extends BasePage {
584
659
  context: Record<string, unknown>;
585
660
  data?: Record<string, unknown>;
586
661
  }[];
662
+ /** Files uploaded by rich editors (`componentFileAttachments.<state path>.<id>`), stored by the editor when it asks for their URL. */
663
+ componentFileAttachments: Record<string, unknown>;
587
664
  getActionMountContext(): Record<string, unknown>;
588
665
  protected getActionRecord(): unknown;
589
666
  /** Record targeted by an action mounted in a given context (table row…). */
@@ -594,6 +671,8 @@ export declare class Page extends BasePage {
594
671
  * Actions owned by a schema component (Repeater: add, delete, move; Section: header actions).
595
672
  * The key comes from the browser: it must match a real component of the schema, or the schema itself.
596
673
  */
674
+ /** Visible component of a schema of this page (or of a mounted action's form) with this key. */
675
+ protected findSchemaComponent(key: string): Promise<any | null>;
597
676
  protected getSchemaComponentActions(key: string): Promise<Action[]>;
598
677
  /** Context kept for a mounted action: the server's, never the browser's as-is. */
599
678
  protected resolveMountContext(clientContext: unknown): Record<string, unknown>;
@@ -632,6 +711,7 @@ export declare class Dashboard extends Page {
632
711
  /** Filters form state. Stays `null` while the page has no filters. */
633
712
  filters: Record<string, unknown> | null;
634
713
  /** Dashboard widgets. Defaults to the panel's (`panel.widgets([...])`). */
714
+ /** The panel's widgets, ordered by their `sort` (stable). */
635
715
  getWidgets(): (typeof Widget)[];
636
716
  /** Grid columns: a number (at the `lg` breakpoint), or one number per breakpoint. */
637
717
  getColumns(): number | Record<string, number>;
@@ -837,6 +917,10 @@ export declare abstract class Resource {
837
917
  /** Columns searched by global search; defaults to the title attribute. Empty array: the resource is not searchable. */
838
918
  static globallySearchableAttributes: string[] | null;
839
919
  static getGloballySearchableAttributes(): string[];
920
+ /** `false`: the resource is left out of global search. */
921
+ static isGloballySearchable: boolean;
922
+ /** `false`: the search term is matched as a whole instead of word by word. */
923
+ static shouldSplitGlobalSearchTerms: boolean;
840
924
  static canGloballySearch(): boolean;
841
925
  /** Position of this resource's group in the results; without one, after the others in registration order. */
842
926
  static globalSearchSort: number | null;
@@ -863,19 +947,21 @@ export type SubNavigationItem = {
863
947
  url: string;
864
948
  active: boolean;
865
949
  };
866
- type ColumnSpan = number | 'full';
950
+ type ColumnSpan = number | 'full' | Record<string, number | 'full'>;
867
951
  /** Widget: a component displayed in a page's widget grid. */
868
952
  export declare abstract class Widget extends Component {
869
953
  static componentName: string;
870
- /** Width in the widget grid: a number of columns, or `'full'`. */
954
+ /** Width in the widget grid: a number of columns or `'full'` (from `lg`), or a span per breakpoint (`{ md: 2, xl: 3 }`). */
871
955
  static columnSpan: ColumnSpan;
956
+ /** Position on the dashboard (lower first); widgets without a sort keep their order, after the sorted ones. */
957
+ static sort: number | null;
872
958
  /** Loaded when it becomes visible. */
873
959
  static isLazy: boolean;
874
960
  get panel(): Panel;
875
961
  /** Restricts the widget to some users: returning `false` removes it from the page and refuses to load it. */
876
962
  static canView(_user: User): boolean | Promise<boolean>;
877
963
  boot(): Promise<void>;
878
- /** Grid column classes and style, at the `lg` breakpoint. */
964
+ /** Grid column classes and style: at the `lg` breakpoint, or per breakpoint. */
879
965
  protected gridColumn(): {
880
966
  classes: string[];
881
967
  style: string;
@@ -1086,6 +1172,10 @@ export declare abstract class StatsOverviewWidget extends Widget {
1086
1172
  /** Polling interval, or `null` to disable polling. */
1087
1173
  static pollingInterval: string | null;
1088
1174
  abstract getStats(): Stat[] | Promise<Stat[]>;
1175
+ /** Heading shown above the stats. */
1176
+ protected getHeading(): string | null;
1177
+ /** Description under the heading. */
1178
+ protected getDescription(): string | null;
1089
1179
  /** Row columns: three, or four when the number of stats fits better. */
1090
1180
  protected getColumns(count: number): number;
1091
1181
  render(): Promise<string>;
@@ -1102,6 +1192,10 @@ export type ChartData = {
1102
1192
  * has changed (checksum).
1103
1193
  */
1104
1194
  export declare abstract class ChartWidget extends Widget {
1195
+ /** Empty state shown when `getData()` returns nothing. */
1196
+ protected getEmptyStateHeading(): string;
1197
+ protected getEmptyStateDescription(): string | null;
1198
+ protected getEmptyStateIcon(): string | null;
1105
1199
  static callable: string[];
1106
1200
  static locked: string[];
1107
1201
  /** Polling interval, or `null` to disable polling. */
@@ -1222,6 +1316,13 @@ export declare class Tab {
1222
1316
  badge(resolver: () => number | string | null | Promise<number | string | null>): this;
1223
1317
  badgeColor(color: string): this;
1224
1318
  query(modifier: (query: any) => void): this;
1319
+ private _icon;
1320
+ private _iconPosition;
1321
+ /** Icon of the tab, before its label (or after, with `iconPosition('after')`). */
1322
+ icon(name: string | null): this;
1323
+ iconPosition(position: 'before' | 'after'): this;
1324
+ getIcon(): string | null;
1325
+ getIconPosition(): 'before' | 'after';
1225
1326
  getLabel(key: string): string;
1226
1327
  hasBadge(): boolean;
1227
1328
  getBadge(): Promise<number | string | null>;
@@ -1253,16 +1354,29 @@ export declare class ListRecords extends Page implements TableHost {
1253
1354
  toggleTableReordering(): Promise<void>;
1254
1355
  /** Saves the new order: only keys belonging to the table are kept, each gets its position. */
1255
1356
  reorderTable(order: unknown, _draggedRecordKey?: unknown): Promise<void>;
1357
+ /** Key of this table's persisted state: per page class (and resource or relationship). */
1358
+ protected tableSessionKey(part: string): string;
1359
+ /** Session of the persisted table state: the browser (its CSRF token) and the signed-in user. */
1360
+ private tableSession;
1361
+ dehydrate(): Promise<void>;
1362
+ /** Restores the persisted table state; every value is sanitized like browser input. */
1363
+ private restoreTableSession;
1256
1364
  mount(_params?: Record<string, unknown>): Promise<void>;
1257
1365
  /** Filters form: one group per filter, bound to `tableDeferredFilters.<filter>`. */
1258
1366
  tableFiltersFormSchema(schema: Schema): Schema;
1367
+ /** Applied state of every declared filter, sanitized from the given (browser) state. */
1368
+ protected sanitizeTableFilters(source: Record<string, unknown> | undefined): Record<string, FilterState>;
1369
+ /** Live filters (`deferFilters(false)`): a changed field applies at once. */
1370
+ updatedTableFilters(value: unknown, key: string | null): Promise<void>;
1259
1371
  applyTableFilters(): void;
1260
1372
  /** Removes an active filter indicator: the field is cleared from both the applied state and the form. */
1261
- removeTableFilter(filterName: unknown, field?: unknown): void;
1373
+ removeTableFilter(filterName: unknown, field?: unknown): Promise<void>;
1262
1374
  /** Live field in the filters form (e.g. a rule's operator changed): runs its `afterStateUpdated`. */
1263
1375
  updatedTableDeferredFilters(_value: unknown, key: string | null): Promise<void>;
1376
+ private runFilterFieldHook;
1264
1377
  resetTableFiltersForm(): void;
1265
- removeTableFilters(): void;
1378
+ /** "Remove all": resets every removable filter (to its empty state, not its default), the search and the column searches. */
1379
+ removeTableFilters(): Promise<void>;
1266
1380
  applyTableColumnManager(state?: unknown, _wasReordered?: unknown): void;
1267
1381
  resetTableColumnManager(): void;
1268
1382
  activeTab: string | null;
@@ -1292,10 +1406,23 @@ export declare class ListRecords extends Page implements TableHost {
1292
1406
  updateTableColumnState(columnName: unknown, recordKey: unknown, input: unknown): Promise<unknown>;
1293
1407
  /** Selected keys: the sent list, or, in "select all" mode, every record of the current query except deselected ones. */
1294
1408
  getSelectedRecordKeys(): Promise<string[]>;
1409
+ /**
1410
+ * Runs `process` for each selected record, loaded in chunks of `chunkSelectedRecords()`. Records failing the action's
1411
+ * `authorizeIndividualRecords()` are skipped; `process` reports a failure by returning `false` or throwing. Then sends the
1412
+ * action's success notification, or its failure notification with the number of records skipped and failed.
1413
+ */
1414
+ processSelectedRecords(action: Action, process: (record: any) => unknown | Promise<unknown>): Promise<{
1415
+ total: number;
1416
+ success: number;
1417
+ unauthorized: number;
1418
+ failed: number;
1419
+ }>;
1295
1420
  deselectAllTableRecords(): void;
1296
1421
  getTable(): Table;
1297
1422
  renderTable(): Promise<string>;
1423
+ protected renderTableEmptyStateActions(ctx: any): Promise<string[]>;
1298
1424
  /** Table header actions (none on a list page: they go in the page header). */
1425
+ protected renderTableToolbarActions(ctx: any): Promise<string[]>;
1299
1426
  protected renderTableHeaderActions(ctx: any): Promise<string[]>;
1300
1427
  sortTable(column?: string | null, direction?: string | null): void;
1301
1428
  gotoPage(page: number, _pageName?: string): void;
@@ -1347,6 +1474,14 @@ export declare class RelationManager extends ListRecords {
1347
1474
  static searchAttachable(_ownerKey: string, _search: string): Promise<[string, string][]>;
1348
1475
  /** Label of an attachable record, or `null` if it is not attachable (also used as validation). */
1349
1476
  static attachableLabel(_ownerKey: string, _recordKey: string): Promise<string | null>;
1477
+ /** Sets the owner of an existing record (its foreign key). */
1478
+ static associate(_ownerKey: string, _recordKey: string): Promise<void>;
1479
+ /** Clears the owner of records (their foreign key), without deleting them. */
1480
+ static dissociate(_ownerKey: string, _recordKeys: string[]): Promise<void>;
1481
+ /** Associable records matching the search: [key, label]. */
1482
+ static searchAssociable(_ownerKey: string, _search: string): Promise<[string, string][]>;
1483
+ /** Label of an associable record, or `null` if it is not associable (also used as validation). */
1484
+ static associableLabel(_ownerKey: string, _recordKey: string): Promise<string | null>;
1350
1485
  getHeaderActions(): Action[];
1351
1486
  placeholder(): string;
1352
1487
  /** A relation manager has its own render hook locations, not the list page's. */
@@ -1425,13 +1560,21 @@ declare abstract class ResourceFormPage extends Page {
1425
1560
  /** Where "Cancel" goes when the browser has no history: the list page, unless the page decides otherwise. */
1426
1561
  protected getCancelUrl(): string;
1427
1562
  protected cancelAction(): Action;
1563
+ /** Before the form is filled (with defaults, or with the record on the edit page). */
1564
+ protected beforeFill(): void | Promise<void>;
1565
+ /** After the form is filled. */
1566
+ protected afterFill(): void | Promise<void>;
1567
+ /** Before the form is validated, when it is submitted. */
1568
+ protected beforeValidate(): void | Promise<void>;
1569
+ /** After the form is validated successfully, before the data is transformed and saved. */
1570
+ protected afterValidate(): void | Promise<void>;
1428
1571
  /** Validates the form; on error, sets the errors on the component and notifies the browser. */
1429
1572
  protected validateForm(): Promise<boolean>;
1430
1573
  }
1431
1574
  /** Record creation page. */
1432
1575
  export declare class CreateRecord extends ResourceFormPage {
1433
1576
  static callable: string[];
1434
- mount(): void;
1577
+ mount(): Promise<void>;
1435
1578
  getOperation(): 'create';
1436
1579
  getSubmitMethod(): string;
1437
1580
  getTitle(): string;
@@ -1461,9 +1604,21 @@ export declare class DeleteAction {
1461
1604
  /** On an edit page, deletes the page's record and returns to the list. On a table row, deletes the row. */
1462
1605
  static make(): Action;
1463
1606
  }
1464
- /** Replicate action: an `Action` with a list of attributes not to copy. */
1607
+ /** Replicate action: an `Action` with a list of attributes not to copy, and hooks around the saving of the copy. */
1465
1608
  export type ReplicateActionInstance = Action & {
1466
1609
  excludeAttributes(attributes: string[]): ReplicateActionInstance;
1610
+ /** Runs before the copy is saved, with its attributes (`replica`, changeable) and the modal form's data. */
1611
+ beforeReplicaSaved(callback: (ctx: {
1612
+ replica: Record<string, unknown>;
1613
+ data: Record<string, unknown>;
1614
+ record: unknown;
1615
+ }) => unknown): ReplicateActionInstance;
1616
+ /** Runs after the copy is saved, with the saved record. */
1617
+ afterReplicaSaved(callback: (ctx: {
1618
+ replica: unknown;
1619
+ data: Record<string, unknown>;
1620
+ record: unknown;
1621
+ }) => unknown): ReplicateActionInstance;
1467
1622
  };
1468
1623
  /** Duplicates a table row: the copy takes the record's attributes, except its key, timestamps and excluded ones. */
1469
1624
  export declare class ReplicateAction {
@@ -1495,6 +1650,18 @@ export declare class DetachAction {
1495
1650
  export declare class DetachBulkAction {
1496
1651
  static make(): Action;
1497
1652
  }
1653
+ /** Gives an existing record to the owner of a one-to-many relation manager (its foreign key). */
1654
+ export declare class AssociateAction {
1655
+ static make(): Action;
1656
+ }
1657
+ /** Removes a row from the owner (clears its foreign key) without deleting it. */
1658
+ export declare class DissociateAction {
1659
+ static make(): Action;
1660
+ }
1661
+ /** Dissociates the selected rows. */
1662
+ export declare class DissociateBulkAction {
1663
+ static make(): Action;
1664
+ }
1498
1665
  /** Restores a soft-deleted record. Only visible when the record is deleted. */
1499
1666
  export declare class RestoreAction {
1500
1667
  static make(): Action;
@@ -1503,10 +1670,21 @@ export declare class RestoreAction {
1503
1670
  export declare class ForceDeleteAction {
1504
1671
  static make(): Action;
1505
1672
  }
1506
- /** Deletes the records selected in a table. */
1673
+ /**
1674
+ * Deletes the records selected in a table. Without `authorizeIndividualRecords()`, the selection is deleted in one query;
1675
+ * with it, record by record, and the notification reports the records that could not be deleted.
1676
+ */
1507
1677
  export declare class DeleteBulkAction {
1508
1678
  static make(): Action;
1509
1679
  }
1680
+ /** Restores the soft-deleted records selected in a table, each one checked with the `restore` ability. */
1681
+ export declare class RestoreBulkAction {
1682
+ static make(): Action;
1683
+ }
1684
+ /** Permanently deletes the soft-deleted records selected in a table, each one checked with the `forceDelete` ability. */
1685
+ export declare class ForceDeleteBulkAction {
1686
+ static make(): Action;
1687
+ }
1510
1688
  /** Record edit page. */
1511
1689
  export declare class EditRecord extends ResourceFormPage {
1512
1690
  static callable: string[];
@@ -1620,5 +1798,4 @@ export declare const discoverPages: (directory: string | URL) => Promise<(typeof
1620
1798
  export declare const discoverWidgets: (directory: string | URL) => Promise<(typeof Widget)[]>;
1621
1799
  /** Panels default-exported (`export default Panel.make()…`) by a folder's modules. */
1622
1800
  export declare function discoverPanels(directory: string | URL): Promise<Panel[]>;
1623
- /** Express router for a panel: assets, runtime endpoint, authentication, tenants, protected pages. */
1624
1801
  export declare function panelRouter(panel: Panel, runtime: WireRuntime): Router;