@beechcms/core 0.8.1 → 0.9.0
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/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -1
- package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -1
- package/dist/auth/account-email.d.ts +3 -0
- package/dist/auth/account-email.d.ts.map +1 -0
- package/dist/auth/account-email.js +9 -0
- package/dist/auth/password-reset-token.repository.d.ts +13 -2
- package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
- package/dist/auth/user.repository.d.ts +69 -4
- package/dist/auth/user.repository.d.ts.map +1 -1
- package/dist/common/canonical-json.d.ts +2 -1
- package/dist/common/canonical-json.d.ts.map +1 -1
- package/dist/common/canonical-json.js +5 -3
- package/dist/content/content-scan.repository.d.ts +1 -1
- package/dist/content/content.repository.d.ts +156 -5
- package/dist/content/content.repository.d.ts.map +1 -1
- package/dist/content/content.repository.js +62 -0
- package/dist/content/seed-media-purge.repository.d.ts +34 -0
- package/dist/content/seed-media-purge.repository.d.ts.map +1 -0
- package/dist/content/seed-media-purge.repository.js +3 -0
- package/dist/content/seed.repository.d.ts +14 -0
- package/dist/content/seed.repository.d.ts.map +1 -1
- package/dist/dashboard-layout/content-view.d.ts +757 -0
- package/dist/dashboard-layout/content-view.d.ts.map +1 -0
- package/dist/dashboard-layout/content-view.js +285 -0
- package/dist/dashboard-layout/content-view.repository.d.ts +39 -0
- package/dist/dashboard-layout/content-view.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/content-view.repository.js +3 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts +5 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/dashboard-layout.js +17 -9
- package/dist/dashboard-layout/kanban/kanban.d.ts +32 -18
- package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -1
- package/dist/dashboard-layout/kanban/kanban.js +45 -21
- package/dist/dashboard-layout/seed-layout.d.ts +22 -31
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.js +93 -18
- package/dist/dashboard-layout/seed-layout.repository.d.ts +1 -5
- package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -1
- package/dist/engine/ddl/ddl.d.ts +31 -0
- package/dist/engine/ddl/ddl.d.ts.map +1 -1
- package/dist/engine/ddl/ddl.js +92 -13
- package/dist/engine/ddl/schema-mutator.d.ts +3 -0
- package/dist/engine/ddl/schema-mutator.d.ts.map +1 -1
- package/dist/engine/ddl/seed-purge-guards.d.ts +7 -0
- package/dist/engine/ddl/seed-purge-guards.d.ts.map +1 -0
- package/dist/engine/ddl/seed-purge-guards.js +19 -0
- package/dist/engine/engine.d.ts +4 -3
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +4 -3
- package/dist/engine/introspection/relation-audit.d.ts +46 -0
- package/dist/engine/introspection/relation-audit.d.ts.map +1 -0
- package/dist/engine/introspection/relation-audit.js +103 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts +2 -1
- package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -1
- package/dist/engine/introspection/schema-fingerprint.js +8 -3
- package/dist/engine/privacy/policies.d.ts +1 -1
- package/dist/engine/privacy/policies.d.ts.map +1 -1
- package/dist/engine/privacy/policies.js +12 -6
- package/dist/engine/privacy/privacy.service.d.ts +13 -0
- package/dist/engine/privacy/privacy.service.d.ts.map +1 -1
- package/dist/engine/privacy/privacy.service.js +24 -0
- package/dist/engine/query/active-clause.d.ts +19 -0
- package/dist/engine/query/active-clause.d.ts.map +1 -0
- package/dist/engine/query/active-clause.js +27 -0
- package/dist/engine/query/query.d.ts +14 -1
- package/dist/engine/query/query.d.ts.map +1 -1
- package/dist/engine/query/query.js +105 -13
- package/dist/engine/seeds/candidate-ids.d.ts +14 -0
- package/dist/engine/seeds/candidate-ids.d.ts.map +1 -0
- package/dist/engine/seeds/candidate-ids.js +40 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts +38 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts.map +1 -0
- package/dist/engine/seeds/seed-definition-diff.js +51 -0
- package/dist/engine/seeds/seed-registry.d.ts +17 -0
- package/dist/engine/seeds/seed-registry.d.ts.map +1 -1
- package/dist/engine/seeds/seed-registry.js +27 -0
- package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -1
- package/dist/engine/seeds/seed-types-generator.js +4 -5
- package/dist/engine/seeds/seed-validation.d.ts +5 -0
- package/dist/engine/seeds/seed-validation.d.ts.map +1 -1
- package/dist/engine/seeds/seed-validation.js +39 -19
- package/dist/engine/seeds/tag-migration.d.ts +73 -0
- package/dist/engine/seeds/tag-migration.d.ts.map +1 -0
- package/dist/engine/seeds/tag-migration.js +212 -0
- package/dist/engine/seeds/tag-options.d.ts +15 -0
- package/dist/engine/seeds/tag-options.d.ts.map +1 -0
- package/dist/engine/seeds/tag-options.js +111 -0
- package/dist/engine/seeds/tag-value.d.ts +31 -0
- package/dist/engine/seeds/tag-value.d.ts.map +1 -0
- package/dist/engine/seeds/tag-value.js +51 -0
- package/dist/engine/types.d.ts +59 -7
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/validation/cache.d.ts.map +1 -1
- package/dist/engine/validation/cache.js +20 -4
- package/dist/engine/validation/index.d.ts +12 -1
- package/dist/engine/validation/index.d.ts.map +1 -1
- package/dist/engine/validation/index.js +29 -33
- package/dist/engine/validation/primitives.d.ts +12 -0
- package/dist/engine/validation/primitives.d.ts.map +1 -1
- package/dist/engine/validation/primitives.js +22 -0
- package/dist/engine/validation/richtext-sanitizer.d.ts +9 -1
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.js +36 -7
- package/dist/engine/validation/schema-builders.d.ts +16 -0
- package/dist/engine/validation/schema-builders.d.ts.map +1 -1
- package/dist/engine/validation/schema-builders.js +125 -18
- package/dist/index.d.ts +34 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -11
- package/dist/media/file-types.d.ts +12 -0
- package/dist/media/file-types.d.ts.map +1 -1
- package/dist/media/file-types.js +24 -4
- package/dist/media/magic-bytes.d.ts.map +1 -1
- package/dist/media/magic-bytes.js +19 -9
- package/dist/media/media-transform.d.ts +8 -4
- package/dist/media/media-transform.d.ts.map +1 -1
- package/dist/media/media-transform.js +10 -6
- package/dist/media/media.repository.d.ts +5 -0
- package/dist/media/media.repository.d.ts.map +1 -1
- package/dist/notifications/notification-service.d.ts +1 -1
- package/dist/notifications/notification.repository.d.ts +11 -2
- package/dist/notifications/notification.repository.d.ts.map +1 -1
- package/dist/oauth/authorization-code.repository.d.ts +10 -5
- package/dist/oauth/authorization-code.repository.d.ts.map +1 -1
- package/dist/oauth/pkce.d.ts +1 -1
- package/dist/oauth/role-guard.d.ts +30 -11
- package/dist/oauth/role-guard.d.ts.map +1 -1
- package/dist/oauth/role-guard.js +27 -5
- package/dist/oauth/scopes.d.ts +1 -1
- package/dist/oauth/scopes.d.ts.map +1 -1
- package/dist/oauth/token.repository.d.ts +2 -0
- package/dist/oauth/token.repository.d.ts.map +1 -1
- package/dist/queue/queue.interface.d.ts +9 -1
- package/dist/queue/queue.interface.d.ts.map +1 -1
- package/dist/queue/queue.stub.d.ts +1 -1
- package/dist/queue/queue.stub.d.ts.map +1 -1
- package/dist/queue/queue.stub.js +2 -3
- package/dist/queue/scheduler.stub.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts +7 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.js +66 -0
- package/dist/rbac/invitation.repository.d.ts +28 -10
- package/dist/rbac/invitation.repository.d.ts.map +1 -1
- package/dist/rbac/types.d.ts +40 -28
- package/dist/rbac/types.d.ts.map +1 -1
- package/dist/rbac/types.js +2 -1
- package/dist/schema/manifest-seeds.js +3 -3
- package/dist/search/search.repository.d.ts +5 -4
- package/dist/search/search.repository.d.ts.map +1 -1
- package/dist/search/vector-extractor.d.ts.map +1 -1
- package/dist/search/vector-extractor.js +23 -2
- package/dist/search/vector.repository.d.ts +2 -1
- package/dist/search/vector.repository.d.ts.map +1 -1
- package/dist/security/time-trap-token.repository.d.ts +5 -3
- package/dist/security/time-trap-token.repository.d.ts.map +1 -1
- package/dist/settings/site-settings.repository.d.ts +8 -0
- package/dist/settings/site-settings.repository.d.ts.map +1 -1
- package/dist/transfer/flat-seed.d.ts.map +1 -1
- package/dist/transfer/flat-seed.js +6 -1
- package/dist/transfer/row-mapping.d.ts.map +1 -1
- package/dist/transfer/row-mapping.js +9 -2
- package/package.json +8 -3
- package/dist/.tsbuildinfo +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-view.d.ts","sourceRoot":"","sources":["../../src/dashboard-layout/content-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { findBranchById } from '../engine/seeds/seed-registry.js';
|
|
5
|
+
import { isSingleChoiceTags, tagOptionValues } from '../engine/seeds/tag-options.js';
|
|
6
|
+
import { isViewAuthorized } from './view-authorization.js';
|
|
7
|
+
import { kanbanViewConfigSchema, kanbanCardConfigSchema, validateCardConfigAgainstSeed, } from './seed-layout.js';
|
|
8
|
+
import { resolveKanbanConfig } from './kanban/kanban.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// View types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Zod needs a tuple. Kept equal to AUTHORIZABLE_VIEWS by content-view.test.ts. */
|
|
13
|
+
export const VIEW_TYPE_IDS = ['table', 'gallery', 'kanban'];
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Column references — Branch IDs or engine system columns, never aliases
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export const VIEW_SYSTEM_COLUMNS = ['slug', 'status', 'created_at', 'updated_at'];
|
|
18
|
+
const BRANCH_REF_RE = /^br_[A-Za-z0-9]+$/;
|
|
19
|
+
export const viewColumnRefSchema = z.union([
|
|
20
|
+
z.enum(VIEW_SYSTEM_COLUMNS),
|
|
21
|
+
z.string().regex(BRANCH_REF_RE),
|
|
22
|
+
]);
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config building blocks
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Mirrors the dashboard toolbar operator set (apps/dashboard/src/lib/filter-dsl.ts). */
|
|
27
|
+
export const VIEW_FILTER_OPERATORS = [
|
|
28
|
+
'eq', 'gt', 'gte', 'lt', 'lte', 'contains', 'is_empty', 'is_not_empty',
|
|
29
|
+
];
|
|
30
|
+
/** Mirrors MAX_CONDITIONS_PER_FILTER in the dashboard toolbar. */
|
|
31
|
+
export const MAX_VIEW_CONDITIONS = 3;
|
|
32
|
+
export const viewConditionSchema = z.object({
|
|
33
|
+
op: z.enum(VIEW_FILTER_OPERATORS),
|
|
34
|
+
value: z.union([z.string().max(500), z.number(), z.boolean(), z.null()]),
|
|
35
|
+
});
|
|
36
|
+
export const viewFilterSchema = z.object({
|
|
37
|
+
columnRef: viewColumnRefSchema,
|
|
38
|
+
conditions: z.array(viewConditionSchema).min(1).max(MAX_VIEW_CONDITIONS),
|
|
39
|
+
});
|
|
40
|
+
export const viewSortSchema = z.object({
|
|
41
|
+
columnRef: viewColumnRefSchema,
|
|
42
|
+
desc: z.boolean(),
|
|
43
|
+
});
|
|
44
|
+
export const viewGroupBySchema = z.object({
|
|
45
|
+
columnRef: viewColumnRefSchema,
|
|
46
|
+
/** Only meaningful on a date column; stripped otherwise by validateViewConfigAgainstSeed. */
|
|
47
|
+
datePrecision: z.object({ year: z.boolean(), month: z.boolean(), day: z.boolean() }).optional(),
|
|
48
|
+
});
|
|
49
|
+
export const VIEW_DENSITIES = ['compact', 'normal', 'comfortable'];
|
|
50
|
+
export const viewAppearanceSchema = z.object({
|
|
51
|
+
density: z.enum(VIEW_DENSITIES).optional(),
|
|
52
|
+
hiddenColumns: z.array(viewColumnRefSchema).max(200).optional(),
|
|
53
|
+
/** Display order of data columns; columns missing from the list follow in default order. */
|
|
54
|
+
columnOrder: z.array(viewColumnRefSchema).max(200).optional(),
|
|
55
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
56
|
+
});
|
|
57
|
+
export const VIEW_FORMAT_TONES = ['neutral', 'info', 'success', 'warning', 'danger'];
|
|
58
|
+
/**
|
|
59
|
+
* View-neutral targets: `element` is the whole row (Table) or card (Gallery/Kanban), `field` is
|
|
60
|
+
* the intersection element × columnRef. The dashboard's legacy `row`/`cell` map 1:1 (Sprint 2).
|
|
61
|
+
*/
|
|
62
|
+
export const VIEW_FORMAT_TARGETS = ['element', 'field'];
|
|
63
|
+
export const VIEW_TEXT_STYLES = ['bold', 'italic', 'underline'];
|
|
64
|
+
export const viewConditionalFormatSchema = z.object({
|
|
65
|
+
id: z.string().min(1).max(64),
|
|
66
|
+
enabled: z.boolean(),
|
|
67
|
+
/** Ascending: 0 wins over 10. */
|
|
68
|
+
priority: z.number().int().min(0).max(1000),
|
|
69
|
+
label: z.string().max(60).optional(),
|
|
70
|
+
columnRef: viewColumnRefSchema,
|
|
71
|
+
conditions: z.array(viewConditionSchema).min(1).max(MAX_VIEW_CONDITIONS),
|
|
72
|
+
tone: z.enum(VIEW_FORMAT_TONES),
|
|
73
|
+
target: z.enum(VIEW_FORMAT_TARGETS),
|
|
74
|
+
textStyles: z.array(z.enum(VIEW_TEXT_STYLES)).max(3).default([]),
|
|
75
|
+
});
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Gallery folder styles
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
export const FOLDER_COLORS = ['slate', 'red', 'orange', 'amber', 'green', 'teal', 'blue', 'violet', 'pink'];
|
|
80
|
+
export const FOLDER_ICONS = [
|
|
81
|
+
'Folder', 'Star', 'Heart', 'Image', 'Camera', 'Calendar', 'Tag', 'Bookmark', 'Home',
|
|
82
|
+
'Gift', 'Video', 'Music', 'Book', 'Flag', 'Briefcase', 'Users', 'Sun', 'Crown',
|
|
83
|
+
];
|
|
84
|
+
export const MAX_FOLDER_STYLES = 100;
|
|
85
|
+
/** Cosmetic override of one gallery folder. `label` and `description` are display-only: the folder's group value is untouched. */
|
|
86
|
+
export const folderStyleSchema = z.object({
|
|
87
|
+
color: z.enum(FOLDER_COLORS).optional(),
|
|
88
|
+
icon: z.enum(FOLDER_ICONS).optional(),
|
|
89
|
+
label: z.string().trim().max(60).optional(),
|
|
90
|
+
description: z.string().trim().max(120).optional(),
|
|
91
|
+
});
|
|
92
|
+
function cleanFolders(folders) {
|
|
93
|
+
const entries = Object.entries(folders)
|
|
94
|
+
.map(([key, style]) => [key, {
|
|
95
|
+
...(style.color ? { color: style.color } : {}),
|
|
96
|
+
...(style.icon ? { icon: style.icon } : {}),
|
|
97
|
+
...(style.label ? { label: style.label } : {}),
|
|
98
|
+
...(style.description ? { description: style.description } : {}),
|
|
99
|
+
}])
|
|
100
|
+
.filter(([, style]) => Object.keys(style).length > 0);
|
|
101
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// The config
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
/**
|
|
107
|
+
* Persisted per-instance configuration. Every key has a default, so `{}` parses into a complete
|
|
108
|
+
* config, and a stored row written by an older dashboard reads back fully shaped. Unknown keys
|
|
109
|
+
* are stripped (zod default), never rejected.
|
|
110
|
+
*/
|
|
111
|
+
export const contentViewConfigSchema = z.object({
|
|
112
|
+
filters: z.array(viewFilterSchema).max(50).default([]),
|
|
113
|
+
sort: viewSortSchema.nullable().default(null),
|
|
114
|
+
groupBy: viewGroupBySchema.nullable().default(null),
|
|
115
|
+
appearance: viewAppearanceSchema.default({}),
|
|
116
|
+
conditionalFormats: z.array(viewConditionalFormatSchema).max(50).default([]),
|
|
117
|
+
/** Kanban-only. Dropped from any non-kanban instance. */
|
|
118
|
+
kanban: kanbanViewConfigSchema.optional(),
|
|
119
|
+
/** Card layout (Kanban and Gallery). Dropped from any other instance. */
|
|
120
|
+
card: kanbanCardConfigSchema.optional(),
|
|
121
|
+
/** Gallery-only. Style per folder, keyed by the normalized group value. Dropped from any other instance. */
|
|
122
|
+
folders: z.record(z.string().min(1).max(100), folderStyleSchema)
|
|
123
|
+
.refine((folders) => Object.keys(folders).length <= MAX_FOLDER_STYLES)
|
|
124
|
+
.optional(),
|
|
125
|
+
});
|
|
126
|
+
export function emptyViewConfig() {
|
|
127
|
+
return contentViewConfigSchema.parse({});
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Request bodies
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export const VIEW_TITLE_MAX_LENGTH = 60;
|
|
133
|
+
/** Trimmed. Empty or whitespace-only collapses to null, which means "render the translated type label". */
|
|
134
|
+
export const viewTitleSchema = z
|
|
135
|
+
.string()
|
|
136
|
+
.trim()
|
|
137
|
+
.max(VIEW_TITLE_MAX_LENGTH)
|
|
138
|
+
.nullable()
|
|
139
|
+
.transform((value) => (value === null || value.length === 0 ? null : value));
|
|
140
|
+
export const createContentViewInputSchema = z.object({
|
|
141
|
+
type: z.enum(VIEW_TYPE_IDS),
|
|
142
|
+
title: viewTitleSchema.optional(),
|
|
143
|
+
config: contentViewConfigSchema.optional(),
|
|
144
|
+
});
|
|
145
|
+
export const updateContentViewInputSchema = z
|
|
146
|
+
.object({
|
|
147
|
+
title: viewTitleSchema.optional(),
|
|
148
|
+
/** Whole-config replacement, not a deep merge. */
|
|
149
|
+
config: contentViewConfigSchema.optional(),
|
|
150
|
+
})
|
|
151
|
+
.refine((body) => body.title !== undefined || body.config !== undefined, {
|
|
152
|
+
message: 'Provide at least one of title, config',
|
|
153
|
+
});
|
|
154
|
+
export const reorderContentViewsInputSchema = z.object({
|
|
155
|
+
ids: z.array(z.string().min(1).max(64)).min(1).max(200),
|
|
156
|
+
});
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Cleanup against the live seed
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
const SYSTEM_COLUMN_SET = new Set(VIEW_SYSTEM_COLUMNS);
|
|
161
|
+
const DATE_SYSTEM_COLUMNS = new Set(['created_at', 'updated_at']);
|
|
162
|
+
function refExists(seed, ref) {
|
|
163
|
+
return SYSTEM_COLUMN_SET.has(ref) || findBranchById(seed, ref) !== null;
|
|
164
|
+
}
|
|
165
|
+
function isDateRef(seed, ref) {
|
|
166
|
+
return DATE_SYSTEM_COLUMNS.has(ref) || findBranchById(seed, ref)?.type === 'date';
|
|
167
|
+
}
|
|
168
|
+
/** True when `ref` addresses a `tags` branch that is multiple choice, which cannot be sorted or grouped. */
|
|
169
|
+
function isMultipleChoiceTagsRef(seed, ref) {
|
|
170
|
+
const branch = findBranchById(seed, ref);
|
|
171
|
+
return branch?.type === 'tags' && !isSingleChoiceTags(branch);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Brief §2.10: `sort`, `groupBy` and `kanban.sort` may reference a `tags` branch only when it is single choice;
|
|
175
|
+
* `kanban.axisBranchId` may reference a `tags` branch only when it is a kanban axis candidate.
|
|
176
|
+
* One message per offending reference, in the order sort, groupBy, kanban axis, kanban sort; `[]` when valid.
|
|
177
|
+
* References to missing branches and to non-`tags` branches are not issues (cleanup handles them). Pure.
|
|
178
|
+
*/
|
|
179
|
+
export function viewConfigChoiceIssues(config, seed) {
|
|
180
|
+
const issues = [];
|
|
181
|
+
const multipleChoiceIssue = (ref, usage) => {
|
|
182
|
+
if (!isMultipleChoiceTagsRef(seed, ref))
|
|
183
|
+
return;
|
|
184
|
+
const alias = findBranchById(seed, ref)?.alias;
|
|
185
|
+
issues.push(`'${alias}' is a multiple-choice tags field: ${usage} needs multiple: false.`);
|
|
186
|
+
};
|
|
187
|
+
if (config.sort)
|
|
188
|
+
multipleChoiceIssue(config.sort.columnRef, 'sort');
|
|
189
|
+
if (config.groupBy)
|
|
190
|
+
multipleChoiceIssue(config.groupBy.columnRef, 'group by');
|
|
191
|
+
const axis = config.kanban?.axisBranchId ? findBranchById(seed, config.kanban.axisBranchId) : null;
|
|
192
|
+
if (axis?.type === 'tags' && !(isSingleChoiceTags(axis) && tagOptionValues(axis).length > 0)) {
|
|
193
|
+
issues.push(`'${axis.alias}' cannot be a kanban axis: it needs a single-choice tags field with options.`);
|
|
194
|
+
}
|
|
195
|
+
if (config.kanban?.sort)
|
|
196
|
+
multipleChoiceIssue(config.kanban.sort.branchId, 'kanban card sort');
|
|
197
|
+
return issues;
|
|
198
|
+
}
|
|
199
|
+
function cleanKanban(kanban, seed) {
|
|
200
|
+
const { candidates } = resolveKanbanConfig(seed);
|
|
201
|
+
const axisOk = kanban.axisBranchId !== null && candidates.some((c) => c.branchId === kanban.axisBranchId);
|
|
202
|
+
const sortOk = kanban.sort !== null && findBranchById(seed, kanban.sort.branchId) !== null
|
|
203
|
+
&& !isMultipleChoiceTagsRef(seed, kanban.sort.branchId);
|
|
204
|
+
return {
|
|
205
|
+
...kanban,
|
|
206
|
+
axisBranchId: axisOk ? kanban.axisBranchId : null,
|
|
207
|
+
sort: sortOk ? kanban.sort : null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Pure auto-cleanup, never an error: drops references to branches the seed no longer has,
|
|
212
|
+
* `sort`/`groupBy`/kanban sort on a multiple-choice `tags` branch,
|
|
213
|
+
* duplicate filters on one column, date precision on a non-date grouping, and the Kanban
|
|
214
|
+
* sub-config on a non-kanban instance and the card layout on a table instance. Same policy as validateCardConfigAgainstSeed.
|
|
215
|
+
*/
|
|
216
|
+
export function validateViewConfigAgainstSeed(config, seed, type) {
|
|
217
|
+
const seenFilterRefs = new Set();
|
|
218
|
+
const filters = config.filters.filter((filter) => {
|
|
219
|
+
if (!refExists(seed, filter.columnRef) || seenFilterRefs.has(filter.columnRef))
|
|
220
|
+
return false;
|
|
221
|
+
seenFilterRefs.add(filter.columnRef);
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
const sort = config.sort && refExists(seed, config.sort.columnRef) && !isMultipleChoiceTagsRef(seed, config.sort.columnRef)
|
|
225
|
+
? config.sort
|
|
226
|
+
: null;
|
|
227
|
+
let groupBy = config.groupBy
|
|
228
|
+
&& refExists(seed, config.groupBy.columnRef)
|
|
229
|
+
&& !isMultipleChoiceTagsRef(seed, config.groupBy.columnRef)
|
|
230
|
+
? config.groupBy
|
|
231
|
+
: null;
|
|
232
|
+
if (groupBy?.datePrecision && !isDateRef(seed, groupBy.columnRef)) {
|
|
233
|
+
groupBy = { columnRef: groupBy.columnRef };
|
|
234
|
+
}
|
|
235
|
+
const appearance = { ...config.appearance };
|
|
236
|
+
if (appearance.hiddenColumns) {
|
|
237
|
+
appearance.hiddenColumns = [...new Set(appearance.hiddenColumns)].filter((ref) => refExists(seed, ref));
|
|
238
|
+
}
|
|
239
|
+
if (appearance.columnOrder) {
|
|
240
|
+
appearance.columnOrder = [...new Set(appearance.columnOrder)].filter((ref) => refExists(seed, ref));
|
|
241
|
+
}
|
|
242
|
+
const conditionalFormats = config.conditionalFormats.filter((rule) => refExists(seed, rule.columnRef));
|
|
243
|
+
const cleaned = { filters, sort, groupBy, appearance, conditionalFormats };
|
|
244
|
+
if (type === 'kanban' && config.kanban)
|
|
245
|
+
cleaned.kanban = cleanKanban(config.kanban, seed);
|
|
246
|
+
if ((type === 'kanban' || type === 'gallery') && config.card) {
|
|
247
|
+
cleaned.card = validateCardConfigAgainstSeed(config.card, seed, { mediaImageOnly: type === 'gallery' }).cleaned;
|
|
248
|
+
}
|
|
249
|
+
if (type === 'gallery' && config.folders) {
|
|
250
|
+
const folders = cleanFolders(config.folders);
|
|
251
|
+
if (folders)
|
|
252
|
+
cleaned.folders = folders;
|
|
253
|
+
}
|
|
254
|
+
return cleaned;
|
|
255
|
+
}
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Projection (storage → API)
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
/** null when the seed's allow-list does not authorize the record's type (hidden, never deleted). */
|
|
260
|
+
export function projectContentView(record, seed) {
|
|
261
|
+
const type = record.type;
|
|
262
|
+
if (!isViewAuthorized(seed, type))
|
|
263
|
+
return null;
|
|
264
|
+
return { ...record, type, config: validateViewConfigAgainstSeed(record.config, seed, type) };
|
|
265
|
+
}
|
|
266
|
+
/** Order-preserving. The caller passes records already sorted by position. */
|
|
267
|
+
export function projectContentViews(records, seed) {
|
|
268
|
+
const views = [];
|
|
269
|
+
for (const record of records) {
|
|
270
|
+
const view = projectContentView(record, seed);
|
|
271
|
+
if (view)
|
|
272
|
+
views.push(view);
|
|
273
|
+
}
|
|
274
|
+
return views;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The full position order of a seed's rows after its visible views were reordered. Hidden rows
|
|
278
|
+
* (types the allow-list currently rejects) keep their slot; visible slots are refilled in the
|
|
279
|
+
* requested order. Writing the result compacts positions to 0..n-1 with no duplicates.
|
|
280
|
+
*/
|
|
281
|
+
export function mergeContentViewOrder(allIds, visibleOrder) {
|
|
282
|
+
const visible = new Set(visibleOrder);
|
|
283
|
+
const queue = [...visibleOrder];
|
|
284
|
+
return allIds.map((id) => (visible.has(id) ? queue.shift() : id));
|
|
285
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DashboardView } from './view-authorization.js';
|
|
2
|
+
import type { ContentViewConfig, ContentViewRecord } from './content-view.js';
|
|
3
|
+
export interface NewContentView {
|
|
4
|
+
seedSlug: string;
|
|
5
|
+
type: DashboardView;
|
|
6
|
+
title: string | null;
|
|
7
|
+
/** Already cleaned by validateViewConfigAgainstSeed. */
|
|
8
|
+
config: ContentViewConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface ContentViewPatch {
|
|
11
|
+
/** undefined → keep; null → reset to the translated type label. */
|
|
12
|
+
title?: string | null;
|
|
13
|
+
/** undefined → keep; otherwise replaces the whole config. */
|
|
14
|
+
config?: ContentViewConfig;
|
|
15
|
+
}
|
|
16
|
+
export type RemoveContentViewResult = 'deleted' | 'not-found' | 'last-table';
|
|
17
|
+
/**
|
|
18
|
+
* Persistence for shared, ordered view instances (`seed_views`). Every write is one statement
|
|
19
|
+
* or one D1 batch, so each method is atomic. Implementations own id minting and timestamps
|
|
20
|
+
* (IIdGenerator / IClock injected).
|
|
21
|
+
*/
|
|
22
|
+
export interface IContentViewRepository {
|
|
23
|
+
/** All rows of a seed, including types the allow-list currently hides, ordered by position. */
|
|
24
|
+
listBySeed(seedSlug: string): Promise<ContentViewRecord[]>;
|
|
25
|
+
get(seedSlug: string, id: string): Promise<ContentViewRecord | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Inserts one untitled instance per type, at positions 0..n-1. Each insert is skipped when the
|
|
28
|
+
* seed already has an instance of that type, so concurrent first reads never duplicate.
|
|
29
|
+
*/
|
|
30
|
+
ensureDefaults(seedSlug: string, types: readonly DashboardView[], updatedBy: string): Promise<void>;
|
|
31
|
+
/** Appends after the current last position. */
|
|
32
|
+
create(input: NewContentView, updatedBy: string): Promise<ContentViewRecord>;
|
|
33
|
+
update(seedSlug: string, id: string, patch: ContentViewPatch, updatedBy: string): Promise<ContentViewRecord | null>;
|
|
34
|
+
/** Refuses ('last-table') to delete the seed's only Table instance, atomically. */
|
|
35
|
+
remove(seedSlug: string, id: string): Promise<RemoveContentViewResult>;
|
|
36
|
+
/** Writes position = index for each id. Ids not listed keep their position. */
|
|
37
|
+
reorder(seedSlug: string, orderedIds: readonly string[], updatedBy: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=content-view.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-view.repository.d.ts","sourceRoot":"","sources":["../../src/dashboard-layout/content-view.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAE7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,aAAa,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,wDAAwD;IACxD,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAC3B;AAED,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,WAAW,GAAG,YAAY,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IAC1D,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IACpE;;;OAGG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnG,+CAA+C;IAC/C,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC5E,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IACnH,mFAAmF;IACnF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;IACtE,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3F"}
|
|
@@ -44,6 +44,11 @@ export declare const WIDGET_TYPE_REGEX: RegExp;
|
|
|
44
44
|
/** Max serialized size of a single widget `config`
|
|
45
45
|
* (`JSON.stringify(widget.config).length`) — guards D1 row bloat. */
|
|
46
46
|
export declare const MAX_WIDGET_CONFIG_BYTES = 8192;
|
|
47
|
+
/** Max serialized size of a whole layout. Keeps the `dashboard_layouts` row
|
|
48
|
+
* far below D1's per-row limit and bounds the per-request cost of every GET. */
|
|
49
|
+
export declare const MAX_DASHBOARD_LAYOUT_BYTES: number;
|
|
50
|
+
export declare const MAX_SECTIONS_PER_PAGE = 50;
|
|
51
|
+
export declare const MAX_WIDGETS_PER_COLUMN = 50;
|
|
47
52
|
export declare const dashboardWidgetInstanceSchema: z.ZodObject<{
|
|
48
53
|
id: z.ZodString;
|
|
49
54
|
type: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-layout.d.ts","sourceRoot":"","sources":["../../src/dashboard-layout/dashboard-layout.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAM9C;;sEAEsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,uBAAuB,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0CAA0C;IAC1C,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B;gCAC4B;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,OAAO,EAAE,CAAC,CAAA;IACV,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAC7B;AAMD;sEACsE;AACtE,eAAO,MAAM,iBAAiB,QAA6B,CAAA;AAE3D;sEACsE;AACtE,eAAO,MAAM,uBAAuB,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dashboard-layout.d.ts","sourceRoot":"","sources":["../../src/dashboard-layout/dashboard-layout.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAM9C;;sEAEsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,uBAAuB,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0CAA0C;IAC1C,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B;gCAC4B;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,OAAO,EAAE,CAAC,CAAA;IACV,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAC7B;AAMD;sEACsE;AACtE,eAAO,MAAM,iBAAiB,QAA6B,CAAA;AAE3D;sEACsE;AACtE,eAAO,MAAM,uBAAuB,OAAO,CAAA;AAE3C;iFACiF;AACjF,eAAO,MAAM,0BAA0B,QAAa,CAAA;AAEpD,eAAO,MAAM,qBAAqB,KAAK,CAAA;AACvC,eAAO,MAAM,sBAAsB,KAAK,CAAA;AAOxC,eAAO,MAAM,6BAA6B;;;;;iBAKxC,CAAA;AACF,eAAO,MAAM,qBAAqB;;;;;;;;iBAGhC,CAAA;AACF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;iBAOjC,CAAA;AACF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;iBAM9B,CAAA;AACF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;iBAQ/B,CAAA;AAQH;;6DAE6D;AAC7D,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7B,eAAe,CA2EjB;AAMD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC9B;uEACmE;IACnE,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACvC;AAED,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEjF;;;;mDAImD;AACnD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,sBAAsB,GAC1B,6BAA6B,CA2E/B"}
|
|
@@ -10,21 +10,27 @@ export const WIDGET_TYPE_REGEX = /^[a-z0-9@][a-z0-9@/_-]*$/;
|
|
|
10
10
|
/** Max serialized size of a single widget `config`
|
|
11
11
|
* (`JSON.stringify(widget.config).length`) — guards D1 row bloat. */
|
|
12
12
|
export const MAX_WIDGET_CONFIG_BYTES = 8192;
|
|
13
|
+
/** Max serialized size of a whole layout. Keeps the `dashboard_layouts` row
|
|
14
|
+
* far below D1's per-row limit and bounds the per-request cost of every GET. */
|
|
15
|
+
export const MAX_DASHBOARD_LAYOUT_BYTES = 256 * 1024;
|
|
16
|
+
export const MAX_SECTIONS_PER_PAGE = 50;
|
|
17
|
+
export const MAX_WIDGETS_PER_COLUMN = 50;
|
|
18
|
+
const ID_MAX = 64;
|
|
13
19
|
// ---------------------------------------------------------------------------
|
|
14
20
|
// Zod schemas for shape validation
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
16
22
|
export const dashboardWidgetInstanceSchema = z.object({
|
|
17
|
-
id: z.string().min(1),
|
|
23
|
+
id: z.string().min(1).max(ID_MAX),
|
|
18
24
|
type: z.string().regex(WIDGET_TYPE_REGEX),
|
|
19
25
|
title: z.string().max(80).optional(),
|
|
20
26
|
config: z.record(z.string(), z.unknown()),
|
|
21
27
|
});
|
|
22
28
|
export const dashboardColumnSchema = z.object({
|
|
23
|
-
id: z.string().min(1),
|
|
24
|
-
widgets: z.array(dashboardWidgetInstanceSchema),
|
|
29
|
+
id: z.string().min(1).max(ID_MAX),
|
|
30
|
+
widgets: z.array(dashboardWidgetInstanceSchema).max(MAX_WIDGETS_PER_COLUMN),
|
|
25
31
|
});
|
|
26
32
|
export const dashboardSectionSchema = z.object({
|
|
27
|
-
id: z.string().min(1),
|
|
33
|
+
id: z.string().min(1).max(ID_MAX),
|
|
28
34
|
label: z.string().max(60).optional(),
|
|
29
35
|
hideLabel: z.boolean().optional(),
|
|
30
36
|
collapsible: z.boolean().optional(),
|
|
@@ -32,16 +38,18 @@ export const dashboardSectionSchema = z.object({
|
|
|
32
38
|
columnSpans: z.array(z.number().int().min(1).max(12)).optional(),
|
|
33
39
|
});
|
|
34
40
|
export const dashboardPageSchema = z.object({
|
|
35
|
-
id: z.string().min(1),
|
|
41
|
+
id: z.string().min(1).max(ID_MAX),
|
|
36
42
|
slug: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).max(40),
|
|
37
43
|
label: z.string().min(1).max(60),
|
|
38
44
|
icon: z.string().max(40).optional(),
|
|
39
|
-
sections: z.array(dashboardSectionSchema).min(1),
|
|
45
|
+
sections: z.array(dashboardSectionSchema).min(1).max(MAX_SECTIONS_PER_PAGE),
|
|
40
46
|
});
|
|
41
|
-
export const dashboardLayoutSchema = z
|
|
47
|
+
export const dashboardLayoutSchema = z
|
|
48
|
+
.object({
|
|
42
49
|
version: z.literal(1),
|
|
43
50
|
pages: z.array(dashboardPageSchema).min(1).max(8),
|
|
44
|
-
})
|
|
51
|
+
})
|
|
52
|
+
.refine((layout) => new TextEncoder().encode(JSON.stringify(layout)).length <= MAX_DASHBOARD_LAYOUT_BYTES, { message: `Layout exceeds the ${MAX_DASHBOARD_LAYOUT_BYTES / 1024} KB size limit.` });
|
|
45
53
|
// ---------------------------------------------------------------------------
|
|
46
54
|
// Default dashboard layout generator
|
|
47
55
|
// ---------------------------------------------------------------------------
|
|
@@ -130,7 +138,7 @@ export function validateDashboardLayout(layout, ctx) {
|
|
|
130
138
|
...col,
|
|
131
139
|
widgets: (col.widgets ?? []).filter((w) => {
|
|
132
140
|
const seedSlug = (w.config ?? {})['seedSlug'];
|
|
133
|
-
if (typeof seedSlug === 'string' && !ctx.seedSlugs.has(seedSlug)) {
|
|
141
|
+
if (typeof seedSlug === 'string' && seedSlug !== '' && !ctx.seedSlugs.has(seedSlug)) {
|
|
134
142
|
warnings.push(`Widget '${w.type}' (id=${w.id}) removed: seed '${seedSlug}' is not registered.`);
|
|
135
143
|
return false;
|
|
136
144
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Seed, Branch, FilterGroup } from '../../engine/types.js';
|
|
2
2
|
/** Branch types that can form a discrete, finite set of columns (KB §2). */
|
|
3
|
-
export type KanbanAxisBranchType = '
|
|
3
|
+
export type KanbanAxisBranchType = 'tags' | 'boolean';
|
|
4
4
|
/** A branch eligible to be the kanban axis. */
|
|
5
5
|
export interface KanbanAxisCandidate {
|
|
6
6
|
/** Stable branch id (br_XX) — persisted as the axis key. Never the alias. */
|
|
@@ -14,9 +14,17 @@ export interface KanbanColumnDescriptor {
|
|
|
14
14
|
/** The discrete axis value. `null` ⇒ the always-last "Senza valore" column. */
|
|
15
15
|
value: string | null;
|
|
16
16
|
label: string;
|
|
17
|
+
/** The option color (#RRGGBB) of a tags-axis column. Absent on boolean columns and on the null column. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/**
|
|
20
|
+
* True on the null column of a required axis. No valid entry belongs there: it only collects entries
|
|
21
|
+
* whose value is missing or no longer an option, so none disappears from the board. The board shows it
|
|
22
|
+
* only while it holds entries and never moves or creates an entry into it.
|
|
23
|
+
*/
|
|
24
|
+
fallback?: true;
|
|
17
25
|
}
|
|
18
26
|
/** Per-seed, dashboard-side kanban view preferences. Persisted in
|
|
19
|
-
*
|
|
27
|
+
* seed_views.config.kanban (one per Kanban view instance). NOT part of FormLayout. */
|
|
20
28
|
export interface KanbanConfig {
|
|
21
29
|
/** Chosen axis branch id, or null when the user has not configured one (KB-U02). */
|
|
22
30
|
axisBranchId: string | null;
|
|
@@ -41,19 +49,16 @@ export interface KanbanCompatibility {
|
|
|
41
49
|
* Q3: seeds with `allowDrafts: true` are NOT kanban-compatible.
|
|
42
50
|
*/
|
|
43
51
|
export declare function resolveKanbanConfig(seed: Seed): KanbanCompatibility;
|
|
52
|
+
/** True when every entry must hold an axis value, so moving a card to the null column is never valid. */
|
|
53
|
+
export declare function isKanbanAxisRequired(branch: Pick<Branch, 'requiredOnCreate' | 'requiredOnUpdate'>): boolean;
|
|
44
54
|
/**
|
|
45
55
|
* Deterministic, stable column order for a chosen axis (Q2). The "Senza valore"
|
|
46
|
-
* (value: null) column is always appended last
|
|
47
|
-
* given their own column (KB-U25)
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
export declare function resolveKanbanColumns(branch: Branch, distinctTagValues?: string[]): KanbanColumnDescriptor[];
|
|
52
|
-
/**
|
|
53
|
-
* Pure: the FilterGroup selecting one kanban column's entries for a given axis branch.
|
|
54
|
-
* `value: null` ⇒ "Senza valore" — entries where the axis field is NULL or empty (KB-U25).
|
|
55
|
-
* Boolean descriptor values 'true'/'false' are coerced to actual booleans for the engine.
|
|
56
|
+
* (value: null) column is always appended last; on a required axis it is a `fallback` column.
|
|
57
|
+
* Values out of `options` are NOT given their own column (KB-U25): {@link kanbanColumnFilter}
|
|
58
|
+
* folds them into "Senza valore".
|
|
59
|
+
* A tags axis yields one column per option, in declared order, carrying the option color.
|
|
56
60
|
*/
|
|
61
|
+
export declare function resolveKanbanColumns(branch: Branch): KanbanColumnDescriptor[];
|
|
57
62
|
/** Body of PATCH /:slug/:id/kanban-move. The server applies the axis change (if any)
|
|
58
63
|
* AND the position upsert atomically (KB-S04e). */
|
|
59
64
|
export interface KanbanMoveBody {
|
|
@@ -61,15 +66,24 @@ export interface KanbanMoveBody {
|
|
|
61
66
|
axisBranchId: string;
|
|
62
67
|
/** New fractional-index key for this entry in the destination column (KB-S04b/d). */
|
|
63
68
|
position: string;
|
|
64
|
-
/** Present only on a cross-column move
|
|
69
|
+
/** Present only on a cross-column move: the destination column value (`null` = "Senza valore"). */
|
|
65
70
|
axis?: {
|
|
66
|
-
kind: 'scalar';
|
|
67
71
|
value: string | null;
|
|
68
|
-
} | {
|
|
69
|
-
kind: 'tags';
|
|
70
|
-
oldValue: string | null;
|
|
71
|
-
newValue: string | null;
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The entry value that puts an entry in the kanban column `columnValue` of `branch`:
|
|
76
|
+
* boolean → `true`/`false` (`null` column → `null`), tags → `[columnValue]` (`null` column → `[]`),
|
|
77
|
+
* any other type → `columnValue` unchanged. Pure.
|
|
78
|
+
*/
|
|
79
|
+
export declare function kanbanColumnEntryValue(branch: Branch, columnValue: string | null): boolean | string | string[] | null;
|
|
80
|
+
/**
|
|
81
|
+
* Pure: the FilterGroup selecting one kanban column's entries for a given axis branch.
|
|
82
|
+
* `value: null` ⇒ "Senza valore" — entries where the axis field is NULL or empty (KB-U25).
|
|
83
|
+
* On a tags axis with options it also holds entries whose stored value is no longer an option
|
|
84
|
+
* (removed or renamed in the seed): `not_in` the option values matches both, so no entry
|
|
85
|
+
* falls outside every column.
|
|
86
|
+
* Boolean descriptor values 'true'/'false' are coerced to actual booleans for the engine.
|
|
87
|
+
*/
|
|
74
88
|
export declare function kanbanColumnFilter(branch: Branch, value: string | null): FilterGroup;
|
|
75
89
|
//# sourceMappingURL=kanban.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kanban.d.ts","sourceRoot":"","sources":["../../../src/dashboard-layout/kanban/kanban.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"kanban.d.ts","sourceRoot":"","sources":["../../../src/dashboard-layout/kanban/kanban.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAItE,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAA;AAErD,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,0GAA0G;IAC1G,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,IAAI,CAAA;CAChB;AAED;uFACuF;AACvF,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oFAAoF;IACpF,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACtD,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAED,MAAM,MAAM,wBAAwB,GAAG,gBAAgB,GAAG,qBAAqB,CAAA;AAE/E,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,wBAAwB,CAAA;IACjC,UAAU,EAAE,mBAAmB,EAAE,CAAA;CAClC;AAkBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,mBAAmB,CAanE;AAED,yGAAyG;AACzG,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAAC,GAAG,OAAO,CAE3G;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,EAAE,CAY7E;AAED;oDACoD;AACpD,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,YAAY,EAAE,MAAM,CAAA;IACpB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAA;IAChB,mGAAmG;IACnG,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,IAAI,GACzB,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAIpC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAgBpF"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { isLocalizedBranch } from '../../engine/localization/localization.js';
|
|
4
|
+
import { isSingleChoiceTags, tagOptionValues } from '../../engine/seeds/tag-options.js';
|
|
3
5
|
/** Branch types that are explicitly never an axis (KB §2). */
|
|
4
6
|
const NON_AXIS_TYPES = new Set([
|
|
5
7
|
'richtext', 'file', 'number', 'date', 'json', 'relation', 'repeater',
|
|
@@ -7,12 +9,14 @@ const NON_AXIS_TYPES = new Set([
|
|
|
7
9
|
function isAxisCandidate(b) {
|
|
8
10
|
if (b.alias === 'status')
|
|
9
11
|
return null; // system status excluded (KB §2)
|
|
12
|
+
// A localized value is a locale dictionary, not one discrete column value, and a card move would
|
|
13
|
+
// overwrite every translation with the column value.
|
|
14
|
+
if (isLocalizedBranch(b))
|
|
15
|
+
return null;
|
|
10
16
|
if (NON_AXIS_TYPES.has(b.type))
|
|
11
17
|
return null;
|
|
12
|
-
if (b.type === 'text')
|
|
13
|
-
return (b.options && b.options.length > 0) ? 'text' : null;
|
|
14
18
|
if (b.type === 'tags')
|
|
15
|
-
return 'tags';
|
|
19
|
+
return isSingleChoiceTags(b) && tagOptionValues(b).length > 0 ? 'tags' : null;
|
|
16
20
|
if (b.type === 'boolean')
|
|
17
21
|
return 'boolean';
|
|
18
22
|
return null;
|
|
@@ -38,37 +42,57 @@ export function resolveKanbanConfig(seed) {
|
|
|
38
42
|
}
|
|
39
43
|
return { compatible: true, candidates };
|
|
40
44
|
}
|
|
45
|
+
/** True when every entry must hold an axis value, so moving a card to the null column is never valid. */
|
|
46
|
+
export function isKanbanAxisRequired(branch) {
|
|
47
|
+
return Boolean(branch.requiredOnCreate || branch.requiredOnUpdate);
|
|
48
|
+
}
|
|
41
49
|
/**
|
|
42
50
|
* Deterministic, stable column order for a chosen axis (Q2). The "Senza valore"
|
|
43
|
-
* (value: null) column is always appended last
|
|
44
|
-
* given their own column (KB-U25)
|
|
45
|
-
*
|
|
46
|
-
*
|
|
51
|
+
* (value: null) column is always appended last; on a required axis it is a `fallback` column.
|
|
52
|
+
* Values out of `options` are NOT given their own column (KB-U25): {@link kanbanColumnFilter}
|
|
53
|
+
* folds them into "Senza valore".
|
|
54
|
+
* A tags axis yields one column per option, in declared order, carrying the option color.
|
|
47
55
|
*/
|
|
48
|
-
export function resolveKanbanColumns(branch
|
|
56
|
+
export function resolveKanbanColumns(branch) {
|
|
49
57
|
const cols = [];
|
|
50
58
|
if (branch.type === 'boolean') {
|
|
51
59
|
cols.push({ value: 'false', label: 'No' }, { value: 'true', label: 'Sì' }); // [false, true]
|
|
52
60
|
}
|
|
53
|
-
else if (branch.type === 'text' && branch.options?.length) {
|
|
54
|
-
for (const o of branch.options)
|
|
55
|
-
cols.push({ value: o, label: o }); // options order
|
|
56
|
-
}
|
|
57
61
|
else if (branch.type === 'tags') {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const o of ordered)
|
|
62
|
-
cols.push({ value: o, label: o });
|
|
63
|
-
}
|
|
64
|
-
const isRequired = Boolean(branch.requiredOnCreate || branch.requiredOnUpdate);
|
|
65
|
-
if (!isRequired) {
|
|
66
|
-
cols.push({ value: null, label: `Senza ${branch.label}` }); // always last if not required
|
|
62
|
+
for (const option of branch.options ?? []) {
|
|
63
|
+
cols.push({ value: option.value, label: option.value, color: option.color });
|
|
64
|
+
}
|
|
67
65
|
}
|
|
66
|
+
const nullColumn = { value: null, label: `Senza ${branch.label}` };
|
|
67
|
+
cols.push(isKanbanAxisRequired(branch) ? { ...nullColumn, fallback: true } : nullColumn);
|
|
68
68
|
return cols;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The entry value that puts an entry in the kanban column `columnValue` of `branch`:
|
|
72
|
+
* boolean → `true`/`false` (`null` column → `null`), tags → `[columnValue]` (`null` column → `[]`),
|
|
73
|
+
* any other type → `columnValue` unchanged. Pure.
|
|
74
|
+
*/
|
|
75
|
+
export function kanbanColumnEntryValue(branch, columnValue) {
|
|
76
|
+
if (branch.type === 'boolean')
|
|
77
|
+
return columnValue === null ? null : columnValue === 'true';
|
|
78
|
+
if (branch.type === 'tags')
|
|
79
|
+
return columnValue === null ? [] : [columnValue];
|
|
80
|
+
return columnValue;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pure: the FilterGroup selecting one kanban column's entries for a given axis branch.
|
|
84
|
+
* `value: null` ⇒ "Senza valore" — entries where the axis field is NULL or empty (KB-U25).
|
|
85
|
+
* On a tags axis with options it also holds entries whose stored value is no longer an option
|
|
86
|
+
* (removed or renamed in the seed): `not_in` the option values matches both, so no entry
|
|
87
|
+
* falls outside every column.
|
|
88
|
+
* Boolean descriptor values 'true'/'false' are coerced to actual booleans for the engine.
|
|
89
|
+
*/
|
|
70
90
|
export function kanbanColumnFilter(branch, value) {
|
|
71
91
|
if (value === null) {
|
|
92
|
+
const optionValues = branch.type === 'tags' ? tagOptionValues(branch) : [];
|
|
93
|
+
if (optionValues.length > 0) {
|
|
94
|
+
return { column: branch.alias, type: 'tags', conditions: [{ op: 'not_in', value: optionValues }] };
|
|
95
|
+
}
|
|
72
96
|
const type = branch.type === 'boolean' ? 'boolean' : branch.type === 'tags' ? 'tags' : 'text';
|
|
73
97
|
return { column: branch.alias, type, conditions: [{ op: 'is_empty', value: null }] };
|
|
74
98
|
}
|