@xleddyl/nuxt-cms 0.4.2 → 0.4.3
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/README.md +74 -96
- package/dist/module.json +1 -1
- package/dist/module.mjs +84 -16
- package/dist/runtime/app/components/cms/BlocksField.vue +30 -5
- package/dist/runtime/app/components/cms/DropdownMenu.vue +1 -5
- package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/EntryDrawer.vue +32 -13
- package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +20 -3
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +2 -2
- package/dist/runtime/app/components/cms/Form.d.vue.ts +8 -6
- package/dist/runtime/app/components/cms/Form.vue +26 -5
- package/dist/runtime/app/components/cms/Form.vue.d.ts +8 -6
- package/dist/runtime/app/components/cms/FormField.vue +6 -1
- package/dist/runtime/app/components/cms/ImportModal.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/ImportModal.vue +252 -0
- package/dist/runtime/app/components/cms/ImportModal.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/ListFilters.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/ListFilters.vue +151 -0
- package/dist/runtime/app/components/cms/ListFilters.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/RelationField.vue +3 -1
- package/dist/runtime/app/components/cms/VisualEditor.vue +81 -12
- package/dist/runtime/app/pages/admin-collection.vue +158 -7
- package/dist/runtime/app/pages/admin-content.vue +18 -7
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/pages/admin-preview.vue +6 -3
- package/dist/runtime/app/preview/PreviewOverlay.vue +4 -0
- package/dist/runtime/app/utils/ui.d.ts +2 -1
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection-export.get.d.ts +2 -0
- package/dist/runtime/server/api/collection-export.get.js +67 -0
- package/dist/runtime/server/api/collection-import.post.d.ts +2 -0
- package/dist/runtime/server/api/collection-import.post.js +85 -0
- package/dist/runtime/server/api/collection.get.js +7 -15
- package/dist/runtime/server/utils/import.d.ts +31 -0
- package/dist/runtime/server/utils/import.js +202 -0
- package/dist/runtime/server/utils/list-query.d.ts +15 -0
- package/dist/runtime/server/utils/list-query.js +54 -0
- package/dist/runtime/server/utils/media-check.d.ts +2 -0
- package/dist/runtime/server/utils/media-check.js +1 -1
- package/dist/runtime/server/utils/media-references.js +4 -1
- package/dist/runtime/server/utils/registry.js +1 -1
- package/dist/runtime/server/utils/search.js +23 -4
- package/dist/runtime/shared/duplicate.d.ts +10 -0
- package/dist/runtime/shared/duplicate.js +54 -0
- package/dist/runtime/shared/index.d.ts +49 -2
- package/dist/runtime/shared/index.js +33 -0
- package/dist/runtime/shared/layout.d.ts +3 -0
- package/dist/runtime/shared/layout.js +35 -2
- package/dist/runtime/shared/list-filters.d.ts +20 -0
- package/dist/runtime/shared/list-filters.js +83 -0
- package/dist/runtime/shared/transfer.d.ts +70 -0
- package/dist/runtime/shared/transfer.js +256 -0
- package/dist/runtime/shared/validation.d.ts +15 -2
- package/dist/runtime/shared/validation.js +102 -9
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CONTENT_PUBLISHED_AT_FIELD, isCollectionKind, isTranslatableField } from "./index.js";
|
|
2
|
+
export const DUPLICATE_TITLE_SUFFIX = " (copy)";
|
|
3
|
+
const DUPLICATE_TOKEN_LENGTH = 4;
|
|
4
|
+
const DUPLICATE_TOKEN_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
5
|
+
const COPIED_SLUG = new RegExp(`-copy-[a-z0-9]{${DUPLICATE_TOKEN_LENGTH}}$`);
|
|
6
|
+
export function isUniqueField(field) {
|
|
7
|
+
return field.type === "slug" || field.type === "relation" && field.cardinality === "one-to-one";
|
|
8
|
+
}
|
|
9
|
+
export function uniqueFieldKeys(entry) {
|
|
10
|
+
return Object.keys(entry.fields).filter((key) => isUniqueField(entry.fields[key]));
|
|
11
|
+
}
|
|
12
|
+
export function duplicateToken() {
|
|
13
|
+
const bytes = new Uint8Array(DUPLICATE_TOKEN_LENGTH);
|
|
14
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
15
|
+
return [...bytes].map((byte) => DUPLICATE_TOKEN_ALPHABET[byte % DUPLICATE_TOKEN_ALPHABET.length]).join("");
|
|
16
|
+
}
|
|
17
|
+
export function duplicateSlug(value, token) {
|
|
18
|
+
return `${value.replace(COPIED_SLUG, "")}-copy-${token}`;
|
|
19
|
+
}
|
|
20
|
+
function suffixTitle(field, value) {
|
|
21
|
+
if (typeof value === "string") return value.trim() ? `${value}${DUPLICATE_TITLE_SUFFIX}` : value;
|
|
22
|
+
if (!value || typeof value !== "object" || !isTranslatableField(field)) return value;
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
Object.entries(value).map(([locale, text]) => [
|
|
25
|
+
locale,
|
|
26
|
+
typeof text === "string" && text.trim() ? `${text}${DUPLICATE_TITLE_SUFFIX}` : text
|
|
27
|
+
])
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
export function duplicateFormKeys(entry) {
|
|
31
|
+
const keys = Object.keys(entry.fields);
|
|
32
|
+
return isCollectionKind(entry) && entry.drafts && !keys.includes("status") ? [...keys, "status"] : keys;
|
|
33
|
+
}
|
|
34
|
+
export function duplicateEntryValues(entry, source, token = duplicateToken()) {
|
|
35
|
+
const picked = Object.fromEntries(
|
|
36
|
+
duplicateFormKeys(entry).map((key) => [key, source[key] ?? null])
|
|
37
|
+
);
|
|
38
|
+
const copy = JSON.parse(JSON.stringify(picked));
|
|
39
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
40
|
+
if (field.type === "slug") {
|
|
41
|
+
const slug = copy[key];
|
|
42
|
+
copy[key] = typeof slug === "string" && slug ? duplicateSlug(slug, token) : null;
|
|
43
|
+
} else if (isUniqueField(field)) {
|
|
44
|
+
copy[key] = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const titleField = entry.titleField ? entry.fields[entry.titleField] : void 0;
|
|
48
|
+
if (entry.titleField && titleField?.type === "text")
|
|
49
|
+
copy[entry.titleField] = suffixTitle(titleField, copy[entry.titleField]);
|
|
50
|
+
if (entry.kind === "content" && Object.hasOwn(copy, CONTENT_PUBLISHED_AT_FIELD))
|
|
51
|
+
copy[CONTENT_PUBLISHED_AT_FIELD] = null;
|
|
52
|
+
if (isCollectionKind(entry) && entry.drafts) copy.status = "draft";
|
|
53
|
+
return copy;
|
|
54
|
+
}
|
|
@@ -80,7 +80,44 @@ export interface BlockConfig {
|
|
|
80
80
|
icon?: string;
|
|
81
81
|
description?: string;
|
|
82
82
|
fields: Record<string, FieldConfig>;
|
|
83
|
+
validate?: CmsBlockValidate;
|
|
83
84
|
}
|
|
85
|
+
export type CmsValidationLevel = 'error' | 'warning';
|
|
86
|
+
export interface CmsValidationIssue {
|
|
87
|
+
field?: string;
|
|
88
|
+
message: string;
|
|
89
|
+
level?: CmsValidationLevel;
|
|
90
|
+
}
|
|
91
|
+
export interface CmsValidateContext {
|
|
92
|
+
locales: string[];
|
|
93
|
+
defaultLocale: string;
|
|
94
|
+
path?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface CmsBlockValidateContext extends CmsValidateContext {
|
|
97
|
+
index: number;
|
|
98
|
+
}
|
|
99
|
+
export type CmsValidateResult = readonly CmsValidationIssue[] | null | undefined | void;
|
|
100
|
+
export type CmsValidate = (values: Record<string, unknown>, context: CmsValidateContext) => CmsValidateResult;
|
|
101
|
+
export type CmsBlockValidate = (values: Record<string, unknown>, context: CmsBlockValidateContext) => CmsValidateResult;
|
|
102
|
+
export interface SelectOption {
|
|
103
|
+
value: string;
|
|
104
|
+
label: string;
|
|
105
|
+
}
|
|
106
|
+
export type SelectOptionInput = string | {
|
|
107
|
+
readonly value: string;
|
|
108
|
+
readonly label: string;
|
|
109
|
+
};
|
|
110
|
+
export type CmsSelectOptionValue<O> = O extends string ? O : O extends {
|
|
111
|
+
readonly value: infer V extends string;
|
|
112
|
+
} ? V : never;
|
|
113
|
+
export type CmsSelectValue<F extends {
|
|
114
|
+
readonly options: readonly unknown[];
|
|
115
|
+
}> = CmsSelectOptionValue<F['options'][number]>;
|
|
116
|
+
export declare function selectOptions(field: Pick<FieldConfig, 'options'>): SelectOption[];
|
|
117
|
+
export declare function selectOptionValues(field: Pick<FieldConfig, 'options'>): string[];
|
|
118
|
+
export declare function selectOptionLabel(field: Pick<FieldConfig, 'options'>, value: unknown): string;
|
|
119
|
+
export declare function selectValuesMatching(field: Pick<FieldConfig, 'options'>, term: string): string[];
|
|
120
|
+
export declare function selectDisplayValue(field: Pick<FieldConfig, 'options'>, value: unknown): string;
|
|
84
121
|
export type ConditionValue = string | number | boolean;
|
|
85
122
|
export interface FieldCondition {
|
|
86
123
|
field: string;
|
|
@@ -102,10 +139,12 @@ export interface FieldConfig {
|
|
|
102
139
|
textarea?: boolean;
|
|
103
140
|
integer?: boolean;
|
|
104
141
|
translatable?: boolean;
|
|
105
|
-
options?:
|
|
142
|
+
options?: readonly SelectOptionInput[];
|
|
106
143
|
multiple?: boolean;
|
|
107
144
|
from?: string;
|
|
108
145
|
blocks?: Record<string, BlockConfig>;
|
|
146
|
+
min?: number;
|
|
147
|
+
max?: number;
|
|
109
148
|
mediaType?: MediaType | MediaType[];
|
|
110
149
|
accept?: string[];
|
|
111
150
|
mobile?: boolean;
|
|
@@ -121,6 +160,7 @@ export declare function fieldConditions(field: FieldConfig): FieldCondition[];
|
|
|
121
160
|
export declare function isFieldVisible(field: FieldConfig, values: Record<string, unknown> | null | undefined): boolean;
|
|
122
161
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
123
162
|
export declare function isTranslatableMediaField(field: FieldConfig): boolean;
|
|
163
|
+
export declare function defaultLocaleValue(value: unknown, defaultLocale: string): string | null;
|
|
124
164
|
export declare function decodeTranslatableValue(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
125
165
|
export declare const decodeTranslatableMedia: typeof decodeTranslatableValue;
|
|
126
166
|
export declare function encodeTranslatableMedia(value: unknown): string | null;
|
|
@@ -132,6 +172,7 @@ export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
|
132
172
|
export declare function translatableFieldKeys(entry: EntryLike): string[];
|
|
133
173
|
export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
|
|
134
174
|
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
175
|
+
export declare function canAddBlock(field: Pick<FieldConfig, 'max'>, count: number): boolean;
|
|
135
176
|
export declare const BLOCK_HIDDEN_KEY = "hidden";
|
|
136
177
|
export declare function declaresHiddenField(block: BlockConfig | undefined): boolean;
|
|
137
178
|
export declare function isHiddenBlock(item: unknown): boolean;
|
|
@@ -168,6 +209,7 @@ export interface CmsEntry {
|
|
|
168
209
|
blocks?: Record<string, BlockConfig>;
|
|
169
210
|
preview?: CmsPreviewOptions;
|
|
170
211
|
declared?: CmsContentDeclared;
|
|
212
|
+
validate?: CmsValidate;
|
|
171
213
|
}
|
|
172
214
|
export interface CmsPreviewOptions {
|
|
173
215
|
component?: string;
|
|
@@ -258,7 +300,7 @@ export interface SlugFieldInput extends FieldInputBase {
|
|
|
258
300
|
}
|
|
259
301
|
export interface SelectFieldInput extends FieldInputBase {
|
|
260
302
|
type: 'select';
|
|
261
|
-
options:
|
|
303
|
+
options: readonly SelectOptionInput[];
|
|
262
304
|
multiple?: boolean;
|
|
263
305
|
}
|
|
264
306
|
export interface JsonFieldInput extends FieldInputBase {
|
|
@@ -285,10 +327,13 @@ export interface BlockInput {
|
|
|
285
327
|
icon?: string;
|
|
286
328
|
description?: string;
|
|
287
329
|
fields: Record<string, BlockFieldInput>;
|
|
330
|
+
validate?: CmsBlockValidate;
|
|
288
331
|
}
|
|
289
332
|
export interface BlocksFieldInput extends FieldInputBase {
|
|
290
333
|
type: 'blocks';
|
|
291
334
|
blocks: Record<string, BlockInput>;
|
|
335
|
+
min?: number;
|
|
336
|
+
max?: number;
|
|
292
337
|
}
|
|
293
338
|
export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | DateTimeFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
|
|
294
339
|
interface CmsEntryInputBase {
|
|
@@ -300,6 +345,7 @@ interface CmsEntryInputBase {
|
|
|
300
345
|
drafts?: boolean;
|
|
301
346
|
tabs?: CmsTab[];
|
|
302
347
|
fields: Record<string, CmsFieldInput>;
|
|
348
|
+
validate?: CmsValidate;
|
|
303
349
|
}
|
|
304
350
|
export interface CmsCollectionInput extends CmsEntryInputBase {
|
|
305
351
|
kind: 'collection';
|
|
@@ -333,6 +379,7 @@ export interface CmsContentInput {
|
|
|
333
379
|
layout?: CmsLayoutBlock[];
|
|
334
380
|
list?: CmsListInput;
|
|
335
381
|
preview?: CmsPreviewOptions;
|
|
382
|
+
validate?: CmsValidate;
|
|
336
383
|
titleField?: never;
|
|
337
384
|
drafts?: never;
|
|
338
385
|
}
|
|
@@ -123,6 +123,31 @@ export function groupMediaUsage(usage) {
|
|
|
123
123
|
export function mediaUsageGroups(usage, keys) {
|
|
124
124
|
return [...new Set(keys)].map((key) => ({ key, entries: groupMediaUsage(usage[key] ?? []) })).filter((group) => group.entries.length > 0);
|
|
125
125
|
}
|
|
126
|
+
export function selectOptions(field) {
|
|
127
|
+
return (field.options ?? []).map(
|
|
128
|
+
(option) => typeof option === "string" ? { value: option, label: option } : {
|
|
129
|
+
value: String(option?.value ?? ""),
|
|
130
|
+
label: String(option?.label ?? option?.value ?? "")
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
export function selectOptionValues(field) {
|
|
135
|
+
return selectOptions(field).map((option) => option.value);
|
|
136
|
+
}
|
|
137
|
+
export function selectOptionLabel(field, value) {
|
|
138
|
+
const text = String(value);
|
|
139
|
+
return selectOptions(field).find((option) => option.value === text)?.label ?? text;
|
|
140
|
+
}
|
|
141
|
+
export function selectValuesMatching(field, term) {
|
|
142
|
+
const needle = term.trim().toLowerCase();
|
|
143
|
+
if (!needle) return [];
|
|
144
|
+
return selectOptions(field).filter((option) => option.label !== option.value).filter((option) => option.label.toLowerCase().includes(needle)).map((option) => option.value);
|
|
145
|
+
}
|
|
146
|
+
export function selectDisplayValue(field, value) {
|
|
147
|
+
if (value == null) return "";
|
|
148
|
+
if (Array.isArray(value)) return value.map((item) => selectOptionLabel(field, item)).join(", ");
|
|
149
|
+
return selectOptionLabel(field, value);
|
|
150
|
+
}
|
|
126
151
|
export function isPrivateField(field) {
|
|
127
152
|
return !!field.private;
|
|
128
153
|
}
|
|
@@ -159,6 +184,11 @@ function parseJsonObject(raw) {
|
|
|
159
184
|
}
|
|
160
185
|
return null;
|
|
161
186
|
}
|
|
187
|
+
export function defaultLocaleValue(value, defaultLocale) {
|
|
188
|
+
const values = decodeTranslatableValue(value, defaultLocale);
|
|
189
|
+
const text = values?.[defaultLocale];
|
|
190
|
+
return typeof text === "string" ? text : null;
|
|
191
|
+
}
|
|
162
192
|
export function decodeTranslatableValue(value, defaultLocale) {
|
|
163
193
|
if (value == null) return null;
|
|
164
194
|
if (typeof value === "object") return value;
|
|
@@ -219,6 +249,9 @@ export function hasTranslatableBlockFields(field) {
|
|
|
219
249
|
(block) => Object.values(block.fields).some(isTranslatableField)
|
|
220
250
|
);
|
|
221
251
|
}
|
|
252
|
+
export function canAddBlock(field, count) {
|
|
253
|
+
return field.max === void 0 || count < field.max;
|
|
254
|
+
}
|
|
222
255
|
export const BLOCK_HIDDEN_KEY = "hidden";
|
|
223
256
|
export function declaresHiddenField(block) {
|
|
224
257
|
return !!block && Object.hasOwn(block.fields, BLOCK_HIDDEN_KEY);
|
|
@@ -19,6 +19,7 @@ export interface CmsFormLayout {
|
|
|
19
19
|
}
|
|
20
20
|
export interface CmsListInput {
|
|
21
21
|
columns?: string[];
|
|
22
|
+
filters?: string[];
|
|
22
23
|
}
|
|
23
24
|
export interface CmsEntrySettings {
|
|
24
25
|
columns?: string[];
|
|
@@ -30,4 +31,6 @@ export declare function resolveFormLayout(fields: Record<string, FieldConfig>, l
|
|
|
30
31
|
export declare function defaultListColumns(fields: Record<string, FieldConfig>, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
31
32
|
export declare function resolveListColumns(fields: Record<string, FieldConfig>, saved: string[] | undefined, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
32
33
|
export declare function layoutErrors(at: string, fields: Record<string, FieldConfig>, input: CmsLayoutBlock[] | undefined): string[];
|
|
34
|
+
export declare function isListFilterField(field: FieldConfig): boolean;
|
|
35
|
+
export declare function listFilterKeys(fields: Record<string, FieldConfig>, list: CmsListInput | undefined, extra?: string[], columns?: string[]): string[];
|
|
33
36
|
export declare function listErrors(at: string, fields: Record<string, FieldConfig>, list: CmsListInput | undefined, extra?: string[]): string[];
|
|
@@ -121,7 +121,40 @@ export function layoutErrors(at, fields, input) {
|
|
|
121
121
|
}
|
|
122
122
|
return errors;
|
|
123
123
|
}
|
|
124
|
+
export function isListFilterField(field) {
|
|
125
|
+
if (field.type === "boolean") return true;
|
|
126
|
+
if (field.type === "select") return !field.multiple;
|
|
127
|
+
if (field.type === "relation") return field.cardinality !== "many-to-many";
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
export function listFilterKeys(fields, list, extra = [], columns) {
|
|
131
|
+
const eligible = [
|
|
132
|
+
...Object.keys(fields).filter((key) => isListFilterField(fields[key])),
|
|
133
|
+
...extra.filter((key) => !Object.hasOwn(fields, key))
|
|
134
|
+
];
|
|
135
|
+
if (Array.isArray(list?.filters))
|
|
136
|
+
return [...new Set(list.filters.filter((key) => eligible.includes(key)))];
|
|
137
|
+
return columns ? eligible.filter((key) => columns.includes(key)) : eligible;
|
|
138
|
+
}
|
|
124
139
|
export function listErrors(at, fields, list, extra = []) {
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
const errors = [];
|
|
141
|
+
for (const key of list?.columns ?? []) {
|
|
142
|
+
if (!Object.hasOwn(fields, key) && !extra.includes(key))
|
|
143
|
+
errors.push(`${at}: list.columns names '${key}', which is not a declared field`);
|
|
144
|
+
}
|
|
145
|
+
if (list?.filters === void 0) return errors;
|
|
146
|
+
if (!Array.isArray(list.filters)) {
|
|
147
|
+
errors.push(`${at}: list.filters must be an array of field names`);
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
for (const key of list.filters) {
|
|
151
|
+
const field = Object.hasOwn(fields, key) ? fields[key] : void 0;
|
|
152
|
+
if (!field && extra.includes(key)) continue;
|
|
153
|
+
if (!field) errors.push(`${at}: list.filters names '${key}', which is not a declared field`);
|
|
154
|
+
else if (!isListFilterField(field))
|
|
155
|
+
errors.push(
|
|
156
|
+
`${at}: list.filters names '${key}', which is not a single select, boolean or single relation field`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return errors;
|
|
127
160
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CmsEntry, SelectOption } from './index.js';
|
|
2
|
+
export declare const LIST_FILTER_PREFIX = "filter.";
|
|
3
|
+
export declare const LIST_FILTER_STATUS = "status";
|
|
4
|
+
export declare const LIST_FILTER_ID_MAX = 256;
|
|
5
|
+
export type CmsListFilters = Record<string, string>;
|
|
6
|
+
type FilterEntry = Pick<CmsEntry, 'kind' | 'fields' | 'drafts' | 'list'>;
|
|
7
|
+
export declare function listFilterExtras(entry: FilterEntry): string[];
|
|
8
|
+
export declare function entryListFilterKeys(entry: FilterEntry, columns?: string[]): string[];
|
|
9
|
+
export declare function listFilterLabel(entry: FilterEntry, key: string): string;
|
|
10
|
+
export declare function listFilterOptions(entry: FilterEntry, key: string): SelectOption[] | null;
|
|
11
|
+
export declare function listFilterValueLabel(entry: FilterEntry, key: string, value: string): string;
|
|
12
|
+
export declare function readListFilterQuery(query: Record<string, unknown>): Record<string, unknown>;
|
|
13
|
+
export interface ListFilterParseResult {
|
|
14
|
+
filters: CmsListFilters;
|
|
15
|
+
errors: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function parseListFilters(entry: FilterEntry, query: Record<string, unknown>): ListFilterParseResult;
|
|
18
|
+
export declare function listFilterQuery(filters: CmsListFilters): Record<string, string>;
|
|
19
|
+
export declare function withoutListFilterQuery<T extends Record<string, unknown>>(query: T): Partial<T>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isCollectionKind, listFilterKeys, selectOptions } from "./index.js";
|
|
2
|
+
export const LIST_FILTER_PREFIX = "filter.";
|
|
3
|
+
export const LIST_FILTER_STATUS = "status";
|
|
4
|
+
export const LIST_FILTER_ID_MAX = 256;
|
|
5
|
+
const STATUS_OPTIONS = [
|
|
6
|
+
{ value: "draft", label: "Draft" },
|
|
7
|
+
{ value: "published", label: "Published" }
|
|
8
|
+
];
|
|
9
|
+
const BOOLEAN_OPTIONS = [
|
|
10
|
+
{ value: "true", label: "Yes" },
|
|
11
|
+
{ value: "false", label: "No" }
|
|
12
|
+
];
|
|
13
|
+
function isStatusFilter(entry, key) {
|
|
14
|
+
return key === LIST_FILTER_STATUS && !Object.hasOwn(entry.fields, key);
|
|
15
|
+
}
|
|
16
|
+
export function listFilterExtras(entry) {
|
|
17
|
+
return isCollectionKind(entry) && entry.drafts ? [LIST_FILTER_STATUS] : [];
|
|
18
|
+
}
|
|
19
|
+
export function entryListFilterKeys(entry, columns) {
|
|
20
|
+
if (!isCollectionKind(entry)) return [];
|
|
21
|
+
return listFilterKeys(entry.fields, entry.list, listFilterExtras(entry), columns);
|
|
22
|
+
}
|
|
23
|
+
export function listFilterLabel(entry, key) {
|
|
24
|
+
if (isStatusFilter(entry, key)) return "Status";
|
|
25
|
+
return entry.fields[key]?.label ?? key;
|
|
26
|
+
}
|
|
27
|
+
export function listFilterOptions(entry, key) {
|
|
28
|
+
if (isStatusFilter(entry, key)) return STATUS_OPTIONS;
|
|
29
|
+
const field = entry.fields[key];
|
|
30
|
+
if (field?.type === "select") return selectOptions(field);
|
|
31
|
+
if (field?.type === "boolean") return BOOLEAN_OPTIONS;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function listFilterValueLabel(entry, key, value) {
|
|
35
|
+
return listFilterOptions(entry, key)?.find((option) => option.value === value)?.label ?? value;
|
|
36
|
+
}
|
|
37
|
+
export function readListFilterQuery(query) {
|
|
38
|
+
const raw = {};
|
|
39
|
+
for (const [name, value] of Object.entries(query)) {
|
|
40
|
+
if (!name.startsWith(LIST_FILTER_PREFIX)) continue;
|
|
41
|
+
const key = name.slice(LIST_FILTER_PREFIX.length);
|
|
42
|
+
if (key) raw[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
export function parseListFilters(entry, query) {
|
|
47
|
+
const allowed = new Set(entryListFilterKeys(entry));
|
|
48
|
+
const filters = {};
|
|
49
|
+
const errors = [];
|
|
50
|
+
for (const [key, raw] of Object.entries(readListFilterQuery(query))) {
|
|
51
|
+
if (!allowed.has(key)) {
|
|
52
|
+
errors.push(`Unknown filter '${key}'`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (typeof raw !== "string") {
|
|
56
|
+
errors.push(`Filter '${key}' takes one value`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const value = raw.trim();
|
|
60
|
+
if (!value) continue;
|
|
61
|
+
const options = listFilterOptions(entry, key);
|
|
62
|
+
if (options && !options.some((option) => option.value === value)) {
|
|
63
|
+
errors.push(`Invalid value for filter '${key}'`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!options && value.length > LIST_FILTER_ID_MAX) {
|
|
67
|
+
errors.push(`Invalid value for filter '${key}'`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
filters[key] = value;
|
|
71
|
+
}
|
|
72
|
+
return { filters, errors };
|
|
73
|
+
}
|
|
74
|
+
export function listFilterQuery(filters) {
|
|
75
|
+
return Object.fromEntries(
|
|
76
|
+
Object.entries(filters).filter(([, value]) => typeof value === "string" && value !== "").map(([key, value]) => [`${LIST_FILTER_PREFIX}${key}`, value])
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
export function withoutListFilterQuery(query) {
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
Object.entries(query).filter(([name]) => !name.startsWith(LIST_FILTER_PREFIX))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CmsEntry, CmsI18n, FieldConfig } from './index.js';
|
|
2
|
+
export type CmsTransferFormat = 'json' | 'csv';
|
|
3
|
+
export type CmsImportMode = 'create' | 'upsert';
|
|
4
|
+
export declare const TRANSFER_FORMATS: CmsTransferFormat[];
|
|
5
|
+
export declare const IMPORT_MODES: CmsImportMode[];
|
|
6
|
+
export declare const IMPORT_MAX_BYTES: number;
|
|
7
|
+
export declare const IMPORT_MAX_ROWS = 2000;
|
|
8
|
+
export declare const IMPORT_ID_PATTERN: RegExp;
|
|
9
|
+
export declare const TRANSFER_ID_KEY = "id";
|
|
10
|
+
export declare const TRANSFER_TIMESTAMP_KEYS: readonly ["createdAt", "updatedAt"];
|
|
11
|
+
type TransferEntry = Pick<CmsEntry, 'kind' | 'fields' | 'drafts'>;
|
|
12
|
+
export interface CmsImportIssue {
|
|
13
|
+
path: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CmsImportSourceRow {
|
|
17
|
+
index: number;
|
|
18
|
+
values: Record<string, unknown>;
|
|
19
|
+
errors: CmsImportIssue[];
|
|
20
|
+
}
|
|
21
|
+
export interface CmsImportSource {
|
|
22
|
+
rows: CmsImportSourceRow[];
|
|
23
|
+
errors: string[];
|
|
24
|
+
}
|
|
25
|
+
export type CmsImportAction = 'create' | 'update';
|
|
26
|
+
export interface CmsImportPreviewRow {
|
|
27
|
+
index: number;
|
|
28
|
+
id: string | null;
|
|
29
|
+
action: CmsImportAction;
|
|
30
|
+
title: string | null;
|
|
31
|
+
errors: CmsImportIssue[];
|
|
32
|
+
}
|
|
33
|
+
export interface CmsImportPreview {
|
|
34
|
+
format: CmsTransferFormat;
|
|
35
|
+
mode: CmsImportMode;
|
|
36
|
+
total: number;
|
|
37
|
+
valid: number;
|
|
38
|
+
create: number;
|
|
39
|
+
update: number;
|
|
40
|
+
errors: string[];
|
|
41
|
+
rows: CmsImportPreviewRow[];
|
|
42
|
+
imported: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface CmsExportFile {
|
|
45
|
+
collection: string;
|
|
46
|
+
exportedAt: string;
|
|
47
|
+
total: number;
|
|
48
|
+
items: Record<string, unknown>[];
|
|
49
|
+
}
|
|
50
|
+
export declare function canTransferCsv(entry: Pick<CmsEntry, 'fields'>): boolean;
|
|
51
|
+
export declare function transferKeys(entry: TransferEntry): string[];
|
|
52
|
+
export declare function exportRow(entry: TransferEntry, row: Record<string, unknown>): Record<string, unknown>;
|
|
53
|
+
export declare function exportFileName(name: string, format: CmsTransferFormat, now?: Date): string;
|
|
54
|
+
export declare function csvColumns(entry: TransferEntry, i18n: CmsI18n): string[];
|
|
55
|
+
export declare function encodeCsvCell(value: string): string;
|
|
56
|
+
export declare function encodeCsv(rows: string[][]): string;
|
|
57
|
+
export declare function parseCsv(text: string): string[][];
|
|
58
|
+
export declare function entryRowsToCsv(entry: TransferEntry, rows: Record<string, unknown>[], i18n: CmsI18n): string;
|
|
59
|
+
type CellResult = {
|
|
60
|
+
value: unknown;
|
|
61
|
+
} | {
|
|
62
|
+
error: string;
|
|
63
|
+
};
|
|
64
|
+
export declare function csvCellValue(field: FieldConfig, text: string): CellResult;
|
|
65
|
+
export declare function csvImportSource(entry: TransferEntry, text: string, i18n: CmsI18n): CmsImportSource;
|
|
66
|
+
export declare function jsonImportSource(entry: TransferEntry, text: string): CmsImportSource;
|
|
67
|
+
export declare function importSource(entry: TransferEntry, format: CmsTransferFormat, text: string, i18n: CmsI18n): CmsImportSource;
|
|
68
|
+
export declare function importRowTitle(entry: Pick<CmsEntry, 'fields' | 'titleField'>, values: Record<string, unknown>, defaultLocale: string): string | null;
|
|
69
|
+
export declare function importIssuePath(path: readonly PropertyKey[]): string;
|
|
70
|
+
export {};
|