@everystack/admin 0.4.5 → 0.4.7

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +22 -5
  3. package/src/adapters/CannotSortError.ts +19 -0
  4. package/src/adapters/EverystackAdapter.ts +33 -20
  5. package/src/adapters/EverystackApiError.ts +18 -0
  6. package/src/adapters/types.ts +13 -4
  7. package/src/components/DateConditionInput.tsx +148 -0
  8. package/src/components/DateFilterBar.tsx +85 -0
  9. package/src/components/DateTimeValue.tsx +37 -0
  10. package/src/components/DeletePreview.tsx +15 -6
  11. package/src/hooks/useAdminData.ts +44 -3
  12. package/src/hooks/useDateRangeFilters.ts +47 -0
  13. package/src/hooks/useViewState.ts +41 -8
  14. package/src/index.ts +16 -0
  15. package/src/providers/AdminConfigProvider.tsx +11 -2
  16. package/src/providers/DataAdapterProvider.tsx +5 -3
  17. package/src/rpc/list-filter.ts +210 -0
  18. package/src/rpc/resource-dashboard-rpc.ts +62 -13
  19. package/src/screens/AdminRouter.tsx +19 -6
  20. package/src/screens/ResourceCreate.tsx +51 -6
  21. package/src/screens/ResourceEdit.tsx +71 -8
  22. package/src/screens/ResourceList.tsx +118 -17
  23. package/src/screens/ResourceListView.tsx +7 -3
  24. package/src/screens/ResourceShow.tsx +14 -9
  25. package/src/types.ts +114 -11
  26. package/src/utils/changed-fields.ts +54 -0
  27. package/src/utils/date-filter-fields.ts +32 -0
  28. package/src/utils/date-range.ts +146 -0
  29. package/src/utils/datetime-submission.ts +25 -0
  30. package/src/utils/field-sortable.ts +19 -0
  31. package/src/utils/format-cell-value.ts +28 -5
  32. package/src/utils/integrity-error.ts +16 -0
  33. package/src/utils/list-query.ts +3 -3
  34. package/src/utils/resource-query.ts +303 -0
  35. package/src/utils/validate-config.ts +85 -0
  36. package/src/utils/view-filters.ts +9 -3
  37. package/src/utils/wall-clock.ts +72 -0
@@ -6,7 +6,7 @@
6
6
  * AnalyticsHeader's DashboardFilters and the CRUD table's PostgREST conditions.
7
7
  */
8
8
 
9
- import type { ViewParams } from '../hooks/useViewState';
9
+ import type { ViewParams, ViewParamValues } from '../hooks/useViewState';
10
10
  import type { DimensionConfig, ResourceDashboardConfig } from '../types';
11
11
  import type { DashboardFilters } from '../rpc/resource-dashboard-rpc';
12
12
 
@@ -39,7 +39,9 @@ export function filtersToParams(filters: DashboardFilters, dimensions: Dimension
39
39
  }
40
40
 
41
41
  /** Query params reserved for view state (not column filters). */
42
- export const RESERVED_PARAMS: ReadonlySet<string> = new Set(['from', 'to', 'sort', 'dir', 'page', 'size', 'q', 'tab']);
42
+ export const RESERVED_PARAMS: ReadonlySet<string> = new Set([
43
+ 'from', 'to', 'sort', 'dir', 'page', 'size', 'q', 'tab',
44
+ ]);
43
45
 
44
46
  /**
45
47
  * Pass-through column filters: any query param that names a real resource field
@@ -51,11 +53,15 @@ export function paramsToColumnFilters(
51
53
  params: ViewParams,
52
54
  fieldNames: Set<string>,
53
55
  exclude: Set<string> = new Set(),
56
+ paramValues?: ViewParamValues,
54
57
  ): Record<string, unknown> {
55
58
  const f: Record<string, unknown> = {};
56
59
  for (const [k, v] of Object.entries(params)) {
57
60
  if (RESERVED_PARAMS.has(k) || exclude.has(k)) continue;
58
- if (fieldNames.has(k) && v) f[k] = v;
61
+ if (!fieldNames.has(k)) continue;
62
+ const values = paramValues?.[k] ?? [v];
63
+ if (values.length > 1) f[k] = values;
64
+ else if (values.length === 1 && values[0]) f[k] = values[0];
59
65
  }
60
66
  return f;
61
67
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Wall-clock datetime fields (`type: 'datetime', timezone, storage: 'wall'`): a naive `timestamp`
3
+ * column holding "this wall-clock time in one business zone". The API carries the stored digits
4
+ * labelled `Z`; these helpers read those digits literally and never through the viewer's zone.
5
+ */
6
+
7
+ import { assertTimeZone, partsToWall, wallTimeExists, wallToParts, zoneAbbreviation } from '@everystack/ui/wall-time';
8
+ import type { AdminConfig, FieldDefinition, FieldDefinitionBase, WallClockDatetime } from '../types';
9
+
10
+ export type WallClockField = FieldDefinitionBase & WallClockDatetime;
11
+
12
+ export function isWallClock(field: FieldDefinition): field is WallClockField {
13
+ return field.storage === 'wall';
14
+ }
15
+
16
+ /**
17
+ * A wall value as the string the input and the parser read. A native Date (a custom adapter's
18
+ * prerogative) carries the digits as its UTC fields, the same as the `Z`-labelled string.
19
+ */
20
+ function wallString(value: unknown): unknown {
21
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value.toISOString() : value;
22
+ }
23
+
24
+ /**
25
+ * The stored digits, formatted with no zone shift (a Date built from the digits as UTC, shown in
26
+ * UTC), then the business zone's abbreviation. An unparseable value passes through untouched.
27
+ */
28
+ export function formatWallClock(field: WallClockField, value: unknown): string {
29
+ const parts = wallToParts(wallString(value));
30
+ if (!parts) return String(value);
31
+ const digits = new Date(partsToWall(parts)).toLocaleString(undefined, { timeZone: 'UTC' });
32
+ return `${digits} ${zoneAbbreviation(parts, field.timezone)}`;
33
+ }
34
+
35
+ /**
36
+ * The field error for a wall value the business zone's clocks skip (a spring-forward gap), or
37
+ * null. An overlap time exists (twice) and is accepted as typed. An empty or unparseable value
38
+ * is not this check's concern.
39
+ */
40
+ export function wallClockError(field: FieldDefinition, value: unknown): string | null {
41
+ if (!isWallClock(field)) return null;
42
+ const parts = wallToParts(wallString(value));
43
+ if (!parts || wallTimeExists(parts, field.timezone)) return null;
44
+ return `${field.label ?? field.name} does not exist in ${field.timezone}: clocks skip that time. Pick another time.`;
45
+ }
46
+
47
+ /**
48
+ * Checks every field's wall-clock options at setup, loudly: a `timezone` needs `storage: 'wall'`
49
+ * on a `datetime` field and must be a time zone this engine knows. Config can arrive untyped,
50
+ * so the TypeScript union is not enough on its own.
51
+ */
52
+ export function validateWallClockFields(config: AdminConfig): void {
53
+ for (const resource of config.resources ?? []) {
54
+ for (const field of resource.fields ?? []) {
55
+ const raw = field as { name: string; type: unknown; timezone?: unknown; storage?: unknown };
56
+ if (raw.timezone === undefined && raw.storage === undefined) continue;
57
+ const where = `Admin resource "${resource.name}", field "${raw.name}"`;
58
+ if (raw.type !== 'datetime') {
59
+ throw new TypeError(`${where}: timezone and storage are only for a datetime field, not type ${JSON.stringify(raw.type)}`);
60
+ }
61
+ if (raw.storage === 'utc' && raw.timezone === undefined) continue;
62
+ if (raw.storage !== 'wall') {
63
+ throw new TypeError(`${where}: a timezone needs storage: 'wall' (a naive timestamp column), got storage ${JSON.stringify(raw.storage)}`);
64
+ }
65
+ try {
66
+ assertTimeZone(raw.timezone as string);
67
+ } catch (err) {
68
+ throw new RangeError(`${where}: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ }
71
+ }
72
+ }