@abhi1693/medusa-extra-images 0.0.0-stage → 0.0.2

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 (37) hide show
  1. package/.medusa/server/src/admin/index.js +356 -0
  2. package/.medusa/server/src/admin/index.mjs +355 -0
  3. package/.medusa/server/src/api/admin/catalog-images/[kind]/[id]/media/route.d.ts +2 -0
  4. package/.medusa/server/src/api/admin/catalog-images/[kind]/[id]/media/route.js +11 -0
  5. package/.medusa/server/src/api/admin/catalog-images/[kind]/[id]/route.d.ts +4 -0
  6. package/.medusa/server/src/api/admin/catalog-images/[kind]/[id]/route.js +19 -0
  7. package/.medusa/server/src/api/admin/catalog-images/config/route.d.ts +2 -0
  8. package/.medusa/server/src/api/admin/catalog-images/config/route.js +13 -0
  9. package/.medusa/server/src/api/guard.d.ts +3 -0
  10. package/.medusa/server/src/api/guard.js +51 -0
  11. package/.medusa/server/src/api/middlewares.d.ts +2 -0
  12. package/.medusa/server/src/api/middlewares.js +33 -0
  13. package/.medusa/server/src/api/store/catalog-images/[kind]/[id]/route.d.ts +2 -0
  14. package/.medusa/server/src/api/store/catalog-images/[kind]/[id]/route.js +10 -0
  15. package/.medusa/server/src/catalog-images/features.d.ts +6 -0
  16. package/.medusa/server/src/catalog-images/features.js +18 -0
  17. package/.medusa/server/src/catalog-images/input.d.ts +10 -0
  18. package/.medusa/server/src/catalog-images/input.js +18 -0
  19. package/.medusa/server/src/catalog-images/media.d.ts +21 -0
  20. package/.medusa/server/src/catalog-images/media.js +45 -0
  21. package/.medusa/server/src/catalog-images/service.d.ts +16 -0
  22. package/.medusa/server/src/catalog-images/service.js +77 -0
  23. package/.medusa/server/src/config/catalog-images-access.d.ts +73 -0
  24. package/.medusa/server/src/config/catalog-images-access.js +43 -0
  25. package/.medusa/server/src/index.d.ts +5 -0
  26. package/.medusa/server/src/index.js +15 -0
  27. package/.medusa/server/src/modules/extra-images/index.d.ts +21 -0
  28. package/.medusa/server/src/modules/extra-images/index.js +11 -0
  29. package/.medusa/server/src/modules/extra-images/migrations/Migration20261106000000.d.ts +5 -0
  30. package/.medusa/server/src/modules/extra-images/migrations/Migration20261106000000.js +15 -0
  31. package/.medusa/server/src/modules/extra-images/models/extra-image.d.ts +8 -0
  32. package/.medusa/server/src/modules/extra-images/models/extra-image.js +20 -0
  33. package/.medusa/server/src/modules/extra-images/service.d.ts +19 -0
  34. package/.medusa/server/src/modules/extra-images/service.js +18 -0
  35. package/LICENSE +9 -0
  36. package/README.md +140 -2
  37. package/package.json +122 -4
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
4
+ const reactQuery = require("@tanstack/react-query");
5
+ const Medusa = require("@medusajs/js-sdk");
6
+ const icons = require("@medusajs/icons");
7
+ const ui = require("@medusajs/ui");
8
+ const react = require("react");
9
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
10
+ const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
11
+ const sdk = new Medusa__default.default({
12
+ baseUrl: typeof __BACKEND_URL__ === "string" ? __BACKEND_URL__ : "/",
13
+ auth: {
14
+ type: typeof __AUTH_TYPE__ === "string" ? __AUTH_TYPE__ : "session",
15
+ jwtTokenStorageKey: typeof __JWT_TOKEN_STORAGE_KEY__ === "string" ? __JWT_TOKEN_STORAGE_KEY__ : void 0
16
+ }
17
+ });
18
+ function ExistingMediaPicker({
19
+ kind,
20
+ id,
21
+ busy,
22
+ onSelect,
23
+ onClose
24
+ }) {
25
+ const [search, setSearch] = react.useState(""), [offset, setOffset] = react.useState(0);
26
+ const products = reactQuery.useQuery({
27
+ queryKey: ["catalog-artwork-media", kind, id, search, offset],
28
+ queryFn: () => sdk.client.fetch(
29
+ `/admin/catalog-images/${kind}/${id}/media`,
30
+ { query: { q: search, offset } }
31
+ ),
32
+ retry: false
33
+ });
34
+ return /* @__PURE__ */ jsxRuntime.jsxs(
35
+ "div",
36
+ {
37
+ role: "group",
38
+ "aria-label": "Existing product media",
39
+ className: "flex flex-col gap-6",
40
+ children: [
41
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
42
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: kind + "-artwork-media-search", children: "Find product media" }),
43
+ /* @__PURE__ */ jsxRuntime.jsx(
44
+ ui.Input,
45
+ {
46
+ id: kind + "-artwork-media-search",
47
+ type: "search",
48
+ value: search,
49
+ disabled: busy,
50
+ onChange: (event) => {
51
+ setSearch(event.target.value);
52
+ setOffset(0);
53
+ }
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
57
+ "Images from products in this ",
58
+ kind,
59
+ "."
60
+ ] })
61
+ ] }),
62
+ products.isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Loading media…" }) : products.isError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { role: "alert", children: "Could not load product media. Product view permission is required." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
63
+ products.data.products.length ? products.data.products.map((product) => {
64
+ const images = [...product.images || []];
65
+ if (product.thumbnail && !images.some((image) => image.url === product.thumbnail))
66
+ images.unshift({
67
+ id: product.id + "-thumbnail",
68
+ url: product.thumbnail
69
+ });
70
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
71
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", children: product.title }),
72
+ images.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-4", children: images.map((image, index) => /* @__PURE__ */ jsxRuntime.jsx(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ disabled: busy,
77
+ "aria-label": `Select ${product.title} image ${index + 1}`,
78
+ onClick: () => onSelect(image.url, product.id),
79
+ className: "relative aspect-square overflow-hidden rounded-[8px] shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:ring-2 focus-visible:ring-ui-border-interactive",
80
+ children: /* @__PURE__ */ jsxRuntime.jsx(
81
+ "img",
82
+ {
83
+ src: image.url,
84
+ alt: "",
85
+ className: "size-full object-cover"
86
+ }
87
+ )
88
+ },
89
+ image.id
90
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: "No media on this product." })
91
+ ] }, product.id);
92
+ }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No products match this search." }),
93
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ ui.Button,
96
+ {
97
+ size: "small",
98
+ variant: "secondary",
99
+ disabled: busy || !offset,
100
+ onClick: () => setOffset(Math.max(0, offset - 12)),
101
+ children: "Previous media"
102
+ }
103
+ ),
104
+ /* @__PURE__ */ jsxRuntime.jsx(
105
+ ui.Button,
106
+ {
107
+ size: "small",
108
+ variant: "secondary",
109
+ disabled: busy || offset + 12 >= products.data.count,
110
+ onClick: () => setOffset(offset + 12),
111
+ children: "Next media"
112
+ }
113
+ )
114
+ ] })
115
+ ] }),
116
+ /* @__PURE__ */ jsxRuntime.jsx(
117
+ ui.Button,
118
+ {
119
+ size: "small",
120
+ variant: "secondary",
121
+ disabled: busy,
122
+ onClick: onClose,
123
+ children: "Cancel"
124
+ }
125
+ )
126
+ ]
127
+ }
128
+ );
129
+ }
130
+ function CatalogArtwork({
131
+ kind,
132
+ id
133
+ }) {
134
+ const cache = reactQuery.useQueryClient(), key = ["catalog-artwork", kind, id], endpoint = `/admin/catalog-images/${kind}/${id}`;
135
+ const query = reactQuery.useQuery({
136
+ queryKey: key,
137
+ queryFn: () => sdk.client.fetch(endpoint),
138
+ retry: false
139
+ });
140
+ const [editing, setEditing] = react.useState(false), [selected, setSelected] = react.useState(false), [busy, setBusy] = react.useState(false), [error, setError] = react.useState("");
141
+ const current = query.data?.image, label = kind === "collection" ? "Collection" : "Category";
142
+ async function save(url, productId) {
143
+ setBusy(true);
144
+ setError("");
145
+ try {
146
+ const result = await sdk.client.fetch(endpoint, {
147
+ method: "POST",
148
+ body: {
149
+ url,
150
+ revision: current.revision,
151
+ ...productId ? { product_id: productId } : {}
152
+ }
153
+ });
154
+ cache.setQueryData(key, result);
155
+ await cache.invalidateQueries({
156
+ queryKey: [kind === "category" ? "product_categories" : "collections"]
157
+ });
158
+ setSelected(false);
159
+ setEditing(false);
160
+ ui.toast.success("Image saved");
161
+ } catch (error2) {
162
+ setError(
163
+ error2 instanceof Error ? error2.message : "Could not save image. Check your edit permission."
164
+ );
165
+ await cache.invalidateQueries({ queryKey: key });
166
+ } finally {
167
+ setBusy(false);
168
+ }
169
+ }
170
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
171
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
172
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
173
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Media" }),
174
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu, { children: [
175
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
176
+ ui.IconButton,
177
+ {
178
+ size: "small",
179
+ variant: "transparent",
180
+ "aria-label": label + " media actions",
181
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.EllipsisHorizontal, {})
182
+ }
183
+ ) }),
184
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Content, { children: /* @__PURE__ */ jsxRuntime.jsxs(
185
+ ui.DropdownMenu.Item,
186
+ {
187
+ disabled: busy || !current,
188
+ onClick: () => setEditing(true),
189
+ children: [
190
+ /* @__PURE__ */ jsxRuntime.jsx(icons.PencilSquare, {}),
191
+ "Edit image"
192
+ ]
193
+ }
194
+ ) })
195
+ ] })
196
+ ] }),
197
+ query.isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "px-6 py-4", children: "Loading media…" }) : query.isError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { role: "alert", className: "px-6 py-4", children: "Could not load media. Check your catalog view permission." }) : current && (current.url ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-4 px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "group relative aspect-square size-full overflow-hidden rounded-[8px] shadow-elevation-card-rest transition-fg hover:shadow-elevation-card-hover", children: [
198
+ /* @__PURE__ */ jsxRuntime.jsx(
199
+ "div",
200
+ {
201
+ className: `absolute right-2 top-2 z-10 transition-fg ${selected ? "visible opacity-100" : "invisible opacity-0 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 focus-within:visible focus-within:opacity-100"}`,
202
+ children: /* @__PURE__ */ jsxRuntime.jsx(
203
+ ui.Checkbox,
204
+ {
205
+ "aria-label": "Select " + kind + " image",
206
+ checked: selected,
207
+ disabled: busy,
208
+ onCheckedChange: () => setSelected((value) => !value)
209
+ }
210
+ )
211
+ }
212
+ ),
213
+ /* @__PURE__ */ jsxRuntime.jsx(
214
+ "button",
215
+ {
216
+ type: "button",
217
+ "aria-label": "Change " + kind + " image",
218
+ className: "size-full focus-visible:ring-2",
219
+ disabled: busy,
220
+ onClick: () => setEditing(true),
221
+ children: /* @__PURE__ */ jsxRuntime.jsx(
222
+ "img",
223
+ {
224
+ src: current.url,
225
+ alt: label + " image",
226
+ className: "size-full object-cover"
227
+ }
228
+ )
229
+ }
230
+ )
231
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-y-4 pb-8 pt-6", children: [
232
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center", children: [
233
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-subtle", children: "No images" }),
234
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-muted", children: [
235
+ "Select an image from products in this ",
236
+ kind,
237
+ "."
238
+ ] })
239
+ ] }),
240
+ /* @__PURE__ */ jsxRuntime.jsx(
241
+ ui.Button,
242
+ {
243
+ size: "small",
244
+ variant: "secondary",
245
+ onClick: () => setEditing(true),
246
+ children: "Add image"
247
+ }
248
+ )
249
+ ] })),
250
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { role: "alert", className: "px-6 py-3 text-ui-fg-error", children: error })
251
+ ] }),
252
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CommandBar, { open: selected, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.CommandBar.Bar, { children: [
253
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CommandBar.Value, { children: "1 selected" }),
254
+ /* @__PURE__ */ jsxRuntime.jsx(ui.CommandBar.Seperator, {}),
255
+ /* @__PURE__ */ jsxRuntime.jsx(
256
+ ui.CommandBar.Command,
257
+ {
258
+ label: "Remove",
259
+ shortcut: "d",
260
+ action: () => save(null)
261
+ }
262
+ )
263
+ ] }) }),
264
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal, { open: editing, onOpenChange: setEditing, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Content, { children: [
265
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { level: "h2", children: [
266
+ "Select ",
267
+ kind,
268
+ " image"
269
+ ] }) }),
270
+ /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Body, { className: "overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto flex w-full max-w-4xl flex-col gap-6 p-6", children: [
271
+ /* @__PURE__ */ jsxRuntime.jsx(
272
+ ExistingMediaPicker,
273
+ {
274
+ kind,
275
+ id,
276
+ busy,
277
+ onSelect: save,
278
+ onClose: () => setEditing(false)
279
+ }
280
+ ),
281
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { role: "alert", className: "text-ui-fg-error", children: error })
282
+ ] }) })
283
+ ] }) })
284
+ ] });
285
+ }
286
+ function FeatureArtwork({
287
+ kind,
288
+ id
289
+ }) {
290
+ const config = reactQuery.useQuery({
291
+ queryKey: ["extra-images-config"],
292
+ queryFn: () => sdk.client.fetch(
293
+ "/admin/catalog-images/config"
294
+ ),
295
+ retry: false
296
+ });
297
+ if (!(kind === "category" ? config.data?.categories : config.data?.collections))
298
+ return null;
299
+ return /* @__PURE__ */ jsxRuntime.jsx(CatalogArtwork, { kind, id });
300
+ }
301
+ function CategoryArtwork({
302
+ data
303
+ }) {
304
+ return /* @__PURE__ */ jsxRuntime.jsx(FeatureArtwork, { kind: "category", id: data.id }, data.id);
305
+ }
306
+ adminSdk.defineWidgetConfig({
307
+ zone: "product_category.details.side",
308
+ id: "extra-images:category-artwork"
309
+ });
310
+ function CollectionArtwork({
311
+ data
312
+ }) {
313
+ return /* @__PURE__ */ jsxRuntime.jsx(FeatureArtwork, { kind: "collection", id: data.id }, data.id);
314
+ }
315
+ adminSdk.defineWidgetConfig({
316
+ zone: "product_collection.details",
317
+ id: "extra-images:collection-artwork"
318
+ });
319
+ const widgetModule = { widgets: [
320
+ {
321
+ Component: CategoryArtwork,
322
+ zone: ["product_category.details.side"],
323
+ widgetId: "extra-images:category-artwork"
324
+ },
325
+ {
326
+ Component: CollectionArtwork,
327
+ zone: ["product_collection.details"],
328
+ widgetId: "extra-images:collection-artwork"
329
+ }
330
+ ] };
331
+ const routeModule = {
332
+ routes: []
333
+ };
334
+ const menuItemModule = {
335
+ menuItems: []
336
+ };
337
+ const formModule = { customFields: {} };
338
+ const displayModule = {
339
+ displays: {}
340
+ };
341
+ const i18nModule = { resources: {} };
342
+ const cellRendererModule = {};
343
+ const layoutModule = { layouts: [] };
344
+ const searchEntityModule = {};
345
+ const plugin = {
346
+ widgetModule,
347
+ routeModule,
348
+ menuItemModule,
349
+ formModule,
350
+ displayModule,
351
+ i18nModule,
352
+ cellRendererModule,
353
+ layoutModule,
354
+ searchEntityModule
355
+ };
356
+ module.exports = plugin;