nuxt-media-library 0.0.56 → 0.0.58

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 (45) hide show
  1. package/README.md +19 -8
  2. package/dist/module.d.mts +13 -3
  3. package/dist/module.d.ts +13 -3
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +11 -19
  6. package/dist/runtime/components/MLMedia/Field.d.vue.ts +6 -3
  7. package/dist/runtime/components/MLMedia/Field.vue +4 -0
  8. package/dist/runtime/components/MLMedia/Field.vue.d.ts +6 -3
  9. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -0
  10. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +11 -4
  11. package/dist/runtime/components/MLMedia/Gallery.vue +109 -4
  12. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +11 -4
  13. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +10 -1
  14. package/dist/runtime/components/MLMedia/GallerySection.vue +13 -2
  15. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +10 -1
  16. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
  17. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
  18. package/dist/runtime/components/MLMedia/UploadProgress.d.vue.ts +17 -0
  19. package/dist/runtime/components/MLMedia/UploadProgress.vue +33 -0
  20. package/dist/runtime/components/MLMedia/UploadProgress.vue.d.ts +17 -0
  21. package/dist/runtime/components/MLMedia/UploadRow.d.vue.ts +12 -0
  22. package/dist/runtime/components/MLMedia/UploadRow.vue +252 -0
  23. package/dist/runtime/components/MLMedia/UploadRow.vue.d.ts +12 -0
  24. package/dist/runtime/components/MLMedia/UploadStep.d.vue.ts +17 -0
  25. package/dist/runtime/components/MLMedia/UploadStep.vue +168 -0
  26. package/dist/runtime/components/MLMedia/UploadStep.vue.d.ts +17 -0
  27. package/dist/runtime/components/MLMedia/UploadSteps.d.vue.ts +14 -0
  28. package/dist/runtime/components/MLMedia/UploadSteps.vue +129 -0
  29. package/dist/runtime/components/MLMedia/UploadSteps.vue.d.ts +14 -0
  30. package/dist/runtime/components/MLMedia/UploadTray.d.vue.ts +12 -0
  31. package/dist/runtime/components/MLMedia/UploadTray.vue +222 -0
  32. package/dist/runtime/components/MLMedia/UploadTray.vue.d.ts +12 -0
  33. package/dist/runtime/composables/useMLMediaField.js +5 -3
  34. package/dist/runtime/composables/useMLMediaGallery.js +4 -1
  35. package/dist/runtime/composables/useMLMediaUpload.d.ts +119 -7
  36. package/dist/runtime/composables/useMLMediaUpload.js +27 -169
  37. package/dist/runtime/composables/useMLUploadQueue.d.ts +33 -0
  38. package/dist/runtime/composables/useMLUploadQueue.js +403 -0
  39. package/dist/runtime/index.d.ts +2 -0
  40. package/dist/runtime/index.js +2 -0
  41. package/dist/runtime/utils/backends.d.ts +63 -0
  42. package/dist/runtime/utils/backends.js +72 -0
  43. package/dist/runtime/utils/processingSteps.d.ts +5 -0
  44. package/dist/runtime/utils/processingSteps.js +36 -0
  45. package/package.json +4 -1
package/README.md CHANGED
@@ -237,9 +237,11 @@ merges the layers, so every widget on the site can use any backend by name.
237
237
  export default defineNuxtConfig({
238
238
  modules: ['nuxt-media-library'],
239
239
  mediaLibrary: {
240
- serviceUploads: {
241
- backends: { videos: { start: '/api/videos/uploads' } },
242
- // refresh defaults to `${start}/:sessionId/grant`
240
+ backends: {
241
+ videos: {
242
+ upload: { start: '/api/videos/uploads' }, // refresh defaults to `${start}/:sessionId/grant`
243
+ claim: '/api/videos/uploads/:sessionId/attach', // py_client on_attach
244
+ },
243
245
  },
244
246
  },
245
247
  })
@@ -257,17 +259,26 @@ export default defineNuxtConfig({
257
259
  ```vue
258
260
  <MLMediaGallery
259
261
  v-model="lesson.mediaIds"
260
- service="videos"
262
+ backend="videos"
261
263
  profile="lesson-video"
262
264
  :service-body="{ context: { lessonId } }"
263
265
  @uploaded="(assetId, role, sessionId) => attach(assetId, sessionId)"
264
266
  />
265
267
  ```
266
268
 
267
- - `service` on `MLMediaGallery`, `MLMediaField` and the file picker (and
268
- `useMLMediaUpload`): a backend name, or `{ start }` for a one-off. Without
269
- it, widgets upload through Nuxt's own route as before; an unknown name
270
- fails that upload with a message naming the missing backend.
269
+ - `backend` on `MLMediaGallery`, `MLMediaField` and the file picker (and
270
+ `useMLMediaUpload`): a name in `mediaLibrary.backends`. `service` is its
271
+ older name and still takes `{ start }` for a one-off; `backend` wins.
272
+ Without either, widgets upload through Nuxt's own route as before; an
273
+ unknown name fails that upload with a message naming the missing backend.
274
+ - Each backend has a `kind`: `media-library` (the default) or `media-kit`.
275
+ This version uploads only to `media-library` backends; a `media-kit` one
276
+ fails the upload with a clear message.
277
+ - With `claim`, the upload queue attaches files whose page was left before
278
+ the widget attached them (`POST { assetId, profile }`; 409 counts as
279
+ attached). The tray shows a refusal with "Add again".
280
+ - `mediaLibrary.serviceUploads.backends` still works: each entry becomes a
281
+ `media-library` backend with that `upload` and no `claim`.
271
282
  - The start request carries the file (`mimeType`, `sizeBytes`, `filename`),
272
283
  `profile` (each role's, in a gallery with roles) and `service-body`. The
273
284
  service's profile decides kind, visibility and processing, so
package/dist/module.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
3
+ import { MediaBackends } from '../dist/runtime/utils/backends.js';
3
4
  export * from '../dist/runtime/types/resourcePolicy.js';
4
5
  export * from '../dist/runtime/types/uploader.js';
5
6
  export * from '../dist/runtime/types/preview.js';
@@ -43,9 +44,8 @@ interface ModuleOptions {
43
44
  * public API root with `/v1`, or a proxy route); with `proxy` it defaults
44
45
  * to the pass-through route.
45
46
  *
46
- * `backends` names the services that start uploads, so widgets can use
47
- * `service="videos"`. Each service's Nuxt layer registers its own; Nuxt
48
- * merges them across layers. Paths only: each layer serves its routes.
47
+ * `backends`: older form of `mediaLibrary.backends` (start/refresh only).
48
+ * Each entry becomes a `media-library` backend with that `upload`.
49
49
  */
50
50
  serviceUploads?: {
51
51
  proxy?: boolean;
@@ -62,6 +62,16 @@ interface ModuleOptions {
62
62
  * pass-through are still registered.
63
63
  */
64
64
  manageProxy?: boolean;
65
+ /**
66
+ * The backend registry: named places widgets upload to and read from
67
+ * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
68
+ * (`media-library`, the default, or `media-kit`) and its routes: `upload`
69
+ * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
70
+ * (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
71
+ * layer registers its own; Nuxt merges them. Paths only: each layer serves
72
+ * its routes.
73
+ */
74
+ backends?: MediaBackends;
65
75
  }
66
76
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
67
77
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
3
+ import { MediaBackends } from '../dist/runtime/utils/backends.js';
3
4
  export * from '../dist/runtime/types/resourcePolicy.js';
4
5
  export * from '../dist/runtime/types/uploader.js';
5
6
  export * from '../dist/runtime/types/preview.js';
@@ -43,9 +44,8 @@ interface ModuleOptions {
43
44
  * public API root with `/v1`, or a proxy route); with `proxy` it defaults
44
45
  * to the pass-through route.
45
46
  *
46
- * `backends` names the services that start uploads, so widgets can use
47
- * `service="videos"`. Each service's Nuxt layer registers its own; Nuxt
48
- * merges them across layers. Paths only: each layer serves its routes.
47
+ * `backends`: older form of `mediaLibrary.backends` (start/refresh only).
48
+ * Each entry becomes a `media-library` backend with that `upload`.
49
49
  */
50
50
  serviceUploads?: {
51
51
  proxy?: boolean;
@@ -62,6 +62,16 @@ interface ModuleOptions {
62
62
  * pass-through are still registered.
63
63
  */
64
64
  manageProxy?: boolean;
65
+ /**
66
+ * The backend registry: named places widgets upload to and read from
67
+ * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
68
+ * (`media-library`, the default, or `media-kit`) and its routes: `upload`
69
+ * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
70
+ * (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
71
+ * layer registers its own; Nuxt merges them. Paths only: each layer serves
72
+ * its routes.
73
+ */
74
+ backends?: MediaBackends;
65
75
  }
66
76
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
67
77
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.56",
4
+ "version": "0.0.58",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
4
+ import { normalizeBackends } from '../dist/runtime/utils/backends.js';
4
5
  export * from '../dist/runtime/types/resourcePolicy.js';
5
6
  export * from '../dist/runtime/types/uploader.js';
6
7
  export * from '../dist/runtime/types/preview.js';
@@ -22,23 +23,6 @@ function browserBaseURL(existingPublic, publicBaseURL) {
22
23
  }
23
24
 
24
25
  const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
25
- function serviceBackends(backends, logger) {
26
- const out = {};
27
- for (const [name, backend] of Object.entries(backends ?? {})) {
28
- if (!backend || typeof backend.start !== "string" || !backend.start) {
29
- logger.warn(`mediaLibrary.serviceUploads.backends.${name} has no start path; ignored.`);
30
- continue;
31
- }
32
- if (backend.refresh && !backend.refresh.includes(":sessionId")) {
33
- logger.warn(`mediaLibrary.serviceUploads.backends.${name}.refresh needs a :sessionId placeholder; using the default.`);
34
- }
35
- out[name] = {
36
- start: backend.start,
37
- ...backend.refresh?.includes(":sessionId") ? { refresh: backend.refresh } : {}
38
- };
39
- }
40
- return out;
41
- }
42
26
  function appCatchAlls(layers) {
43
27
  const found = [];
44
28
  for (const { config } of layers) {
@@ -171,6 +155,10 @@ const module$1 = defineNuxtModule({
171
155
  if (existingPublic.proxyUploads === false && !existingPublic.baseURL && !options.publicBaseURL) {
172
156
  logger.warn("proxyUploads is false but no public media library address is set: set mediaLibrary.publicBaseURL. baseURL is server-side only.");
173
157
  }
158
+ const backends = {
159
+ ...normalizeBackends(options.backends, options.serviceUploads?.backends, (m) => logger.warn(m)),
160
+ ...normalizeBackends(existingPublic.backends, existingPublic.serviceUploadBackends, (m) => logger.warn(m))
161
+ };
174
162
  nuxt.options.runtimeConfig.public.mediaLibrary = {
175
163
  ...existingPublic,
176
164
  baseURL: browserBaseURL(existingPublic.baseURL, options.publicBaseURL),
@@ -186,8 +174,12 @@ const module$1 = defineNuxtModule({
186
174
  headers: publicHeaders.headers,
187
175
  // Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
188
176
  serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : ""),
189
- // Names widgets can pass as `service` (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BACKENDS).
190
- serviceUploadBackends: { ...serviceBackends(options.serviceUploads?.backends, logger), ...existingPublic.serviceUploadBackends }
177
+ // The backend registry (NUXT_PUBLIC_MEDIA_LIBRARY_BACKENDS): names widgets pass as `backend`.
178
+ backends,
179
+ // Older form, kept for code that reads it: each backend's upload routes.
180
+ serviceUploadBackends: Object.fromEntries(
181
+ Object.entries(backends).flatMap(([name, b]) => b.upload ? [[name, b.upload]] : [])
182
+ )
191
183
  };
192
184
  addPlugin({
193
185
  src: resolver.resolve("./runtime/plugins/config")
@@ -18,10 +18,12 @@ type __VLS_Props = {
18
18
  /** Upload profile name for new files. */
19
19
  profile?: string | null;
20
20
  /**
21
- * Start uploads at a backend service instead of Nuxt's own route: a name
22
- * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
23
- * or `{ start }`. See useMLMediaUpload.
21
+ * The backend registered under `mediaLibrary.backends` that uploads go to
22
+ * (e.g. "videos"). Its `claim` route attaches files whose page was left
23
+ * before they finished. Wins over `service`.
24
24
  */
25
+ backend?: string | null;
26
+ /** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
25
27
  service?: MediaUploadOptions['service'];
26
28
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
27
29
  serviceBody?: Record<string, unknown> | null;
@@ -65,6 +67,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
65
67
  };
66
68
  collectionName: string | null;
67
69
  profile: string | null;
70
+ backend: string | null;
68
71
  service: string | import("../../index.js").MediaServiceUpload | null;
69
72
  serviceBody: Record<string, unknown> | null;
70
73
  hint: string;
@@ -16,6 +16,7 @@ const props = defineProps({
16
16
  processingConfig: { type: Object, required: false, default: void 0 },
17
17
  collectionName: { type: [String, null], required: false, default: null },
18
18
  profile: { type: [String, null], required: false, default: null },
19
+ backend: { type: [String, null], required: false, default: null },
19
20
  service: { type: null, required: false, default: void 0 },
20
21
  serviceBody: { type: [Object, null], required: false, default: null },
21
22
  picker: { type: Boolean, required: false, default: true },
@@ -34,8 +35,10 @@ const field = useMLMediaField(model, {
34
35
  processingConfig: props.processingConfig,
35
36
  collectionName: props.collectionName,
36
37
  profile: props.profile,
38
+ backend: props.backend,
37
39
  service: props.service,
38
40
  serviceBody: props.serviceBody,
41
+ label: props.label || void 0,
39
42
  onUploaded: (id, sessionId) => emit("uploaded", id, sessionId)
40
43
  });
41
44
  const { state, asset, upload, message } = field;
@@ -76,6 +79,7 @@ const uploadOptions = computed(() => ({
76
79
  processingConfig: props.processingConfig,
77
80
  collectionName: props.collectionName,
78
81
  profile: props.profile,
82
+ backend: props.backend,
79
83
  service: props.service,
80
84
  serviceBody: props.serviceBody
81
85
  }));
@@ -18,10 +18,12 @@ type __VLS_Props = {
18
18
  /** Upload profile name for new files. */
19
19
  profile?: string | null;
20
20
  /**
21
- * Start uploads at a backend service instead of Nuxt's own route: a name
22
- * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
23
- * or `{ start }`. See useMLMediaUpload.
21
+ * The backend registered under `mediaLibrary.backends` that uploads go to
22
+ * (e.g. "videos"). Its `claim` route attaches files whose page was left
23
+ * before they finished. Wins over `service`.
24
24
  */
25
+ backend?: string | null;
26
+ /** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
25
27
  service?: MediaUploadOptions['service'];
26
28
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
27
29
  serviceBody?: Record<string, unknown> | null;
@@ -65,6 +67,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
65
67
  };
66
68
  collectionName: string | null;
67
69
  profile: string | null;
70
+ backend: string | null;
68
71
  service: string | import("../../index.js").MediaServiceUpload | null;
69
72
  serviceBody: Record<string, unknown> | null;
70
73
  hint: string;
@@ -72,6 +72,7 @@ function onFiles(files) {
72
72
  const u = uploader.start(file);
73
73
  void u.created.then((id) => {
74
74
  if (!id) return;
75
+ u.attached = true;
75
76
  props.onUploaded?.(id, u.sessionId);
76
77
  if (!isChosen(id)) choose(id);
77
78
  });
@@ -15,7 +15,11 @@ export type MediaGalleryProfiles = Record<string, MediaGalleryRole>;
15
15
  type __VLS_Props = {
16
16
  /** Roles of this usage. Omit for a single list driven by v-model. */
17
17
  profiles?: MediaGalleryProfiles | null;
18
- /** Role new uploads and picked files go to. Defaults to the first role that holds more than one file. */
18
+ /**
19
+ * Role tried first for new uploads and picked files, when it takes their
20
+ * kind. Defaults to the first role that holds more than one file. Files of
21
+ * other kinds go to the first role that takes them.
22
+ */
19
23
  defaultRole?: string;
20
24
  accept?: readonly string[] | string;
21
25
  max?: number | null;
@@ -31,10 +35,12 @@ type __VLS_Props = {
31
35
  /** Upload profile for the single list (with `profiles`, each role has its own). */
32
36
  profile?: string | null;
33
37
  /**
34
- * Start uploads at a backend service instead of Nuxt's own route: a name
35
- * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
36
- * or `{ start }`. Each role's profile is sent with the start request.
38
+ * The backend registered under `mediaLibrary.backends` that uploads go to
39
+ * (e.g. "videos"). Its `claim` route attaches files whose page was left
40
+ * before they finished. Each role's profile is sent with the start request. Wins over `service`.
37
41
  */
42
+ backend?: string | null;
43
+ /** Older name for `backend`; also takes an inline `{ start }`. */
38
44
  service?: MediaUploadOptions['service'];
39
45
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
40
46
  serviceBody?: Record<string, unknown> | null;
@@ -72,6 +78,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
72
78
  };
73
79
  collectionName: string | null;
74
80
  profile: string | null;
81
+ backend: string | null;
75
82
  service: string | import("../../index.js").MediaServiceUpload | null;
76
83
  serviceBody: Record<string, unknown> | null;
77
84
  hint: string;
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
+ import { $fetch } from "ofetch";
4
+ import { kindForFile } from "../../utils/fileAccept";
3
5
  import { GALLERY_TILE_TYPE } from "../../utils/galleryDrag";
4
6
  import GallerySection from "./GallerySection.vue";
5
7
  const props = defineProps({
@@ -17,6 +19,7 @@ const props = defineProps({
17
19
  processingConfig: { type: Object, required: false, default: void 0 },
18
20
  collectionName: { type: [String, null], required: false, default: null },
19
21
  profile: { type: [String, null], required: false, default: null },
22
+ backend: { type: [String, null], required: false, default: null },
20
23
  service: { type: null, required: false, default: void 0 },
21
24
  serviceBody: { type: [Object, null], required: false, default: null },
22
25
  picker: { type: Boolean, required: false, default: true },
@@ -78,16 +81,108 @@ function setRole(id, key, on) {
78
81
  }
79
82
  const pendingRole = /* @__PURE__ */ new Map();
80
83
  const uploadedRole = /* @__PURE__ */ new Map();
84
+ const roleAccept = (r) => r.accept ?? props.accept;
85
+ const anyAccept = computed(() => {
86
+ const kinds = /* @__PURE__ */ new Set();
87
+ for (const r of roles.value) {
88
+ const a = roleAccept(r);
89
+ const list = (typeof a === "string" ? a.split(",") : [...a ?? []]).map((k) => k.trim().toLowerCase()).filter(Boolean);
90
+ if (!list.length) return "";
91
+ list.forEach((k) => kinds.add(k));
92
+ }
93
+ return [...kinds];
94
+ });
95
+ const anyMaxSize = computed(() => {
96
+ let max = 0;
97
+ for (const r of roles.value) {
98
+ const m = r.maxSize ?? props.maxSize;
99
+ if (m == null) return null;
100
+ max = Math.max(max, m);
101
+ }
102
+ return max || null;
103
+ });
104
+ const kindMatters = computed(() => {
105
+ const seen = new Set(roles.value.map((r) => JSON.stringify(roleAccept(r) ?? "")));
106
+ return seen.size > 1;
107
+ });
108
+ function roleForKind(kind, taken = /* @__PURE__ */ new Set()) {
109
+ const ordered = [...roles.value.filter((r) => r.key === defaultKey.value), ...roles.value.filter((r) => r.key !== defaultKey.value)];
110
+ const takes = ordered.filter((r) => kindAllowed(kind, roleAccept(r)));
111
+ const room = takes.find((r) => single(r) ? !taken.has(r.key) && !sel(r.key).length : r.max == null || sel(r.key).length < r.max);
112
+ return (room ?? takes[0])?.key ?? null;
113
+ }
114
+ const profileFor = (file) => {
115
+ const key = roleForKind(kindForFile(file));
116
+ return key ? props.profiles?.[key]?.profile ?? null : null;
117
+ };
118
+ function routeFiles(files) {
119
+ notice.value = null;
120
+ const taken = /* @__PURE__ */ new Set();
121
+ const groups = /* @__PURE__ */ new Map();
122
+ const left = [];
123
+ for (const file of files) {
124
+ const key = roleForKind(kindForFile(file), taken);
125
+ if (!key) {
126
+ left.push(file);
127
+ continue;
128
+ }
129
+ taken.add(key);
130
+ groups.set(key, [...groups.get(key) ?? [], file]);
131
+ }
132
+ for (const [key, list] of groups) {
133
+ const role = props.profiles[key];
134
+ grid.value?.uploadFiles(list, {
135
+ profile: role.profile ?? null,
136
+ tag: key,
137
+ accept: roleAccept(role),
138
+ maxSize: role.maxSize ?? props.maxSize,
139
+ maxFiles: single(role) ? 1 : role.max != null ? Math.max(0, role.max - sel(key).length) : void 0
140
+ });
141
+ }
142
+ if (left.length) notice.value = `${left[0].name}${left.length > 1 ? ` and ${left.length - 1} more` : ""} can't be used in any role here.`;
143
+ }
144
+ async function placeByKind(ids) {
145
+ let kinds = /* @__PURE__ */ new Map();
146
+ try {
147
+ const res = await $fetch(
148
+ "/api/media-library/manage/assets/resolve",
149
+ { method: "POST", body: { assetIds: ids } }
150
+ );
151
+ kinds = new Map(res.items.map((it, i) => [it.asset?.id ?? it.assetId ?? ids[i], it.asset?.kind?.toLowerCase() ?? null]));
152
+ } catch {
153
+ }
154
+ const taken = /* @__PURE__ */ new Set();
155
+ const changes = {};
156
+ for (const id of ids) {
157
+ const key = roleForKind(kinds.get(id) ?? null, taken) ?? defaultKey.value;
158
+ taken.add(key);
159
+ const placed = withFile(key, id, changes[key] ?? sel(key));
160
+ if (placed) changes[key] = placed;
161
+ const session = pendingEmit.get(id);
162
+ if (session !== void 0) {
163
+ pendingEmit.delete(id);
164
+ emit("uploaded", id, key, session);
165
+ }
166
+ }
167
+ if (Object.keys(changes).length) commit(changes);
168
+ }
169
+ const pendingEmit = /* @__PURE__ */ new Map();
81
170
  function onUnionUpdate(next) {
82
171
  notice.value = null;
83
172
  const keep = new Set(next);
84
173
  const before = new Set(union.value);
85
174
  const changes = {};
86
175
  for (const r of roles.value) changes[r.key] = sel(r.key).filter((id) => keep.has(id));
176
+ const unplaced = [];
87
177
  for (const id of next) {
88
178
  if (before.has(id)) continue;
89
- const key = pendingRole.get(id) ?? defaultKey.value;
179
+ const tagged = pendingRole.get(id);
90
180
  pendingRole.delete(id);
181
+ if (!tagged && kindMatters.value) {
182
+ unplaced.push(id);
183
+ continue;
184
+ }
185
+ const key = tagged ?? defaultKey.value;
91
186
  const placed = withFile(key, id, changes[key]);
92
187
  if (placed) changes[key] = placed;
93
188
  }
@@ -96,6 +191,7 @@ function onUnionUpdate(next) {
96
191
  if (!single(r)) changes[r.key] = [...changes[r.key]].sort((a, b) => (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
97
192
  }
98
193
  commit(changes);
194
+ if (unplaced.length) void placeByKind(unplaced);
99
195
  }
100
196
  function onAdd(id, tag) {
101
197
  const key = tag ?? defaultKey.value;
@@ -104,8 +200,13 @@ function onAdd(id, tag) {
104
200
  else pendingRole.set(id, key);
105
201
  }
106
202
  function onUploaded(id, sessionId) {
107
- emit("uploaded", id, uploadedRole.get(id) ?? defaultKey.value, sessionId);
203
+ const key = uploadedRole.get(id);
108
204
  uploadedRole.delete(id);
205
+ if (!key && kindMatters.value && !union.value.includes(id)) {
206
+ pendingEmit.set(id, sessionId);
207
+ return;
208
+ }
209
+ emit("uploaded", id, key ?? defaultKey.value, sessionId);
109
210
  }
110
211
  const badges = (id) => roles.value.filter((r) => sel(r.key).includes(id)).map((r) => r.label);
111
212
  function tileMenu(id, kind) {
@@ -170,9 +271,12 @@ function chipCount(key, max) {
170
271
  :model-value="union"
171
272
  :title="title"
172
273
  :hint="hint"
173
- :accept="defaultRole?.accept ?? accept"
174
- :max-size="defaultRole?.maxSize ?? maxSize"
274
+ :accept="anyAccept"
275
+ :max-size="anyMaxSize"
175
276
  :profile="defaultRole?.profile ?? null"
277
+ :profile-for="profileFor"
278
+ :on-route-files="routeFiles"
279
+ :backend="backend"
176
280
  :service="service"
177
281
  :service-body="serviceBody"
178
282
  :disabled="disabled"
@@ -251,6 +355,7 @@ function chipCount(key, max) {
251
355
  :processing-config="processingConfig"
252
356
  :collection-name="collectionName"
253
357
  :profile="profile"
358
+ :backend="backend"
254
359
  :service="service"
255
360
  :service-body="serviceBody"
256
361
  :picker="picker"
@@ -15,7 +15,11 @@ export type MediaGalleryProfiles = Record<string, MediaGalleryRole>;
15
15
  type __VLS_Props = {
16
16
  /** Roles of this usage. Omit for a single list driven by v-model. */
17
17
  profiles?: MediaGalleryProfiles | null;
18
- /** Role new uploads and picked files go to. Defaults to the first role that holds more than one file. */
18
+ /**
19
+ * Role tried first for new uploads and picked files, when it takes their
20
+ * kind. Defaults to the first role that holds more than one file. Files of
21
+ * other kinds go to the first role that takes them.
22
+ */
19
23
  defaultRole?: string;
20
24
  accept?: readonly string[] | string;
21
25
  max?: number | null;
@@ -31,10 +35,12 @@ type __VLS_Props = {
31
35
  /** Upload profile for the single list (with `profiles`, each role has its own). */
32
36
  profile?: string | null;
33
37
  /**
34
- * Start uploads at a backend service instead of Nuxt's own route: a name
35
- * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
36
- * or `{ start }`. Each role's profile is sent with the start request.
38
+ * The backend registered under `mediaLibrary.backends` that uploads go to
39
+ * (e.g. "videos"). Its `claim` route attaches files whose page was left
40
+ * before they finished. Each role's profile is sent with the start request. Wins over `service`.
37
41
  */
42
+ backend?: string | null;
43
+ /** Older name for `backend`; also takes an inline `{ start }`. */
38
44
  service?: MediaUploadOptions['service'];
39
45
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
40
46
  serviceBody?: Record<string, unknown> | null;
@@ -72,6 +78,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
72
78
  };
73
79
  collectionName: string | null;
74
80
  profile: string | null;
81
+ backend: string | null;
75
82
  service: string | import("../../index.js").MediaServiceUpload | null;
76
83
  serviceBody: Record<string, unknown> | null;
77
84
  hint: string;
@@ -26,7 +26,9 @@ type __VLS_Props = {
26
26
  onOpen?: (assetId: string) => void;
27
27
  /** Upload profile name for new files in this list. */
28
28
  profile?: string | null;
29
- /** Start uploads at a backend service (name or `{ start }`). See useMLMediaUpload. */
29
+ /** Registered backend uploads go to (`mediaLibrary.backends`). Wins over `service`. */
30
+ backend?: string | null;
31
+ /** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
30
32
  service?: MediaUploadOptions['service'];
31
33
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
32
34
  serviceBody?: Record<string, unknown> | null;
@@ -40,6 +42,10 @@ type __VLS_Props = {
40
42
  highlight?: ReadonlySet<string> | null;
41
43
  /** See useMLMediaGallery's onAdd. */
42
44
  onAdd?: (assetId: string, tag: string | undefined) => void;
45
+ /** Take new files instead of uploading them here (a gallery with roles routes them by kind). */
46
+ onRouteFiles?: (files: File[]) => void;
47
+ /** Upload profile per file, for uploads inside the picker. */
48
+ profileFor?: (file: File) => string | null | undefined;
43
49
  };
44
50
  /** Upload files with another profile than the list's own, e.g. from a role chip. */
45
51
  declare function uploadFiles(files: File[], opts?: {
@@ -85,8 +91,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
85
91
  };
86
92
  collectionName: string | null;
87
93
  profile: string | null;
94
+ backend: string | null;
88
95
  service: string | import("../../index.js").MediaServiceUpload | null;
89
96
  serviceBody: Record<string, unknown> | null;
97
+ profileFor: (file: File) => string | null | undefined;
90
98
  onAdd: (assetId: string, tag: string | undefined) => void;
91
99
  hint: string;
92
100
  picker: boolean;
@@ -97,6 +105,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
97
105
  recordName: string;
98
106
  badges: (assetId: string) => string[];
99
107
  tileMenu: (assetId: string, kind: string | null) => DropdownMenuItem[][];
108
+ onRouteFiles: (files: File[]) => void;
100
109
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
101
110
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
102
111
  declare const _default: typeof __VLS_export;
@@ -24,13 +24,16 @@ const props = defineProps({
24
24
  editor: { type: Boolean, required: false, default: true },
25
25
  onOpen: { type: Function, required: false, default: void 0 },
26
26
  profile: { type: [String, null], required: false, default: null },
27
+ backend: { type: [String, null], required: false, default: null },
27
28
  service: { type: null, required: false, default: void 0 },
28
29
  serviceBody: { type: [Object, null], required: false, default: null },
29
30
  removeLabel: { type: String, required: false, default: void 0 },
30
31
  badges: { type: Function, required: false, default: void 0 },
31
32
  tileMenu: { type: Function, required: false, default: void 0 },
32
33
  highlight: { type: [Set, null], required: false, default: null },
33
- onAdd: { type: Function, required: false, default: void 0 }
34
+ onAdd: { type: Function, required: false, default: void 0 },
35
+ onRouteFiles: { type: Function, required: false, default: void 0 },
36
+ profileFor: { type: Function, required: false, default: void 0 }
34
37
  });
35
38
  const model = defineModel({ type: Array, ...{ default: () => [] } });
36
39
  const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
@@ -43,8 +46,10 @@ const gallery = useMLMediaGallery(model, {
43
46
  processingConfig: props.processingConfig,
44
47
  collectionName: props.collectionName,
45
48
  profile: props.profile,
49
+ backend: props.backend,
46
50
  service: props.service,
47
51
  serviceBody: props.serviceBody,
52
+ label: props.recordName ? `${props.title} of ${props.recordName}` : props.title,
48
53
  onUploaded: (id, sessionId) => emit("uploaded", id, sessionId),
49
54
  onAdd: (id, tag) => props.onAdd?.(id, tag)
50
55
  });
@@ -126,6 +131,8 @@ async function pickExisting() {
126
131
  processingConfig: props.processingConfig,
127
132
  collectionName: props.collectionName,
128
133
  profile: props.profile,
134
+ profileFor: props.profileFor,
135
+ backend: props.backend,
129
136
  service: props.service,
130
137
  serviceBody: props.serviceBody
131
138
  },
@@ -199,6 +206,10 @@ function uploadFiles(files, opts = {}) {
199
206
  if (rejected.length) gallery.onRejected(rejected);
200
207
  if (accepted.length) gallery.onFiles(accepted, { profile: opts.profile, tag: opts.tag });
201
208
  }
209
+ function onDropFiles(files) {
210
+ if (props.onRouteFiles) props.onRouteFiles(files);
211
+ else gallery.onFiles(files);
212
+ }
202
213
  defineExpose({ uploadFiles });
203
214
  function moveAndAnnounce(from, to) {
204
215
  const id = model.value[from];
@@ -279,7 +290,7 @@ async function onTileKey(e, index) {
279
290
  :disabled="disabled || full"
280
291
  title="Drop to add"
281
292
  class="rounded-lg"
282
- @files="gallery.onFiles"
293
+ @files="onDropFiles"
283
294
  @rejected="gallery.onRejected"
284
295
  >
285
296
  <template #default="{ open }">
@@ -26,7 +26,9 @@ type __VLS_Props = {
26
26
  onOpen?: (assetId: string) => void;
27
27
  /** Upload profile name for new files in this list. */
28
28
  profile?: string | null;
29
- /** Start uploads at a backend service (name or `{ start }`). See useMLMediaUpload. */
29
+ /** Registered backend uploads go to (`mediaLibrary.backends`). Wins over `service`. */
30
+ backend?: string | null;
31
+ /** Older name for `backend`; also takes an inline `{ start }`. See useMLMediaUpload. */
30
32
  service?: MediaUploadOptions['service'];
31
33
  /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
32
34
  serviceBody?: Record<string, unknown> | null;
@@ -40,6 +42,10 @@ type __VLS_Props = {
40
42
  highlight?: ReadonlySet<string> | null;
41
43
  /** See useMLMediaGallery's onAdd. */
42
44
  onAdd?: (assetId: string, tag: string | undefined) => void;
45
+ /** Take new files instead of uploading them here (a gallery with roles routes them by kind). */
46
+ onRouteFiles?: (files: File[]) => void;
47
+ /** Upload profile per file, for uploads inside the picker. */
48
+ profileFor?: (file: File) => string | null | undefined;
43
49
  };
44
50
  /** Upload files with another profile than the list's own, e.g. from a role chip. */
45
51
  declare function uploadFiles(files: File[], opts?: {
@@ -85,8 +91,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
85
91
  };
86
92
  collectionName: string | null;
87
93
  profile: string | null;
94
+ backend: string | null;
88
95
  service: string | import("../../index.js").MediaServiceUpload | null;
89
96
  serviceBody: Record<string, unknown> | null;
97
+ profileFor: (file: File) => string | null | undefined;
90
98
  onAdd: (assetId: string, tag: string | undefined) => void;
91
99
  hint: string;
92
100
  picker: boolean;
@@ -97,6 +105,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
97
105
  recordName: string;
98
106
  badges: (assetId: string) => string[];
99
107
  tileMenu: (assetId: string, kind: string | null) => DropdownMenuItem[][];
108
+ onRouteFiles: (files: File[]) => void;
100
109
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
101
110
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
102
111
  declare const _default: typeof __VLS_export;