@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
|
@@ -3,6 +3,7 @@ type __VLS_Props = {
|
|
|
3
3
|
collection: string;
|
|
4
4
|
config: CmsEntry;
|
|
5
5
|
entryId: string | null;
|
|
6
|
+
initial?: Record<string, unknown> | null;
|
|
6
7
|
};
|
|
7
8
|
type __VLS_ModelProps = {
|
|
8
9
|
'open'?: boolean;
|
|
@@ -11,10 +12,12 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
|
11
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
13
|
saved: () => any;
|
|
13
14
|
deleted: () => any;
|
|
15
|
+
duplicate: (values: Record<string, unknown>) => any;
|
|
14
16
|
"update:open": (value: boolean) => any;
|
|
15
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
18
|
onSaved?: (() => any) | undefined;
|
|
17
19
|
onDeleted?: (() => any) | undefined;
|
|
20
|
+
onDuplicate?: ((values: Record<string, unknown>) => any) | undefined;
|
|
18
21
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
19
22
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
23
|
declare const _default: typeof __VLS_export;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
ref="form"
|
|
5
5
|
:state="state"
|
|
6
6
|
:schema="schema"
|
|
7
|
+
:warnings="warnings"
|
|
7
8
|
@submit="emit('submit')"
|
|
8
9
|
@error="onError"
|
|
9
10
|
>
|
|
@@ -112,7 +113,8 @@ import {
|
|
|
112
113
|
slugSourceValue
|
|
113
114
|
} from "#nuxt-cms";
|
|
114
115
|
import { computed, nextTick, onMounted, ref, watch } from "#imports";
|
|
115
|
-
import
|
|
116
|
+
import cmsConfig from "#cms-config";
|
|
117
|
+
import { buildEntrySchema, entryValidationWarnings } from "../../../shared/validation";
|
|
116
118
|
import { useCmsFieldFocus } from "../../composables/cms-field-focus";
|
|
117
119
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
118
120
|
import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
|
|
@@ -125,7 +127,8 @@ const props = defineProps({
|
|
|
125
127
|
loading: { type: Boolean, required: false },
|
|
126
128
|
formId: { type: String, required: false },
|
|
127
129
|
footer: { type: Boolean, required: false, default: true },
|
|
128
|
-
focus: { type: [Object, null], required: false }
|
|
130
|
+
focus: { type: [Object, null], required: false },
|
|
131
|
+
path: { type: String, required: false }
|
|
129
132
|
});
|
|
130
133
|
const state = defineModel({ type: Object, ...{ required: true } });
|
|
131
134
|
const emit = defineEmits(["submit", "error"]);
|
|
@@ -193,9 +196,23 @@ function localeFor(key) {
|
|
|
193
196
|
function setLocale(key, locale) {
|
|
194
197
|
activeLocale.value = { ...activeLocale.value, [key]: locale };
|
|
195
198
|
}
|
|
199
|
+
const validate = computed(
|
|
200
|
+
() => props.entryName ? cmsConfig[props.entryName]?.validate : void 0
|
|
201
|
+
);
|
|
196
202
|
const schema = computed(
|
|
197
|
-
() => buildEntrySchema(
|
|
203
|
+
() => buildEntrySchema(
|
|
204
|
+
{ fields: props.fields, drafts: props.drafts, validate: validate.value, path: props.path },
|
|
205
|
+
i18n
|
|
206
|
+
)
|
|
198
207
|
);
|
|
208
|
+
function warnings(values) {
|
|
209
|
+
return entryValidationWarnings(
|
|
210
|
+
{ fields: props.fields, validate: validate.value },
|
|
211
|
+
values,
|
|
212
|
+
i18n,
|
|
213
|
+
props.path
|
|
214
|
+
);
|
|
215
|
+
}
|
|
199
216
|
const published = computed({
|
|
200
217
|
get: () => state.value.status === "published",
|
|
201
218
|
set: (value) => {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
</template>
|
|
65
65
|
|
|
66
66
|
<script setup>
|
|
67
|
-
import { isTranslatableField } from "#nuxt-cms";
|
|
67
|
+
import { isTranslatableField, selectOptions } from "#nuxt-cms";
|
|
68
68
|
import { computed } from "#imports";
|
|
69
69
|
const props = defineProps({
|
|
70
70
|
field: { type: Object, required: true },
|
|
@@ -76,7 +76,7 @@ const props = defineProps({
|
|
|
76
76
|
const model = defineModel({ type: null, ...{ required: true } });
|
|
77
77
|
const selectItems = computed(() => [
|
|
78
78
|
...props.field.required || props.field.multiple ? [] : [{ label: "\u2014", value: null }],
|
|
79
|
-
...(props.field
|
|
79
|
+
...selectOptions(props.field).map(({ value, label }) => ({ label, value }))
|
|
80
80
|
]);
|
|
81
81
|
function proxy(fromModel, toModel = (v) => v) {
|
|
82
82
|
return computed({
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
interface FormIssue {
|
|
2
|
+
path: readonly PropertyKey[];
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
1
5
|
interface ParseResult {
|
|
2
6
|
success: boolean;
|
|
3
7
|
error?: {
|
|
4
|
-
issues: readonly
|
|
5
|
-
path: readonly PropertyKey[];
|
|
6
|
-
message: string;
|
|
7
|
-
}[];
|
|
8
|
+
issues: readonly FormIssue[];
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
type __VLS_Props = {
|
|
@@ -13,10 +14,11 @@ type __VLS_Props = {
|
|
|
13
14
|
schema?: {
|
|
14
15
|
safeParse: (value: unknown) => ParseResult;
|
|
15
16
|
};
|
|
17
|
+
warnings?: (value: Record<string, unknown>) => readonly FormIssue[];
|
|
16
18
|
};
|
|
17
|
-
declare var
|
|
19
|
+
declare var __VLS_13: {};
|
|
18
20
|
type __VLS_Slots = {} & {
|
|
19
|
-
default?: (props: typeof
|
|
21
|
+
default?: (props: typeof __VLS_13) => any;
|
|
20
22
|
};
|
|
21
23
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
24
|
error: () => any;
|
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<form :id="id" class="cms-form" @submit.prevent="onSubmit">
|
|
3
|
+
<CmsAlert v-if="errors[ROOT_KEY]" color="error">{{ errors[ROOT_KEY] }}</CmsAlert>
|
|
4
|
+
<CmsAlert v-for="message in rootWarnings" :key="message" color="warning">
|
|
5
|
+
{{ message }}
|
|
6
|
+
</CmsAlert>
|
|
3
7
|
<slot />
|
|
4
8
|
</form>
|
|
5
9
|
</template>
|
|
6
10
|
|
|
7
11
|
<script setup>
|
|
8
|
-
import { provide, reactive, watch } from "#imports";
|
|
9
|
-
import { CMS_FORM_ERRORS } from "../../utils/ui";
|
|
12
|
+
import { computed, provide, reactive, watch } from "#imports";
|
|
13
|
+
import { CMS_FORM_ERRORS, CMS_FORM_WARNINGS } from "../../utils/ui";
|
|
14
|
+
const ROOT_KEY = "";
|
|
10
15
|
const props = defineProps({
|
|
11
16
|
id: { type: String, required: false },
|
|
12
17
|
state: { type: Object, required: true },
|
|
13
|
-
schema: { type: Object, required: false }
|
|
18
|
+
schema: { type: Object, required: false },
|
|
19
|
+
warnings: { type: Function, required: false }
|
|
14
20
|
});
|
|
15
21
|
const emit = defineEmits(["submit", "error"]);
|
|
16
22
|
const errors = reactive({});
|
|
17
23
|
provide(CMS_FORM_ERRORS, errors);
|
|
24
|
+
const warningList = computed(() => props.warnings?.(props.state) ?? []);
|
|
25
|
+
const warningsByKey = computed(() => {
|
|
26
|
+
const grouped = {};
|
|
27
|
+
for (const issue of warningList.value) {
|
|
28
|
+
const key = String(issue.path[0] ?? ROOT_KEY);
|
|
29
|
+
if (key !== ROOT_KEY) grouped[key] ??= issue.message;
|
|
30
|
+
}
|
|
31
|
+
return grouped;
|
|
32
|
+
});
|
|
33
|
+
provide(CMS_FORM_WARNINGS, warningsByKey);
|
|
34
|
+
const rootWarnings = computed(() => [
|
|
35
|
+
...new Set(
|
|
36
|
+
warningList.value.filter((issue) => !issue.path.length).map((issue) => issue.message)
|
|
37
|
+
)
|
|
38
|
+
]);
|
|
18
39
|
let submitted = false;
|
|
19
40
|
function validate() {
|
|
20
41
|
for (const key of Object.keys(errors)) delete errors[key];
|
|
@@ -22,8 +43,8 @@ function validate() {
|
|
|
22
43
|
const result = props.schema.safeParse(props.state);
|
|
23
44
|
if (result.success) return true;
|
|
24
45
|
for (const issue of result.error?.issues ?? []) {
|
|
25
|
-
const key = String(issue.path[0] ??
|
|
26
|
-
if (
|
|
46
|
+
const key = String(issue.path[0] ?? ROOT_KEY);
|
|
47
|
+
if (!errors[key]) errors[key] = issue.message;
|
|
27
48
|
}
|
|
28
49
|
return false;
|
|
29
50
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
interface FormIssue {
|
|
2
|
+
path: readonly PropertyKey[];
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
1
5
|
interface ParseResult {
|
|
2
6
|
success: boolean;
|
|
3
7
|
error?: {
|
|
4
|
-
issues: readonly
|
|
5
|
-
path: readonly PropertyKey[];
|
|
6
|
-
message: string;
|
|
7
|
-
}[];
|
|
8
|
+
issues: readonly FormIssue[];
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
type __VLS_Props = {
|
|
@@ -13,10 +14,11 @@ type __VLS_Props = {
|
|
|
13
14
|
schema?: {
|
|
14
15
|
safeParse: (value: unknown) => ParseResult;
|
|
15
16
|
};
|
|
17
|
+
warnings?: (value: Record<string, unknown>) => readonly FormIssue[];
|
|
16
18
|
};
|
|
17
|
-
declare var
|
|
19
|
+
declare var __VLS_13: {};
|
|
18
20
|
type __VLS_Slots = {} & {
|
|
19
|
-
default?: (props: typeof
|
|
21
|
+
default?: (props: typeof __VLS_13) => any;
|
|
20
22
|
};
|
|
21
23
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
24
|
error: () => any;
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
<p v-if="error" class="cms-form-error">
|
|
13
13
|
<CmsIcon name="exclamation-circle" class="size-3.5 shrink-0" />{{ error }}
|
|
14
14
|
</p>
|
|
15
|
+
<p v-else-if="warning" class="cms-form-warning">
|
|
16
|
+
<CmsIcon name="exclamation-triangle" class="size-3.5 shrink-0" />{{ warning }}
|
|
17
|
+
</p>
|
|
15
18
|
</div>
|
|
16
19
|
</template>
|
|
17
20
|
|
|
18
21
|
<script setup>
|
|
19
22
|
import { computed, inject } from "#imports";
|
|
20
|
-
import { CMS_FORM_ERRORS } from "../../utils/ui";
|
|
23
|
+
import { CMS_FORM_ERRORS, CMS_FORM_WARNINGS } from "../../utils/ui";
|
|
21
24
|
const props = defineProps({
|
|
22
25
|
label: { type: String, required: false },
|
|
23
26
|
icon: { type: String, required: false },
|
|
@@ -26,5 +29,7 @@ const props = defineProps({
|
|
|
26
29
|
required: { type: Boolean, required: false }
|
|
27
30
|
});
|
|
28
31
|
const errors = inject(CMS_FORM_ERRORS, null);
|
|
32
|
+
const warnings = inject(CMS_FORM_WARNINGS, null);
|
|
29
33
|
const error = computed(() => props.name && errors ? errors[props.name] : void 0);
|
|
34
|
+
const warning = computed(() => props.name && warnings ? warnings.value[props.name] : void 0);
|
|
30
35
|
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CmsEntry } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
collection: string;
|
|
4
|
+
config: CmsEntry;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
'open'?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
imported: () => any;
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
|
+
onImported?: (() => any) | undefined;
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const _default: typeof __VLS_export;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<CmsModal v-model:open="open" :title="`Import ${config.label}`" size="lg">
|
|
3
|
+
<div class="cms-form">
|
|
4
|
+
<p class="cms-form-description">
|
|
5
|
+
{{ csv ? "A JSON or CSV file" : "A JSON file" }} in the format of the export, up to
|
|
6
|
+
{{ maxSize }} and {{ IMPORT_MAX_ROWS }} rows. Media fields take keys of files that are
|
|
7
|
+
already in the media library. Nothing is written until you confirm.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<div class="cms-import-file">
|
|
11
|
+
<input
|
|
12
|
+
ref="input"
|
|
13
|
+
type="file"
|
|
14
|
+
class="hidden"
|
|
15
|
+
:accept="csv ? '.json,.csv,application/json,text/csv' : '.json,application/json'"
|
|
16
|
+
@change="onFile"
|
|
17
|
+
/>
|
|
18
|
+
<CmsButton
|
|
19
|
+
:label="file ? 'Choose another file' : 'Choose file'"
|
|
20
|
+
icon="document-arrow-up"
|
|
21
|
+
variant="soft"
|
|
22
|
+
color="neutral"
|
|
23
|
+
:disabled="busy"
|
|
24
|
+
@click="input?.click()"
|
|
25
|
+
/>
|
|
26
|
+
<span v-if="file" class="cms-import-file-name">
|
|
27
|
+
{{ file.name }} · {{ formatFileSize(file.size) }}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<CmsFormField
|
|
32
|
+
label="Mode"
|
|
33
|
+
:description="
|
|
34
|
+
mode === 'create' ? 'Every row becomes a new entry. A row with the id of an existing entry is an error.' : 'A row with the id of an existing entry replaces it. Other rows become new entries.'
|
|
35
|
+
"
|
|
36
|
+
>
|
|
37
|
+
<div class="cms-media-filters">
|
|
38
|
+
<button
|
|
39
|
+
v-for="option in MODE_OPTIONS"
|
|
40
|
+
:key="option.value"
|
|
41
|
+
type="button"
|
|
42
|
+
class="cms-pill"
|
|
43
|
+
:class="{ 'is-active': mode === option.value }"
|
|
44
|
+
:disabled="busy"
|
|
45
|
+
@click="setMode(option.value)"
|
|
46
|
+
>
|
|
47
|
+
{{ option.label }}
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</CmsFormField>
|
|
51
|
+
|
|
52
|
+
<CmsSpinner v-if="checking" />
|
|
53
|
+
|
|
54
|
+
<CmsAlert v-else-if="fileErrors.length" color="error" title="The file cannot be imported">
|
|
55
|
+
<ul class="cms-import-list">
|
|
56
|
+
<li v-for="message in fileErrors" :key="message">{{ message }}</li>
|
|
57
|
+
</ul>
|
|
58
|
+
</CmsAlert>
|
|
59
|
+
|
|
60
|
+
<template v-else-if="preview">
|
|
61
|
+
<CmsAlert :color="invalidCount ? 'warning' : 'success'" :title="summary" />
|
|
62
|
+
<div class="cms-card cms-table-shell cms-import-preview">
|
|
63
|
+
<div class="cms-table-scroll">
|
|
64
|
+
<table class="cms-table">
|
|
65
|
+
<thead>
|
|
66
|
+
<tr>
|
|
67
|
+
<th>Row</th>
|
|
68
|
+
<th>Entry</th>
|
|
69
|
+
<th>Action</th>
|
|
70
|
+
<th>Problems</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
<tr v-for="row in shownRows" :key="row.index">
|
|
75
|
+
<td>{{ row.index }}</td>
|
|
76
|
+
<td class="cms-import-title">{{ row.title || row.id || "Untitled" }}</td>
|
|
77
|
+
<td>
|
|
78
|
+
<span class="cms-tag">
|
|
79
|
+
{{ row.action === "update" ? "Update" : "New" }}
|
|
80
|
+
</span>
|
|
81
|
+
</td>
|
|
82
|
+
<td>
|
|
83
|
+
<ul
|
|
84
|
+
v-if="row.errors.length"
|
|
85
|
+
class="cms-import-list cms-import-issues"
|
|
86
|
+
>
|
|
87
|
+
<li v-for="(issue, index) in row.errors" :key="index">
|
|
88
|
+
{{ issue.path ? `${issue.path}: ` : "" }}{{ issue.message }}
|
|
89
|
+
</li>
|
|
90
|
+
</ul>
|
|
91
|
+
<span v-else class="cms-import-ok">
|
|
92
|
+
<CmsIcon name="check" class="size-3.5" />Ready
|
|
93
|
+
</span>
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<div class="cms-actions is-end">
|
|
103
|
+
<CmsButton label="Cancel" variant="soft" color="neutral" @click="open = false" />
|
|
104
|
+
<CmsButton
|
|
105
|
+
:label="importLabel"
|
|
106
|
+
icon="arrow-down-tray"
|
|
107
|
+
:disabled="!canImport"
|
|
108
|
+
:loading="importing"
|
|
109
|
+
@click="commit"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</CmsModal>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<script setup>
|
|
117
|
+
import { formatFileSize } from "#nuxt-cms";
|
|
118
|
+
import { computed, ref, watch } from "#imports";
|
|
119
|
+
import { canTransferCsv, IMPORT_MAX_BYTES, IMPORT_MAX_ROWS } from "../../../shared/transfer";
|
|
120
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
121
|
+
import { errorMessage } from "../../utils/ui";
|
|
122
|
+
const MODE_OPTIONS = [
|
|
123
|
+
{ value: "create", label: "Create only" },
|
|
124
|
+
{ value: "upsert", label: "Create or update by id" }
|
|
125
|
+
];
|
|
126
|
+
const props = defineProps({
|
|
127
|
+
collection: { type: String, required: true },
|
|
128
|
+
config: { type: Object, required: true }
|
|
129
|
+
});
|
|
130
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
131
|
+
const emit = defineEmits(["imported"]);
|
|
132
|
+
const toast = useCmsToast();
|
|
133
|
+
const csv = canTransferCsv(props.config);
|
|
134
|
+
const maxSize = formatFileSize(IMPORT_MAX_BYTES);
|
|
135
|
+
const input = ref(null);
|
|
136
|
+
const file = ref(null);
|
|
137
|
+
const text = ref("");
|
|
138
|
+
const mode = ref("create");
|
|
139
|
+
const preview = ref(null);
|
|
140
|
+
const fileErrors = ref([]);
|
|
141
|
+
const checking = ref(false);
|
|
142
|
+
const importing = ref(false);
|
|
143
|
+
const busy = computed(() => checking.value || importing.value);
|
|
144
|
+
const invalidCount = computed(() => preview.value ? preview.value.total - preview.value.valid : 0);
|
|
145
|
+
const summary = computed(() => {
|
|
146
|
+
const current = preview.value;
|
|
147
|
+
if (!current) return "";
|
|
148
|
+
if (!current.total) return "The file has no rows";
|
|
149
|
+
const parts = [
|
|
150
|
+
current.create ? `${current.create} new` : "",
|
|
151
|
+
current.update ? `${current.update} to update` : ""
|
|
152
|
+
].filter(Boolean);
|
|
153
|
+
const ready = parts.length ? parts.join(", ") : "nothing ready";
|
|
154
|
+
return invalidCount.value ? `${invalidCount.value} of ${current.total} rows have problems; fix the file and choose it again` : `${current.total} rows checked: ${ready}`;
|
|
155
|
+
});
|
|
156
|
+
const shownRows = computed(() => {
|
|
157
|
+
const rows = preview.value?.rows ?? [];
|
|
158
|
+
return invalidCount.value ? rows.filter((row) => row.errors.length) : rows;
|
|
159
|
+
});
|
|
160
|
+
const canImport = computed(
|
|
161
|
+
() => !!preview.value && !busy.value && !fileErrors.value.length && preview.value.total > 0 && !invalidCount.value
|
|
162
|
+
);
|
|
163
|
+
const importLabel = computed(() => {
|
|
164
|
+
const total = preview.value?.total ?? 0;
|
|
165
|
+
return total ? `Import ${total} ${total === 1 ? "entry" : "entries"}` : "Import";
|
|
166
|
+
});
|
|
167
|
+
function formatOf(name) {
|
|
168
|
+
return name.toLowerCase().endsWith(".csv") ? "csv" : "json";
|
|
169
|
+
}
|
|
170
|
+
function reset() {
|
|
171
|
+
file.value = null;
|
|
172
|
+
text.value = "";
|
|
173
|
+
preview.value = null;
|
|
174
|
+
fileErrors.value = [];
|
|
175
|
+
mode.value = "create";
|
|
176
|
+
if (input.value) input.value.value = "";
|
|
177
|
+
}
|
|
178
|
+
watch(open, (value) => {
|
|
179
|
+
if (value) reset();
|
|
180
|
+
});
|
|
181
|
+
function send(dryRun) {
|
|
182
|
+
return $fetch(`/api/cms/import/${props.collection}`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
query: { format: formatOf(file.value.name), mode: mode.value, dryRun: String(dryRun) },
|
|
185
|
+
body: text.value,
|
|
186
|
+
headers: { "content-type": "text/plain" }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function readPreview(error) {
|
|
190
|
+
const data = error.data?.data;
|
|
191
|
+
return data && typeof data === "object" && Array.isArray(data.rows) ? data : null;
|
|
192
|
+
}
|
|
193
|
+
async function check() {
|
|
194
|
+
if (!file.value) return;
|
|
195
|
+
checking.value = true;
|
|
196
|
+
preview.value = null;
|
|
197
|
+
fileErrors.value = [];
|
|
198
|
+
try {
|
|
199
|
+
const result = await send(true);
|
|
200
|
+
preview.value = result;
|
|
201
|
+
fileErrors.value = result.errors;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
fileErrors.value = [errorMessage(error) ?? "The file could not be checked"];
|
|
204
|
+
} finally {
|
|
205
|
+
checking.value = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function onFile(event) {
|
|
209
|
+
const picked = event.target.files?.[0];
|
|
210
|
+
if (!picked) return;
|
|
211
|
+
file.value = picked;
|
|
212
|
+
preview.value = null;
|
|
213
|
+
if (picked.size > IMPORT_MAX_BYTES) {
|
|
214
|
+
fileErrors.value = [`The file is larger than ${maxSize}`];
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (formatOf(picked.name) === "csv" && !csv) {
|
|
218
|
+
fileErrors.value = ["This entry has blocks or many-to-many fields: use a JSON file"];
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
text.value = await picked.text();
|
|
222
|
+
await check();
|
|
223
|
+
}
|
|
224
|
+
function setMode(value) {
|
|
225
|
+
if (mode.value === value) return;
|
|
226
|
+
mode.value = value;
|
|
227
|
+
if (text.value) void check();
|
|
228
|
+
}
|
|
229
|
+
async function commit() {
|
|
230
|
+
if (!canImport.value) return;
|
|
231
|
+
importing.value = true;
|
|
232
|
+
try {
|
|
233
|
+
const result = await send(false);
|
|
234
|
+
const count = result.create + result.update;
|
|
235
|
+
toast.add({
|
|
236
|
+
title: `Imported ${count} ${count === 1 ? "entry" : "entries"}`,
|
|
237
|
+
color: "success"
|
|
238
|
+
});
|
|
239
|
+
emit("imported");
|
|
240
|
+
open.value = false;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const returned = readPreview(error);
|
|
243
|
+
if (returned) {
|
|
244
|
+
preview.value = returned;
|
|
245
|
+
fileErrors.value = returned.errors;
|
|
246
|
+
}
|
|
247
|
+
toast.add({ title: "Import failed", description: errorMessage(error), color: "error" });
|
|
248
|
+
} finally {
|
|
249
|
+
importing.value = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CmsEntry } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
collection: string;
|
|
4
|
+
config: CmsEntry;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
'open'?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
imported: () => any;
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
|
+
onImported?: (() => any) | undefined;
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const _default: typeof __VLS_export;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CmsEntry } from '#nuxt-cms';
|
|
2
|
+
import type { CmsListFilters } from '../../../shared/list-filters.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
config: CmsEntry;
|
|
5
|
+
keys: string[];
|
|
6
|
+
relations?: Record<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_ModelProps = {
|
|
9
|
+
modelValue: CmsListFilters;
|
|
10
|
+
};
|
|
11
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
"update:modelValue": (value: CmsListFilters) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:modelValue"?: ((value: CmsListFilters) => any) | undefined;
|
|
16
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const _default: typeof __VLS_export;
|
|
18
|
+
export default _default;
|