@xleddyl/nuxt-cms 0.1.61 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +133 -14
  3. package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
  4. package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
  5. package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
  6. package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
  7. package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
  8. package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
  9. package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
  10. package/dist/runtime/app/components/cms/Drawer.vue +5 -9
  11. package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
  12. package/dist/runtime/app/components/cms/EntryDrawer.vue +12 -3
  13. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue +104 -30
  15. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  17. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  18. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  19. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  20. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  21. package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
  22. package/dist/runtime/app/components/cms/Input.vue +7 -4
  23. package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
  24. package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
  25. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
  26. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
  27. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
  28. package/dist/runtime/app/components/cms/MediaGallery.vue +24 -10
  29. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  30. package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
  31. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  32. package/dist/runtime/app/components/cms/Modal.vue +5 -9
  33. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  34. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  35. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  36. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  37. package/dist/runtime/app/components/cms/PasswordInput.vue +81 -0
  38. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  39. package/dist/runtime/app/components/cms/RelationField.vue +7 -2
  40. package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
  41. package/dist/runtime/app/components/cms/SelectMenu.vue +4 -11
  42. package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
  43. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  44. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  45. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  46. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  47. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  48. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  49. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  50. package/dist/runtime/app/components/cms/SettingsEntry.vue +126 -0
  51. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  52. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  53. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  54. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  55. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  56. package/dist/runtime/app/components/cms/SettingsModal.vue +174 -0
  57. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  58. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  59. package/dist/runtime/app/components/cms/SettingsUsers.vue +274 -0
  60. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  61. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  62. package/dist/runtime/app/components/cms/Table.d.vue.ts +6 -8
  63. package/dist/runtime/app/components/cms/Table.vue +42 -47
  64. package/dist/runtime/app/components/cms/Table.vue.d.ts +6 -8
  65. package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
  66. package/dist/runtime/app/components/cms/Textarea.vue +1 -2
  67. package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
  68. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  69. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  70. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  71. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  72. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  73. package/dist/runtime/app/composables/cms-account.js +23 -0
  74. package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
  75. package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
  76. package/dist/runtime/app/composables/cms-media-keys.js +5 -0
  77. package/dist/runtime/app/composables/cms-media-library.js +3 -0
  78. package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -2
  79. package/dist/runtime/app/composables/cms-media-uploader.js +139 -12
  80. package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
  81. package/dist/runtime/app/composables/cms-overlay.js +23 -0
  82. package/dist/runtime/app/composables/cms-settings.d.ts +12 -0
  83. package/dist/runtime/app/composables/cms-settings.js +59 -0
  84. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  85. package/dist/runtime/app/composables/cms-theme.js +27 -0
  86. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  87. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  88. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  89. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  90. package/dist/runtime/app/pages/admin-entry.vue +2 -0
  91. package/dist/runtime/app/pages/admin-login.vue +18 -3
  92. package/dist/runtime/app/pages/admin-media.vue +5 -1
  93. package/dist/runtime/app/utils/ui.d.ts +2 -0
  94. package/dist/runtime/app/utils/ui.js +18 -0
  95. package/dist/runtime/assets/main.css +1 -1
  96. package/dist/runtime/generate.js +1 -1
  97. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  98. package/dist/runtime/server/api/account-password.put.js +38 -0
  99. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  100. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  101. package/dist/runtime/server/api/account.put.d.ts +3 -0
  102. package/dist/runtime/server/api/account.put.js +25 -0
  103. package/dist/runtime/server/api/collection.get.js +1 -1
  104. package/dist/runtime/server/api/media.delete.js +2 -1
  105. package/dist/runtime/server/api/media.post.js +2 -1
  106. package/dist/runtime/server/api/media.put.js +2 -1
  107. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  108. package/dist/runtime/server/api/settings.delete.js +12 -0
  109. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  110. package/dist/runtime/server/api/settings.get.js +15 -0
  111. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  112. package/dist/runtime/server/api/settings.put.js +14 -0
  113. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  114. package/dist/runtime/server/api/user.delete.js +12 -0
  115. package/dist/runtime/server/api/user.put.d.ts +3 -0
  116. package/dist/runtime/server/api/user.put.js +27 -0
  117. package/dist/runtime/server/api/users.get.d.ts +3 -0
  118. package/dist/runtime/server/api/users.get.js +11 -0
  119. package/dist/runtime/server/api/users.post.d.ts +3 -0
  120. package/dist/runtime/server/api/users.post.js +36 -0
  121. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  122. package/dist/runtime/server/plugins/session-validate.js +13 -0
  123. package/dist/runtime/server/routes/auth/login.post.js +20 -10
  124. package/dist/runtime/server/utils/graphql-depth.js +12 -5
  125. package/dist/runtime/server/utils/graphql.js +2 -2
  126. package/dist/runtime/server/utils/media-folders.js +10 -2
  127. package/dist/runtime/server/utils/media.d.ts +1 -0
  128. package/dist/runtime/server/utils/media.js +4 -1
  129. package/dist/runtime/server/utils/migrate.js +1 -1
  130. package/dist/runtime/server/utils/password.d.ts +3 -0
  131. package/dist/runtime/server/utils/password.js +31 -0
  132. package/dist/runtime/server/utils/require-admin.d.ts +4 -2
  133. package/dist/runtime/server/utils/require-admin.js +32 -28
  134. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  135. package/dist/runtime/server/utils/same-origin.js +24 -0
  136. package/dist/runtime/server/utils/settings.d.ts +16 -0
  137. package/dist/runtime/server/utils/settings.js +31 -0
  138. package/dist/runtime/server/utils/users.d.ts +15 -0
  139. package/dist/runtime/server/utils/users.js +34 -0
  140. package/dist/runtime/shared/index.d.ts +13 -1
  141. package/dist/runtime/shared/index.js +2 -0
  142. package/dist/runtime/shared/layout.d.ts +33 -0
  143. package/dist/runtime/shared/layout.js +127 -0
  144. package/dist/runtime/shared/users.d.ts +18 -0
  145. package/dist/runtime/shared/users.js +1 -0
  146. package/dist/runtime/shared/validation.js +5 -4
  147. package/package.json +3 -3
@@ -14,29 +14,72 @@
14
14
  {{ tab.label }}
15
15
  </button>
16
16
  </div>
17
- <CmsFormField
18
- v-for="(field, key) in visibleFields"
19
- :key="key"
20
- :label="field.label"
21
- :name="key"
22
- :required="field.required"
23
- >
24
- <template v-if="hasLocaleSwitch(field) && i18n.locales.length > 1" #label-actions>
25
- <CmsLocaleSwitch
26
- :model-value="localeFor(key)"
27
- :value="
28
- isTranslatableField(field) ? state[key] : null
17
+ <div class="cms-form-layout">
18
+ <div class="cms-form-main">
19
+ <component
20
+ :is="section.title ? 'section' : 'div'"
21
+ v-for="section in sections"
22
+ :key="section.id"
23
+ class="cms-form-section"
24
+ :class="{ 'is-titled': !!section.title, 'is-collapsed': isCollapsed(section) }"
25
+ >
26
+ <button
27
+ v-if="section.title"
28
+ type="button"
29
+ class="cms-form-section-header"
30
+ :aria-expanded="!isCollapsed(section)"
31
+ @click="toggleSection(section.id)"
32
+ >
33
+ <span class="cms-form-section-text">
34
+ <span class="cms-form-section-title">{{ section.title }}</span>
35
+ <span v-if="section.description" class="cms-form-section-description">
36
+ {{ section.description }}
37
+ </span>
38
+ </span>
39
+ <CmsIcon name="chevron-down" class="cms-form-section-chevron size-4" />
40
+ </button>
41
+ <div v-show="!isCollapsed(section)" class="cms-form-rows">
42
+ <div
43
+ v-for="row in section.rows"
44
+ :key="row.join('|')"
45
+ class="cms-form-row"
46
+ :style="{ '--cms-row-cols': row.length }"
47
+ >
48
+ <CmsFormField
49
+ v-for="key in row"
50
+ :key="key"
51
+ :label="fields[key].label"
52
+ :description="fields[key].description"
53
+ :icon="fieldIcon(fields[key])"
54
+ :name="key"
55
+ :required="fields[key].required"
56
+ >
57
+ <template
58
+ v-if="hasLocaleSwitch(fields[key]) && i18n.locales.length > 1"
59
+ #label-actions
60
+ >
61
+ <CmsLocaleSwitch
62
+ :model-value="localeFor(key)"
63
+ :value="
64
+ isTranslatableField(fields[key]) ? state[key] : null
29
65
  "
30
- @update:model-value="(locale) => setLocale(key, locale)"
31
- />
32
- </template>
33
- <CmsFieldInput
34
- v-model="state[key]"
35
- :field="field"
36
- :locale="localeFor(key)"
37
- :slug-source="field.from ? state[field.from] : void 0"
38
- />
39
- </CmsFormField>
66
+ @update:model-value="(locale) => setLocale(key, locale)"
67
+ />
68
+ </template>
69
+ <CmsFieldInput
70
+ v-model="state[key]"
71
+ :field="fields[key]"
72
+ :locale="localeFor(key)"
73
+ :slug-source="
74
+ fields[key].from ? state[fields[key].from] : void 0
75
+ "
76
+ />
77
+ </CmsFormField>
78
+ </div>
79
+ </div>
80
+ </component>
81
+ </div>
82
+ </div>
40
83
  <div v-if="footer" class="cms-form-actions">
41
84
  <CmsButton type="submit" label="Save" :loading="loading" />
42
85
  <CmsButton
@@ -55,14 +98,18 @@ import {
55
98
  fieldTab,
56
99
  hasTranslatableBlockFields,
57
100
  isFieldVisible,
58
- isTranslatableField
101
+ isTranslatableField,
102
+ resolveFormLayout
59
103
  } from "#nuxt-cms";
60
- import { computed, ref, watch } from "#imports";
104
+ import { computed, nextTick, ref, watch } from "#imports";
61
105
  import { buildEntrySchema } from "../../../shared/validation";
62
106
  import { useCmsRuntime } from "../../composables/cms-runtime";
107
+ import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
108
+ import { fieldIcon } from "../../utils/ui";
63
109
  const props = defineProps({
64
110
  fields: { type: Object, required: true },
65
111
  tabs: { type: Array, required: false },
112
+ entryName: { type: String, required: false },
66
113
  drafts: { type: Boolean, required: false },
67
114
  loading: { type: Boolean, required: false },
68
115
  formId: { type: String, required: false },
@@ -77,13 +124,39 @@ const activeTab = ref(tabList.value[0]?.id ?? "");
77
124
  watch(tabList, (tabs) => {
78
125
  if (!tabs.some((tab) => tab.id === activeTab.value)) activeTab.value = tabs[0]?.id ?? "";
79
126
  });
80
- const visibleFields = computed(
81
- () => Object.fromEntries(
82
- Object.entries(props.fields).filter(
83
- ([, field]) => isFieldVisible(field, state.value) && (!tabList.value.length || fieldTab(field, tabList.value) === activeTab.value)
84
- )
85
- )
127
+ const settings = useCmsSettingsState();
128
+ const layout = computed(
129
+ () => props.entryName ? entryFormLayout(props.entryName, settings.value, props.fields) : resolveFormLayout(props.fields, void 0)
130
+ );
131
+ function isShown(key) {
132
+ const field = props.fields[key];
133
+ if (!field || !isFieldVisible(field, state.value)) return false;
134
+ if (!tabList.value.length) return true;
135
+ return fieldTab(field, tabList.value) === activeTab.value;
136
+ }
137
+ const sections = computed(
138
+ () => layout.value.main.map((section, index) => ({
139
+ ...section,
140
+ id: `${index}-${section.title ?? ""}`,
141
+ rows: section.rows.map((row) => row.filter((key) => isShown(key))).filter((row) => row.length)
142
+ })).filter((section) => section.rows.length)
86
143
  );
144
+ const toggled = ref({});
145
+ function isCollapsed(section) {
146
+ if (!section.title) return false;
147
+ return toggled.value[section.id] ?? !!section.collapsed;
148
+ }
149
+ function toggleSection(id) {
150
+ const section = sections.value.find((s) => s.id === id);
151
+ if (section) toggled.value = { ...toggled.value, [id]: !isCollapsed(section) };
152
+ }
153
+ function revealField(key) {
154
+ for (const section of sections.value) {
155
+ if (section.rows.some((row) => row.includes(key)) && isCollapsed(section)) {
156
+ toggled.value = { ...toggled.value, [section.id]: false };
157
+ }
158
+ }
159
+ }
87
160
  function onError() {
88
161
  const issue = schema.value.safeParse(state.value).error?.issues[0];
89
162
  const key = issue ? String(issue.path[0] ?? "") : "";
@@ -92,6 +165,7 @@ function onError() {
92
165
  const tab = fieldTab(field, tabList.value);
93
166
  if (tab) activeTab.value = tab;
94
167
  }
168
+ if (key) void nextTick(() => revealField(key));
95
169
  emit("error");
96
170
  }
97
171
  function hasLocaleSwitch(field) {
@@ -2,6 +2,7 @@ import type { CmsEntry, CmsTab } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  fields: CmsEntry['fields'];
4
4
  tabs?: CmsTab[];
5
+ entryName?: string;
5
6
  drafts?: boolean;
6
7
  loading?: boolean;
7
8
  formId?: string;
@@ -12,16 +12,26 @@
12
12
  :locale="locale"
13
13
  />
14
14
  <CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
15
- <CmsTextarea v-else-if="field.type === 'text' && field.textarea" v-model="str" :rows="8" />
16
- <CmsInput v-else-if="field.type === 'text'" v-model="str" />
17
- <CmsInput v-else-if="field.type === 'email'" v-model="str" type="email" />
15
+ <CmsTextarea
16
+ v-else-if="field.type === 'text' && field.textarea"
17
+ v-model="str"
18
+ :rows="8"
19
+ :placeholder="field.placeholder"
20
+ />
21
+ <CmsInput v-else-if="field.type === 'text'" v-model="str" :placeholder="field.placeholder" />
22
+ <CmsInput
23
+ v-else-if="field.type === 'email'"
24
+ v-model="str"
25
+ type="email"
26
+ :placeholder="field.placeholder"
27
+ />
18
28
  <CmsSlugField v-else-if="field.type === 'slug'" v-model="strOrNull" :source="slugSource" />
19
29
  <CmsSelectMenu
20
30
  v-else-if="field.type === 'select'"
21
31
  v-model="selValue"
22
32
  :multiple="field.multiple"
23
33
  :items="selectItems"
24
- placeholder="Select…"
34
+ :placeholder="field.placeholder ?? 'Select\u2026'"
25
35
  />
26
36
  <CmsSwitch
27
37
  v-else-if="field.type === 'boolean'"
@@ -33,7 +43,12 @@
33
43
  "
34
44
  />
35
45
  <CmsJsonField v-else-if="field.type === 'json'" v-model="jsonValue" />
36
- <CmsInput v-else-if="field.type === 'number'" v-model.number="num" type="number" />
46
+ <CmsInput
47
+ v-else-if="field.type === 'number'"
48
+ v-model.number="num"
49
+ type="number"
50
+ :placeholder="field.placeholder"
51
+ />
37
52
  <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
38
53
  <CmsRelationField
39
54
  v-else-if="field.type === 'relation'"
@@ -1,13 +1,15 @@
1
1
  type __VLS_Props = {
2
2
  label?: string;
3
+ icon?: string;
4
+ description?: string;
3
5
  name?: string;
4
6
  required?: boolean;
5
7
  };
6
- declare var __VLS_1: {}, __VLS_3: {};
8
+ declare var __VLS_6: {}, __VLS_8: {};
7
9
  type __VLS_Slots = {} & {
8
- 'label-actions'?: (props: typeof __VLS_1) => any;
10
+ 'label-actions'?: (props: typeof __VLS_6) => any;
9
11
  } & {
10
- default?: (props: typeof __VLS_3) => any;
12
+ default?: (props: typeof __VLS_8) => any;
11
13
  };
12
14
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,13 +1,17 @@
1
1
  <template>
2
- <div class="cms-form-field">
2
+ <div class="cms-form-field" :class="{ 'is-error': error }">
3
3
  <div v-if="label || $slots['label-actions']" class="cms-form-label-row">
4
4
  <label v-if="label" class="cms-form-label">
5
+ <CmsIcon v-if="icon" :name="icon" class="cms-form-label-icon size-3.5" />
5
6
  {{ label }}<span v-if="required" class="cms-form-required"> *</span>
6
7
  </label>
7
8
  <slot name="label-actions" />
8
9
  </div>
9
10
  <slot />
10
- <p v-if="error" class="cms-form-error">{{ error }}</p>
11
+ <p v-if="description" class="cms-form-description">{{ description }}</p>
12
+ <p v-if="error" class="cms-form-error">
13
+ <CmsIcon name="exclamation-circle" class="size-3.5 shrink-0" />{{ error }}
14
+ </p>
11
15
  </div>
12
16
  </template>
13
17
 
@@ -16,6 +20,8 @@ import { computed, inject } from "#imports";
16
20
  import { CMS_FORM_ERRORS } from "../../utils/ui";
17
21
  const props = defineProps({
18
22
  label: { type: String, required: false },
23
+ icon: { type: String, required: false },
24
+ description: { type: String, required: false },
19
25
  name: { type: String, required: false },
20
26
  required: { type: Boolean, required: false }
21
27
  });
@@ -1,13 +1,15 @@
1
1
  type __VLS_Props = {
2
2
  label?: string;
3
+ icon?: string;
4
+ description?: string;
3
5
  name?: string;
4
6
  required?: boolean;
5
7
  };
6
- declare var __VLS_1: {}, __VLS_3: {};
8
+ declare var __VLS_6: {}, __VLS_8: {};
7
9
  type __VLS_Slots = {} & {
8
- 'label-actions'?: (props: typeof __VLS_1) => any;
10
+ 'label-actions'?: (props: typeof __VLS_6) => any;
9
11
  } & {
10
- default?: (props: typeof __VLS_3) => any;
12
+ default?: (props: typeof __VLS_8) => any;
11
13
  };
12
14
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <component :is="icon" v-if="icon" class="size-5 stroke-[1.5]" />
2
+ <component :is="icon" v-if="icon" :data-icon="name" class="size-5 stroke-[1.5]" />
3
3
  </template>
4
4
 
5
5
  <script setup>
@@ -2,7 +2,6 @@ type __VLS_Props = {
2
2
  type?: string;
3
3
  size?: 'sm' | 'md' | 'lg';
4
4
  icon?: string;
5
- color?: 'error';
6
5
  };
7
6
  type __VLS_ModelProps = {
8
7
  modelValue?: string | number | null;
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <div class="cms-input-wrap" :class="[$attrs.class, { 'has-lead': icon }]">
2
+ <div
3
+ class="cms-input-wrap"
4
+ :class="[$attrs.class, { 'has-lead': icon }]"
5
+ :style="$attrs.style"
6
+ >
3
7
  <span v-if="icon" class="cms-input-lead">
4
8
  <CmsIcon :name="icon" class="size-4" />
5
9
  </span>
@@ -7,7 +11,7 @@
7
11
  v-bind="inputAttrs"
8
12
  :type="type ?? 'text'"
9
13
  :value="model ?? ''"
10
- :class="['cms-field', `cms-field-${size ?? 'md'}`, { 'is-error': color === 'error' }]"
14
+ :class="['cms-field', `cms-field-${size ?? 'md'}`]"
11
15
  @input="onInput"
12
16
  />
13
17
  <span v-if="$slots.trailing" class="cms-input-trail">
@@ -22,8 +26,7 @@ defineOptions({ inheritAttrs: false });
22
26
  defineProps({
23
27
  type: { type: String, required: false },
24
28
  size: { type: String, required: false },
25
- icon: { type: String, required: false },
26
- color: { type: String, required: false }
29
+ icon: { type: String, required: false }
27
30
  });
28
31
  const [model, modifiers] = defineModel({ type: [String, Number, null] });
29
32
  const attrs = useAttrs();
@@ -2,7 +2,6 @@ type __VLS_Props = {
2
2
  type?: string;
3
3
  size?: 'sm' | 'md' | 'lg';
4
4
  icon?: string;
5
- color?: 'error';
6
5
  };
7
6
  type __VLS_ModelProps = {
8
7
  modelValue?: string | number | null;
@@ -6,7 +6,7 @@
6
6
  type="button"
7
7
  role="tab"
8
8
  :aria-selected="model === locale"
9
- :title="filled(locale) ? locale : `${locale} \u2014 empty`"
9
+ :title="filled(locale) ? locale : `${locale}: empty`"
10
10
  class="cms-locale-option"
11
11
  :class="{ 'is-active': model === locale, 'is-empty': !filled(locale) }"
12
12
  @click="model = locale"
@@ -1,7 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  folders: string[];
3
3
  exclude?: string | null;
4
- disabled?: (string | null)[];
5
4
  };
6
5
  type __VLS_ModelProps = {
7
6
  modelValue?: string | null;
@@ -50,8 +50,7 @@ import { computed, ref } from "#imports";
50
50
  import { isWithinMediaFolder, mediaFolderDepth, mediaFolderName } from "#nuxt-cms";
51
51
  const props = defineProps({
52
52
  folders: { type: Array, required: true },
53
- exclude: { type: [String, null], required: false },
54
- disabled: { type: Array, required: false }
53
+ exclude: { type: [String, null], required: false }
55
54
  });
56
55
  const model = defineModel({ type: [String, null], ...{ default: null } });
57
56
  const query = ref("");
@@ -60,7 +59,6 @@ const visible = computed(() => {
60
59
  return term ? props.folders.filter((folder) => folder.includes(term)) : props.folders;
61
60
  });
62
61
  function isDisabled(folder) {
63
- if (props.disabled?.includes(folder)) return true;
64
62
  return !!props.exclude && !!folder && isWithinMediaFolder(folder, props.exclude);
65
63
  }
66
64
  </script>
@@ -1,7 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  folders: string[];
3
3
  exclude?: string | null;
4
- disabled?: (string | null)[];
5
4
  };
6
5
  type __VLS_ModelProps = {
7
6
  modelValue?: string | null;
@@ -431,10 +431,11 @@
431
431
  :media-type="mediaType"
432
432
  :accept="accept"
433
433
  :folder="folder"
434
+ :directories="!selectable"
434
435
  @uploaded="onUploaded"
435
436
  />
436
437
  <p class="cms-form-hint cms-media-upload-hint">
437
- Tip: you can also drop files anywhere on the library, or on a folder.
438
+ Tip: you can also drop files or folders anywhere on the library, or on a folder.
438
439
  </p>
439
440
  </template>
440
441
  </CmsModal>
@@ -562,7 +563,11 @@ import {
562
563
  } from "#nuxt-cms";
563
564
  import { useCmsConfirm } from "../../composables/cms-confirm";
564
565
  import { useCmsMediaLibrary, usageSummary } from "../../composables/cms-media-library";
565
- import { useCmsMediaUploader } from "../../composables/cms-media-uploader";
566
+ import {
567
+ uploadTreeFromDrop,
568
+ uploadTreeFromFiles,
569
+ useCmsMediaUploader
570
+ } from "../../composables/cms-media-uploader";
566
571
  import { useCmsRuntime } from "../../composables/cms-runtime";
567
572
  import { useCmsToast } from "../../composables/cms-toast";
568
573
  const MEDIA_DRAG = "application/x-cms-media";
@@ -946,7 +951,10 @@ async function moveSelection() {
946
951
  }
947
952
  const uploadOpen = ref(false);
948
953
  async function afterUpload(uploaded) {
949
- if (!uploaded.length) return;
954
+ if (!uploaded.length) {
955
+ if (!props.selectable) await library.reload({ quiet: true });
956
+ return;
957
+ }
950
958
  if (props.selectable && uploaded[0]) {
951
959
  emitSelect(uploaded[0]);
952
960
  return;
@@ -961,13 +969,19 @@ function onUploaded(uploaded) {
961
969
  uploadOpen.value = false;
962
970
  void afterUpload(uploaded);
963
971
  }
964
- async function uploadFiles(files, target) {
965
- const uploaded = await uploader.upload(files, {
972
+ function droppedTree(event) {
973
+ const data = event.dataTransfer;
974
+ if (!data?.files.length) return null;
975
+ return props.selectable ? Promise.resolve(uploadTreeFromFiles(data.files)) : uploadTreeFromDrop(data);
976
+ }
977
+ async function uploadTree(pending, target) {
978
+ const tree = await pending;
979
+ const uploaded = await uploader.upload(tree, {
966
980
  folder: target,
967
981
  accept: acceptAttr.value,
968
982
  multiple: !props.selectable
969
983
  });
970
- await afterUpload(uploaded);
984
+ if (uploaded.length || tree.directories.length) await afterUpload(uploaded);
971
985
  }
972
986
  const draggingKeys = ref([]);
973
987
  const draggingFolder = ref(null);
@@ -1036,7 +1050,7 @@ async function onTargetDrop(event, target) {
1036
1050
  event.stopPropagation();
1037
1051
  const keys = [...draggingKeys.value];
1038
1052
  const path = draggingFolder.value;
1039
- const files = event.dataTransfer?.files;
1053
+ const tree = hasFiles(event) ? droppedTree(event) : null;
1040
1054
  onDragEnd();
1041
1055
  fileDragDepth.value = 0;
1042
1056
  if (keys.length) {
@@ -1051,7 +1065,7 @@ async function onTargetDrop(event, target) {
1051
1065
  }
1052
1066
  return;
1053
1067
  }
1054
- if (files?.length) await uploadFiles(files, target);
1068
+ if (tree) await uploadTree(tree, target);
1055
1069
  }
1056
1070
  function onGalleryDragEnter(event) {
1057
1071
  if (!hasFiles(event) || !canUpload.value) return;
@@ -1071,8 +1085,8 @@ async function onGalleryDrop(event) {
1071
1085
  event.preventDefault();
1072
1086
  fileDragDepth.value = 0;
1073
1087
  dropTarget.value = null;
1074
- const files = event.dataTransfer?.files;
1075
- if (files?.length) await uploadFiles(files, folder.value);
1088
+ const tree = droppedTree(event);
1089
+ if (tree) await uploadTree(tree, folder.value);
1076
1090
  }
1077
1091
  function tileInfo(item) {
1078
1092
  const parts = [extension(item.key)];
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  folder?: string | null;
7
+ directories?: boolean;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  uploaded: (items: MediaItem[]) => any;
@@ -1,72 +1,102 @@
1
1
  <template>
2
- <button
3
- type="button"
4
- class="cms-dropzone"
5
- :class="{ 'is-over': dragOver, 'is-busy': uploading }"
6
- :disabled="uploading"
7
- @click="input?.click()"
8
- @dragover.prevent="dragOver = true"
9
- @dragleave.prevent="dragOver = false"
10
- @drop.prevent.stop="onDrop"
11
- >
12
- <input
13
- ref="input"
14
- type="file"
15
- class="hidden"
16
- :accept="acceptAttr"
17
- :multiple="multiple"
18
- @change="onChange"
19
- @click.stop
20
- />
21
- <CmsIcon
22
- :name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
23
- class="size-5 shrink-0"
24
- :class="{ 'animate-spin': uploading }"
25
- />
26
- <span class="text-sm font-medium">
27
- {{
28
- uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
29
- }}
30
- </span>
31
- <span v-if="!uploading" class="cms-dropzone-hint">
32
- <CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
33
- {{ folder ?? "Library root" }}
34
- </span>
35
- </button>
2
+ <div class="flex flex-col items-center gap-2">
3
+ <button
4
+ type="button"
5
+ class="cms-dropzone"
6
+ :class="{ 'is-over': dragOver, 'is-busy': uploading }"
7
+ :disabled="uploading"
8
+ @click="input?.click()"
9
+ @dragover.prevent="dragOver = true"
10
+ @dragleave.prevent="dragOver = false"
11
+ @drop.prevent.stop="onDrop"
12
+ >
13
+ <input
14
+ ref="input"
15
+ type="file"
16
+ class="hidden"
17
+ :accept="acceptAttr"
18
+ :multiple="multiple"
19
+ @change="onChange"
20
+ @click.stop
21
+ />
22
+ <CmsIcon
23
+ :name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
24
+ class="size-5 shrink-0"
25
+ :class="{ 'animate-spin': uploading }"
26
+ />
27
+ <span class="text-sm font-medium">
28
+ {{
29
+ uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
30
+ }}
31
+ </span>
32
+ <span v-if="!uploading" class="cms-dropzone-hint">
33
+ <CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
34
+ {{ folder ?? "Library root" }}
35
+ </span>
36
+ </button>
37
+ <template v-if="directories && multiple && !uploading">
38
+ <input
39
+ ref="directoryInput"
40
+ type="file"
41
+ class="hidden"
42
+ webkitdirectory
43
+ multiple
44
+ @change="onChange"
45
+ />
46
+ <CmsButton
47
+ label="Upload a folder"
48
+ icon="folder-arrow-down"
49
+ variant="ghost"
50
+ color="neutral"
51
+ size="sm"
52
+ @click="directoryInput?.click()"
53
+ />
54
+ </template>
55
+ </div>
36
56
  </template>
37
57
 
38
58
  <script setup>
39
59
  import { mediaTypeAccept } from "#nuxt-cms";
40
60
  import { computed, ref } from "#imports";
41
- import { useCmsMediaUploader } from "../../composables/cms-media-uploader";
61
+ import {
62
+ uploadTreeFromDrop,
63
+ uploadTreeFromFiles,
64
+ useCmsMediaUploader
65
+ } from "../../composables/cms-media-uploader";
42
66
  const props = defineProps({
43
67
  multiple: { type: Boolean, required: false },
44
68
  mediaType: { type: null, required: false, default: "file" },
45
69
  accept: { type: Array, required: false },
46
- folder: { type: [String, null], required: false, default: null }
70
+ folder: { type: [String, null], required: false, default: null },
71
+ directories: { type: Boolean, required: false }
47
72
  });
48
73
  const emit = defineEmits(["uploaded"]);
49
74
  const { upload, uploading, done, total } = useCmsMediaUploader();
50
75
  const input = ref(null);
76
+ const directoryInput = ref(null);
51
77
  const dragOver = ref(false);
52
78
  const acceptAttr = computed(
53
79
  () => (props.accept?.length ? props.accept : mediaTypeAccept(props.mediaType))?.join(",")
54
80
  );
55
- async function handleFiles(list) {
56
- const items = await upload(list, {
81
+ async function handleTree(pending) {
82
+ const tree = await pending;
83
+ const items = await upload(tree, {
57
84
  folder: props.folder,
58
85
  accept: acceptAttr.value,
59
86
  multiple: props.multiple
60
87
  });
61
- if (items.length) emit("uploaded", items);
88
+ if (items.length || props.directories && tree.directories.length) emit("uploaded", items);
62
89
  }
63
90
  function onChange(event) {
64
91
  const el = event.target;
65
- if (el.files?.length) void handleFiles(el.files);
92
+ if (el.files?.length) void handleTree(uploadTreeFromFiles(el.files));
66
93
  el.value = "";
67
94
  }
68
95
  function onDrop(event) {
69
96
  dragOver.value = false;
70
- if (event.dataTransfer?.files.length) void handleFiles(event.dataTransfer.files);
97
+ if (!event.dataTransfer?.files.length) return;
98
+ void handleTree(
99
+ props.directories ? uploadTreeFromDrop(event.dataTransfer) : uploadTreeFromFiles(event.dataTransfer.files)
100
+ );
71
101
  }
72
102
  </script>
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  folder?: string | null;
7
+ directories?: boolean;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  uploaded: (items: MediaItem[]) => any;