@csbeker/medusa-product-attributes 2.2.0

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 (237) hide show
  1. package/.medusa/server/src/admin/index.js +9012 -0
  2. package/.medusa/server/src/admin/index.mjs +9010 -0
  3. package/.medusa/server/src/api/admin/middlewares.js +10 -0
  4. package/.medusa/server/src/api/admin/plugin/attribute-set/[id]/attributes/route.js +17 -0
  5. package/.medusa/server/src/api/admin/plugin/attribute-set/[id]/route.js +33 -0
  6. package/.medusa/server/src/api/admin/plugin/attribute-set/middlewares.js +44 -0
  7. package/.medusa/server/src/api/admin/plugin/attribute-set/query-config.js +22 -0
  8. package/.medusa/server/src/api/admin/plugin/attribute-set/route.js +25 -0
  9. package/.medusa/server/src/api/admin/plugin/attribute-set/validators.js +37 -0
  10. package/.medusa/server/src/api/admin/plugin/attributes/[id]/route.js +68 -0
  11. package/.medusa/server/src/api/admin/plugin/attributes/[id]/values/[valueId]/route.js +37 -0
  12. package/.medusa/server/src/api/admin/plugin/attributes/[id]/values/route.js +31 -0
  13. package/.medusa/server/src/api/admin/plugin/attributes/middlewares.js +103 -0
  14. package/.medusa/server/src/api/admin/plugin/attributes/query-config.js +41 -0
  15. package/.medusa/server/src/api/admin/plugin/attributes/route.js +28 -0
  16. package/.medusa/server/src/api/admin/plugin/attributes/validators.js +69 -0
  17. package/.medusa/server/src/api/admin/plugin/route.js +7 -0
  18. package/.medusa/server/src/api/middlewares.js +12 -0
  19. package/.medusa/server/src/api/store/middlewares.js +8 -0
  20. package/.medusa/server/src/api/store/plugin/attributes/middlewares.js +64 -0
  21. package/.medusa/server/src/api/store/plugin/attributes/products/middlewares.js +100 -0
  22. package/.medusa/server/src/api/store/plugin/attributes/products/query-config.js +20 -0
  23. package/.medusa/server/src/api/store/plugin/attributes/products/route.js +48 -0
  24. package/.medusa/server/src/api/store/plugin/attributes/products/validators.js +39 -0
  25. package/.medusa/server/src/api/store/plugin/attributes/query-config.js +21 -0
  26. package/.medusa/server/src/api/store/plugin/attributes/route.js +15 -0
  27. package/.medusa/server/src/api/store/plugin/attributes/validators.js +14 -0
  28. package/.medusa/server/src/api/store/plugin/route.js +7 -0
  29. package/.medusa/server/src/api/utils/common-validators.js +23 -0
  30. package/.medusa/server/src/api/utils/constants.js +6 -0
  31. package/.medusa/server/src/api/utils/middlewares.js +34 -0
  32. package/.medusa/server/src/links/attribute-product-category.js +16 -0
  33. package/.medusa/server/src/links/attribute-value-product.js +16 -0
  34. package/.medusa/server/src/modules/attribute/events/index.js +8 -0
  35. package/.medusa/server/src/modules/attribute/index.js +13 -0
  36. package/.medusa/server/src/modules/attribute/migrations/Migration20250319161229.js +24 -0
  37. package/.medusa/server/src/modules/attribute/migrations/Migration20250320182643.js +16 -0
  38. package/.medusa/server/src/modules/attribute/migrations/Migration20250321162638.js +14 -0
  39. package/.medusa/server/src/modules/attribute/migrations/Migration20250505144933.js +23 -0
  40. package/.medusa/server/src/modules/attribute/migrations/Migration20250505201747.js +21 -0
  41. package/.medusa/server/src/modules/attribute/migrations/Migration20250506162300.js +14 -0
  42. package/.medusa/server/src/modules/attribute/migrations/Migration20250611160552.js +14 -0
  43. package/.medusa/server/src/modules/attribute/migrations/Migration20250611173345.js +16 -0
  44. package/.medusa/server/src/modules/attribute/migrations/Migration20250612192857.js +16 -0
  45. package/.medusa/server/src/modules/attribute/models/attribute-possible-value.js +24 -0
  46. package/.medusa/server/src/modules/attribute/models/attribute-set.js +22 -0
  47. package/.medusa/server/src/modules/attribute/models/attribute-value.js +17 -0
  48. package/.medusa/server/src/modules/attribute/models/attribute.js +27 -0
  49. package/.medusa/server/src/modules/attribute/service.js +84 -0
  50. package/.medusa/server/src/modules/attribute/types/attribute/common.js +13 -0
  51. package/.medusa/server/src/modules/attribute/types/attribute/index.js +18 -0
  52. package/.medusa/server/src/modules/attribute/types/attribute-set/index.js +18 -0
  53. package/.medusa/server/src/modules/attribute/types/attribute-set/mutations.js +3 -0
  54. package/.medusa/server/src/modules/attribute/types/attribute-value/index.js +18 -0
  55. package/.medusa/server/src/modules/attribute/types/attribute-value/mutations.js +3 -0
  56. package/.medusa/server/src/modules/attribute/types/index.js +20 -0
  57. package/.medusa/server/src/types/attribute/common.js +3 -0
  58. package/.medusa/server/src/types/attribute/http/attribute/admin/index.js +3 -0
  59. package/.medusa/server/src/types/attribute/http/attribute/index.js +18 -0
  60. package/.medusa/server/src/types/attribute/http/attribute-set/index.js +3 -0
  61. package/.medusa/server/src/types/attribute/http/index.js +19 -0
  62. package/.medusa/server/src/types/attribute/index.js +19 -0
  63. package/.medusa/server/src/utils/index.js +18 -0
  64. package/.medusa/server/src/utils/products-created-handler.js +22 -0
  65. package/.medusa/server/src/utils/products-updated-handler.js +58 -0
  66. package/.medusa/server/src/utils/validate-attribute-values-to-link.js +43 -0
  67. package/.medusa/server/src/workflows/attribute/index.js +19 -0
  68. package/.medusa/server/src/workflows/attribute/steps/create-attribute-possible-values.js +18 -0
  69. package/.medusa/server/src/workflows/attribute/steps/create-attributes.js +27 -0
  70. package/.medusa/server/src/workflows/attribute/steps/delete-attribute.js +31 -0
  71. package/.medusa/server/src/workflows/attribute/steps/index.js +21 -0
  72. package/.medusa/server/src/workflows/attribute/steps/update-attributes.js +34 -0
  73. package/.medusa/server/src/workflows/attribute/workflows/create-attribute-possible-values.js +11 -0
  74. package/.medusa/server/src/workflows/attribute/workflows/create-attributes.js +46 -0
  75. package/.medusa/server/src/workflows/attribute/workflows/delete-attribute.js +10 -0
  76. package/.medusa/server/src/workflows/attribute/workflows/index.js +20 -0
  77. package/.medusa/server/src/workflows/attribute/workflows/update-attributes.js +73 -0
  78. package/.medusa/server/src/workflows/attribute-set/steps/batch-link-attribute-set-attributes.js +66 -0
  79. package/.medusa/server/src/workflows/attribute-set/steps/create-attribute-set.js +24 -0
  80. package/.medusa/server/src/workflows/attribute-set/steps/index.js +20 -0
  81. package/.medusa/server/src/workflows/attribute-set/steps/update-attribute-set.js +22 -0
  82. package/.medusa/server/src/workflows/attribute-set/workflows/batch-link-attribute-set-attributes.js +10 -0
  83. package/.medusa/server/src/workflows/attribute-set/workflows/create-attribute-set.js +11 -0
  84. package/.medusa/server/src/workflows/attribute-set/workflows/index.js +20 -0
  85. package/.medusa/server/src/workflows/attribute-set/workflows/update-attribute-set.js +10 -0
  86. package/.medusa/server/src/workflows/attribute-value/steps/create-attribute-value.js +18 -0
  87. package/.medusa/server/src/workflows/attribute-value/steps/delete-attribute-value.js +18 -0
  88. package/.medusa/server/src/workflows/attribute-value/steps/index.js +20 -0
  89. package/.medusa/server/src/workflows/attribute-value/steps/validate-attribute-value.js +53 -0
  90. package/.medusa/server/src/workflows/attribute-value/workflow/create-attribute-value.js +28 -0
  91. package/.medusa/server/src/workflows/attribute-value/workflow/delete-attribute-value.js +38 -0
  92. package/.medusa/server/src/workflows/attribute-value/workflow/index.js +19 -0
  93. package/.medusa/server/src/workflows/attribute_possible_value/index.js +19 -0
  94. package/.medusa/server/src/workflows/attribute_possible_value/steps/index.js +18 -0
  95. package/.medusa/server/src/workflows/attribute_possible_value/steps/update-attribute-possible-value.js +18 -0
  96. package/.medusa/server/src/workflows/attribute_possible_value/workflows/index.js +18 -0
  97. package/.medusa/server/src/workflows/attribute_possible_value/workflows/update-attribute-possible-value.js +11 -0
  98. package/.medusa/server/src/workflows/index.js +18 -0
  99. package/CHANGELOG.md +104 -0
  100. package/README.md +86 -0
  101. package/package.json +90 -0
  102. package/src/admin/README.md +31 -0
  103. package/src/admin/components/metadata-editor/index.tsx +101 -0
  104. package/src/admin/components/section-row.tsx +41 -0
  105. package/src/admin/hooks/api/attribute-set.ts +122 -0
  106. package/src/admin/hooks/api/attributes.ts +126 -0
  107. package/src/admin/hooks/table/columns/index.ts +1 -0
  108. package/src/admin/hooks/table/columns/use-attribute-table-columns.tsx +280 -0
  109. package/src/admin/layouts/single-column.tsx +11 -0
  110. package/src/admin/lib/config.ts +8 -0
  111. package/src/admin/lib/query-key-factory.ts +53 -0
  112. package/src/admin/routes/attributes/[id]/edit/page.tsx +133 -0
  113. package/src/admin/routes/attributes/[id]/edit-possible-value/page.tsx +174 -0
  114. package/src/admin/routes/attributes/[id]/page.tsx +127 -0
  115. package/src/admin/routes/attributes/components/AttributeForm.tsx +301 -0
  116. package/src/admin/routes/attributes/components/AttributeSetTable.tsx +108 -0
  117. package/src/admin/routes/attributes/components/category-selection-modal.tsx +82 -0
  118. package/src/admin/routes/attributes/components/possible-values-table.tsx +119 -0
  119. package/src/admin/routes/attributes/create/components/MultiSelectCategory.tsx +148 -0
  120. package/src/admin/routes/attributes/create/components/PossibleValuesList.tsx +151 -0
  121. package/src/admin/routes/attributes/create/page.tsx +123 -0
  122. package/src/admin/routes/attributes/create-set/page.tsx +110 -0
  123. package/src/admin/routes/attributes/page.tsx +346 -0
  124. package/src/admin/routes/attributes/set/[id]/attributes/page.tsx +35 -0
  125. package/src/admin/routes/attributes/set/[id]/components/AttributeSetAttributesSection.tsx +114 -0
  126. package/src/admin/routes/attributes/set/[id]/components/AttributeSetGeneralSection.tsx +42 -0
  127. package/src/admin/routes/attributes/set/[id]/components/attribute-set-attributes-form.tsx +143 -0
  128. package/src/admin/routes/attributes/set/[id]/components/index.ts +2 -0
  129. package/src/admin/routes/attributes/set/[id]/edit/page.tsx +119 -0
  130. package/src/admin/routes/attributes/set/[id]/page.tsx +45 -0
  131. package/src/admin/tsconfig.json +27 -0
  132. package/src/admin/types/global.d.ts +3 -0
  133. package/src/admin/vite-env.d.ts +1 -0
  134. package/src/api/README.md +133 -0
  135. package/src/api/admin/middlewares.ts +8 -0
  136. package/src/api/admin/plugin/attribute-set/[id]/attributes/route.ts +17 -0
  137. package/src/api/admin/plugin/attribute-set/[id]/route.ts +41 -0
  138. package/src/api/admin/plugin/attribute-set/middlewares.ts +42 -0
  139. package/src/api/admin/plugin/attribute-set/query-config.ts +20 -0
  140. package/src/api/admin/plugin/attribute-set/route.ts +34 -0
  141. package/src/api/admin/plugin/attribute-set/validators.ts +45 -0
  142. package/src/api/admin/plugin/attributes/[id]/route.ts +85 -0
  143. package/src/api/admin/plugin/attributes/[id]/values/[valueId]/route.ts +41 -0
  144. package/src/api/admin/plugin/attributes/[id]/values/route.ts +39 -0
  145. package/src/api/admin/plugin/attributes/middlewares.ts +91 -0
  146. package/src/api/admin/plugin/attributes/query-config.ts +42 -0
  147. package/src/api/admin/plugin/attributes/route.ts +33 -0
  148. package/src/api/admin/plugin/attributes/validators.ts +91 -0
  149. package/src/api/admin/plugin/route.ts +8 -0
  150. package/src/api/middlewares.ts +10 -0
  151. package/src/api/store/middlewares.ts +6 -0
  152. package/src/api/store/plugin/attributes/middlewares.ts +33 -0
  153. package/src/api/store/plugin/attributes/products/middlewares.ts +73 -0
  154. package/src/api/store/plugin/attributes/products/query-config.ts +19 -0
  155. package/src/api/store/plugin/attributes/products/route.ts +68 -0
  156. package/src/api/store/plugin/attributes/products/validators.ts +55 -0
  157. package/src/api/store/plugin/attributes/query-config.ts +19 -0
  158. package/src/api/store/plugin/attributes/route.ts +13 -0
  159. package/src/api/store/plugin/attributes/validators.ts +14 -0
  160. package/src/api/store/plugin/route.ts +8 -0
  161. package/src/api/utils/common-validators.ts +24 -0
  162. package/src/api/utils/constants.ts +2 -0
  163. package/src/api/utils/middlewares.ts +31 -0
  164. package/src/jobs/README.md +36 -0
  165. package/src/links/README.md +26 -0
  166. package/src/links/attribute-product-category.ts +14 -0
  167. package/src/links/attribute-value-product.ts +14 -0
  168. package/src/modules/README.md +116 -0
  169. package/src/modules/attribute/events/index.ts +4 -0
  170. package/src/modules/attribute/index.ts +8 -0
  171. package/src/modules/attribute/migrations/.snapshot-medusa-attribute.json +624 -0
  172. package/src/modules/attribute/migrations/Migration20250319161229.ts +27 -0
  173. package/src/modules/attribute/migrations/Migration20250320182643.ts +15 -0
  174. package/src/modules/attribute/migrations/Migration20250321162638.ts +13 -0
  175. package/src/modules/attribute/migrations/Migration20250505144933.ts +26 -0
  176. package/src/modules/attribute/migrations/Migration20250505201747.ts +23 -0
  177. package/src/modules/attribute/migrations/Migration20250506162300.ts +13 -0
  178. package/src/modules/attribute/migrations/Migration20250611160552.ts +13 -0
  179. package/src/modules/attribute/migrations/Migration20250611173345.ts +17 -0
  180. package/src/modules/attribute/migrations/Migration20250612192857.ts +17 -0
  181. package/src/modules/attribute/models/attribute-possible-value.ts +20 -0
  182. package/src/modules/attribute/models/attribute-set.ts +18 -0
  183. package/src/modules/attribute/models/attribute-value.ts +13 -0
  184. package/src/modules/attribute/models/attribute.ts +23 -0
  185. package/src/modules/attribute/service.ts +102 -0
  186. package/src/modules/attribute/types/attribute/common.ts +94 -0
  187. package/src/modules/attribute/types/attribute/index.ts +1 -0
  188. package/src/modules/attribute/types/attribute-set/index.ts +1 -0
  189. package/src/modules/attribute/types/attribute-set/mutations.ts +7 -0
  190. package/src/modules/attribute/types/attribute-value/index.ts +1 -0
  191. package/src/modules/attribute/types/attribute-value/mutations.ts +5 -0
  192. package/src/modules/attribute/types/index.ts +3 -0
  193. package/src/providers/README.md +30 -0
  194. package/src/subscribers/README.md +59 -0
  195. package/src/types/attribute/common.ts +173 -0
  196. package/src/types/attribute/http/attribute/admin/index.ts +0 -0
  197. package/src/types/attribute/http/attribute/index.ts +42 -0
  198. package/src/types/attribute/http/attribute-set/index.ts +10 -0
  199. package/src/types/attribute/http/index.ts +2 -0
  200. package/src/types/attribute/index.ts +2 -0
  201. package/src/utils/index.ts +1 -0
  202. package/src/utils/products-created-handler.ts +35 -0
  203. package/src/utils/products-updated-handler.ts +74 -0
  204. package/src/utils/validate-attribute-values-to-link.ts +67 -0
  205. package/src/workflows/README.md +79 -0
  206. package/src/workflows/attribute/index.ts +2 -0
  207. package/src/workflows/attribute/steps/create-attribute-possible-values.ts +29 -0
  208. package/src/workflows/attribute/steps/create-attributes.ts +35 -0
  209. package/src/workflows/attribute/steps/delete-attribute.ts +41 -0
  210. package/src/workflows/attribute/steps/index.ts +4 -0
  211. package/src/workflows/attribute/steps/update-attributes.ts +45 -0
  212. package/src/workflows/attribute/workflows/create-attribute-possible-values.ts +17 -0
  213. package/src/workflows/attribute/workflows/create-attributes.ts +56 -0
  214. package/src/workflows/attribute/workflows/delete-attribute.ts +15 -0
  215. package/src/workflows/attribute/workflows/index.ts +3 -0
  216. package/src/workflows/attribute/workflows/update-attributes.ts +103 -0
  217. package/src/workflows/attribute-set/steps/batch-link-attribute-set-attributes.ts +82 -0
  218. package/src/workflows/attribute-set/steps/create-attribute-set.ts +34 -0
  219. package/src/workflows/attribute-set/steps/index.ts +3 -0
  220. package/src/workflows/attribute-set/steps/update-attribute-set.ts +32 -0
  221. package/src/workflows/attribute-set/workflows/batch-link-attribute-set-attributes.ts +12 -0
  222. package/src/workflows/attribute-set/workflows/create-attribute-set.ts +17 -0
  223. package/src/workflows/attribute-set/workflows/index.ts +3 -0
  224. package/src/workflows/attribute-set/workflows/update-attribute-set.ts +14 -0
  225. package/src/workflows/attribute-value/steps/create-attribute-value.ts +26 -0
  226. package/src/workflows/attribute-value/steps/delete-attribute-value.ts +26 -0
  227. package/src/workflows/attribute-value/steps/index.ts +3 -0
  228. package/src/workflows/attribute-value/steps/validate-attribute-value.ts +95 -0
  229. package/src/workflows/attribute-value/workflow/create-attribute-value.ts +36 -0
  230. package/src/workflows/attribute-value/workflow/delete-attribute-value.ts +46 -0
  231. package/src/workflows/attribute-value/workflow/index.ts +2 -0
  232. package/src/workflows/attribute_possible_value/index.ts +2 -0
  233. package/src/workflows/attribute_possible_value/steps/index.ts +1 -0
  234. package/src/workflows/attribute_possible_value/steps/update-attribute-possible-value.ts +25 -0
  235. package/src/workflows/attribute_possible_value/workflows/index.ts +1 -0
  236. package/src/workflows/attribute_possible_value/workflows/update-attribute-possible-value.ts +15 -0
  237. package/src/workflows/index.ts +1 -0
@@ -0,0 +1,301 @@
1
+ import {
2
+ Text,
3
+ Input,
4
+ Label,
5
+ Switch,
6
+ Select,
7
+ Textarea,
8
+ InlineTip
9
+ } from "@medusajs/ui";
10
+ import { useForm, FormProvider } from "react-hook-form";
11
+ import { zodResolver } from "@hookform/resolvers/zod";
12
+ import { z } from "zod";
13
+ import { AttributeUIComponent } from "../../../../modules/attribute/types";
14
+ import { AdminUpdateAttribute, CreateAttribute } from "../../../../api/admin/plugin/attributes/validators";
15
+ import { AdminProductCategory } from "@medusajs/types";
16
+ import MultiSelectCategory from "../create/components/MultiSelectCategory";
17
+ import PossibleValuesList from "../create/components/PossibleValuesList";
18
+ import { Attribute } from "../../../../types/attribute/http/attribute";
19
+ import { useEffect, useState } from "react";
20
+
21
+ export const CreateAttributeFormSchema = CreateAttribute
22
+
23
+ type CreateFormValues = z.infer<typeof CreateAttributeFormSchema>;
24
+
25
+ export const UdpateAttributeFormSchema = AdminUpdateAttribute
26
+
27
+ type UpdateFormValues = z.infer<typeof UdpateAttributeFormSchema>;
28
+ interface AttributeFormProps {
29
+ initialData?: Attribute;
30
+ onSubmit: (data: CreateFormValues | UpdateFormValues) => Promise<void>;
31
+ categories?: AdminProductCategory[]
32
+ mode?: 'create' | 'update'
33
+ activeTab?: "details" | "type"
34
+ onFormStateChange?: (formState: {
35
+ detailsStatus: "not-started" | "in-progress" | "completed";
36
+ typeStatus: "not-started" | "in-progress" | "completed";
37
+ }) => void
38
+ }
39
+
40
+ export const AttributeForm = ({
41
+ initialData,
42
+ onSubmit,
43
+ categories = [],
44
+ mode = 'create',
45
+ activeTab = "details",
46
+ onFormStateChange
47
+ }: AttributeFormProps) => {
48
+ const [showCategorySection, setShowCategorySection] = useState(
49
+ (initialData?.product_categories?.length || 0) > 0
50
+ );
51
+
52
+ const form = useForm<CreateFormValues | UpdateFormValues>({
53
+ resolver: zodResolver(mode === 'create' ? CreateAttributeFormSchema : UdpateAttributeFormSchema),
54
+ defaultValues: {
55
+ name: initialData?.name || "",
56
+ description: initialData?.description || "",
57
+ handle: initialData?.handle || "",
58
+ ui_component: initialData?.ui_component || AttributeUIComponent.SELECT,
59
+ is_variant_defining: initialData?.is_variant_defining ?? true,
60
+ is_filterable: initialData?.is_filterable ?? true,
61
+ possible_values: initialData?.possible_values || [],
62
+ product_category_ids: initialData?.product_categories?.map(c => c.id) || [],
63
+ metadata: initialData?.metadata || null,
64
+ },
65
+ });
66
+
67
+ const handleSubmit = form.handleSubmit(async (data) => {
68
+ try {
69
+ await onSubmit(data);
70
+ } catch (error) {
71
+ console.error(error);
72
+ }
73
+ });
74
+
75
+ const getTabStatus = () => {
76
+ const formData = form.getValues();
77
+
78
+ // Details tab status
79
+ const hasName = formData.name?.trim();
80
+ const hasDetailsData =
81
+ formData.description?.trim() ||
82
+ formData.handle?.trim() ||
83
+ formData.product_category_ids?.length;
84
+ const detailsStatus = hasName
85
+ ? "completed"
86
+ : hasDetailsData
87
+ ? "in-progress"
88
+ : "not-started";
89
+
90
+ // Type tab status
91
+ const hasTypeData =
92
+ formData.ui_component && (formData.possible_values?.length || 0) > 0;
93
+ const typeStatus = hasTypeData ? "completed" : "not-started";
94
+
95
+ return {
96
+ detailsStatus: detailsStatus as
97
+ | "not-started"
98
+ | "in-progress"
99
+ | "completed",
100
+ typeStatus: typeStatus as "not-started" | "in-progress" | "completed",
101
+ };
102
+ };
103
+
104
+ useEffect(() => {
105
+ if (onFormStateChange) {
106
+ const statuses = getTabStatus();
107
+ onFormStateChange(statuses);
108
+ }
109
+ }, [form.watch(), onFormStateChange]);
110
+
111
+ const renderDetailsTab = () => (
112
+ <div className="grid gap-6">
113
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
114
+ <div>
115
+ <Label size="small" htmlFor="name">Name</Label>
116
+ <Input size="small" id="name" className="mt-1" {...form.register("name")} />
117
+ {form.formState.errors.name && (
118
+ <Text className="text-red-500 text-sm mt-1">
119
+ {form.formState.errors.name.message}
120
+ </Text>
121
+ )}
122
+ </div>
123
+ <div>
124
+ <Label size="small" htmlFor="handle">Handle <span className="text-ui-fg-subtle text-xs">(Optional)</span></Label>
125
+ <div className="relative">
126
+ <Input size="small" id="handle" className="pl-9 mt-1" {...form.register("handle")} />
127
+ <div className="absolute z-100 left-0 top-1 bottom-0 flex items-center justify-center px-2 w-7 border-r border-ui-border-base text-ui-fg-muted">/</div>
128
+ {form.formState.errors.handle && (
129
+ <Text className="text-red-500 text-sm mt-1">
130
+ {form.formState.errors.handle.message}
131
+ </Text>
132
+ )}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ <div className="grid grid-cols-1 gap-4">
137
+ <div>
138
+ <Label size="small" htmlFor="description">Description <span className="text-ui-fg-subtle text-xs">(Optional)</span></Label>
139
+ <Textarea className="mt-1"
140
+ id="description"
141
+ {...form.register("description")}
142
+ />
143
+ {form.formState.errors.description && (
144
+ <Text className="text-red-500 text-sm mt-1">
145
+ {form.formState.errors.description.message}
146
+ </Text>
147
+ )}
148
+ </div>
149
+
150
+ <div className="bg-ui-bg-component p-4 rounded-lg shadow-elevation-card-rest">
151
+ <div className="flex gap-3">
152
+ <Switch
153
+ id="is_filterable"
154
+ checked={form.watch("is_filterable")}
155
+ onCheckedChange={(checked) =>
156
+ form.setValue("is_filterable", checked)
157
+ }
158
+ className="mt-1"
159
+ />
160
+ <div>
161
+ <Label size="small" htmlFor="is_filterable">Yes, this is a filterable attribute</Label>
162
+ <Text className="text-ui-fg-subtle text-xs mt-1">
163
+ If checked, buyers will be able to filter products using this attribute.
164
+ </Text>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ {/* <div className="bg-ui-bg-component p-4 rounded-lg shadow-elevation-card-rest">
170
+ <div className="flex gap-3">
171
+ <Switch
172
+ id="is_required"
173
+ checked={form.watch("is_required")}
174
+ onCheckedChange={(checked) =>
175
+ form.setValue("is_required", checked)
176
+ }
177
+ className="mt-1"
178
+ />
179
+ <div>
180
+ <Label size="small" htmlFor="is_required">Yes, this is a required attribute</Label>
181
+ <Text className="text-ui-fg-subtle text-xs mt-1">
182
+ If checked, vendors must set a value to this attribute.
183
+ </Text>
184
+ </div>
185
+ </div>
186
+ </div> */}
187
+
188
+ <div className="bg-ui-bg-component p-4 rounded-lg shadow-elevation-card-rest">
189
+ <div className="flex gap-3">
190
+ <Switch
191
+ id="is_global"
192
+ checked={!form.watch("product_category_ids")?.length && !showCategorySection}
193
+ onCheckedChange={(checked) => {
194
+ if (checked) {
195
+ form.setValue("product_category_ids", []);
196
+ setShowCategorySection(false);
197
+ } else {
198
+ setShowCategorySection(true);
199
+ }
200
+ }}
201
+ className="mt-1"
202
+ />
203
+ <div>
204
+ <Label size="small" htmlFor="is_global">Yes, this is a global attribute</Label>
205
+ <Text className="text-ui-fg-subtle text-xs mt-1">
206
+ If checked, this attribute will be available for all products regardless of category.
207
+ </Text>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ {(showCategorySection || (form.watch("product_category_ids")?.length || 0) > 0) && (
213
+ <div>
214
+ <Label size="small" htmlFor="product_categories">
215
+ Category
216
+ </Label>
217
+ <div className="mt-1">
218
+ <MultiSelectCategory
219
+ categories={categories || []}
220
+ value={form.watch("product_category_ids") || []}
221
+ onChange={(value) =>
222
+ form.setValue("product_category_ids", value)
223
+ }
224
+ />
225
+ {form.formState.errors.product_category_ids && (
226
+ <Text className="text-red-500 text-sm mt-1">
227
+ {form.formState.errors.product_category_ids.message}
228
+ </Text>
229
+ )}
230
+ </div>
231
+
232
+ </div>
233
+ )}
234
+ <div className={`mt-3 ${!(showCategorySection || (form.watch("product_category_ids")?.length || 0) > 0) && 'collapse'}`}>
235
+ <InlineTip label="Warning" variant="warning">
236
+ All child categories of the selected category will automatically inherit its attributes.
237
+ </InlineTip>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ );
242
+
243
+ const renderTypeTab = () => (
244
+ <div className="grid gap-6 w-[720px]">
245
+ <div>
246
+ <Label size="small" htmlFor="ui_component">Type</Label>
247
+ <Select
248
+ value={form.watch("ui_component")}
249
+ onValueChange={(value) =>
250
+ form.setValue(
251
+ "ui_component",
252
+ value as AttributeUIComponent
253
+ )
254
+ }
255
+ >
256
+ <Select.Trigger className="mt-1">
257
+ <Select.Value placeholder="Select Type" />
258
+ </Select.Trigger>
259
+ <Select.Content>
260
+ {Object.values(AttributeUIComponent).map((component) => (
261
+ <Select.Item key={component} value={component}>
262
+ {component === 'select' ? 'Single Select' :
263
+ component === 'multivalue' ? 'Multi Select' :
264
+ component === 'unit' ? 'Unit' :
265
+ component === 'toggle' ? 'Toggle' :
266
+ component === 'text_area' ? 'Text' :
267
+ component}
268
+ </Select.Item>
269
+ ))}
270
+ </Select.Content>
271
+ </Select>
272
+ {form.formState.errors.ui_component && (
273
+ <Text className="text-red-500 text-sm mt-1">
274
+ {form.formState.errors.ui_component.message}
275
+ </Text>
276
+ )}
277
+ </div>
278
+
279
+ {form.watch("ui_component") === AttributeUIComponent.SELECT && (
280
+ <InlineTip label="Tip" variant="info" >
281
+ When creating Single Select buyers will be able to choose only one value. This type of attribute will be good for product specifications.
282
+ </InlineTip>
283
+ )}
284
+
285
+ {(form.watch("ui_component") === AttributeUIComponent.SELECT || form.watch("ui_component") === AttributeUIComponent.MULTIVALUE) && (
286
+ <div>
287
+ <PossibleValuesList />
288
+ </div>
289
+ )}
290
+ </div>
291
+ );
292
+
293
+ return (
294
+ <FormProvider {...form}>
295
+ <form id="attribute-form" onSubmit={handleSubmit}>
296
+ {activeTab === "details" && renderDetailsTab()}
297
+ {activeTab === "type" && renderTypeTab()}
298
+ </form>
299
+ </FormProvider>
300
+ );
301
+ };
@@ -0,0 +1,108 @@
1
+ import {
2
+ createDataTableColumnHelper,
3
+ Badge,
4
+ DataTablePaginationState,
5
+ useDataTable,
6
+ Container,
7
+ Heading,
8
+ Button,
9
+ DataTable,
10
+ } from "@medusajs/ui";
11
+ import { useState } from "react";
12
+ import { useNavigate } from "react-router-dom";
13
+ import { AttributeSet } from "../../../../types/attribute/http/attribute-set";
14
+ import { useAttributeSets } from "../../../hooks/api/attribute-set";
15
+
16
+ export const AttributeSetTable = () => {
17
+ const [page, setPage] = useState(1);
18
+ const pageSize = 10;
19
+ const navigate = useNavigate();
20
+
21
+ const { attributeSets, count, isLoading } = useAttributeSets({
22
+ limit: pageSize,
23
+ offset: (page - 1) * pageSize,
24
+ })
25
+
26
+ const columnHelper = createDataTableColumnHelper<AttributeSet>();
27
+
28
+ const columns = [
29
+ columnHelper.accessor("name", {
30
+ header: "Name",
31
+ }),
32
+ columnHelper.accessor("description", {
33
+ header: "Description",
34
+ }),
35
+ columnHelper.accessor("handle", {
36
+ header: "Handle",
37
+ }),
38
+ columnHelper.accessor("attributes", {
39
+ header: "Attributes",
40
+ cell: (info) => {
41
+ const attributes = info.getValue();
42
+ return (
43
+ <div className="flex flex-wrap gap-2">
44
+ {attributes?.map((attribute) => (
45
+ <Badge size="xsmall" key={attribute.id}>
46
+ {attribute.name}
47
+ </Badge>
48
+ )) || "-"}
49
+ </div>
50
+ );
51
+ },
52
+ }),
53
+ ];
54
+
55
+ const [pagination, setPagination] = useState<DataTablePaginationState>({
56
+ pageIndex: page - 1,
57
+ pageSize,
58
+ });
59
+
60
+ const [search, setSearch] = useState("");
61
+
62
+ const table = useDataTable({
63
+ columns,
64
+ data: attributeSets || [],
65
+ getRowId: (attributeSet: AttributeSet) => attributeSet.id,
66
+ rowCount: count || 0,
67
+ isLoading,
68
+ pagination: {
69
+ state: pagination,
70
+ onPaginationChange: (newPagination) => {
71
+ setPagination(newPagination);
72
+ setPage(newPagination.pageIndex + 1);
73
+ },
74
+ },
75
+ search: {
76
+ state: search,
77
+ onSearchChange: setSearch,
78
+ },
79
+ onRowClick: (_event, row: AttributeSet) => {
80
+ navigate(`set/${row.id}`);
81
+ },
82
+ });
83
+
84
+ return (
85
+ <Container className="divide-y p-0">
86
+ <div className="flex items-center justify-between px-6 py-4">
87
+ <Heading level="h2">Attribute Sets</Heading>
88
+ <Button
89
+ variant="secondary"
90
+ size="small"
91
+ onClick={() => navigate("/attributes/create-set")}
92
+ >
93
+ Create
94
+ </Button>
95
+ </div>
96
+
97
+ <div>
98
+ <DataTable instance={table}>
99
+ <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
100
+ <DataTable.Search placeholder="Search attribute sets..." />
101
+ </DataTable.Toolbar>
102
+ <DataTable.Table />
103
+ <DataTable.Pagination />
104
+ </DataTable>
105
+ </div>
106
+ </Container>
107
+ );
108
+ };
@@ -0,0 +1,82 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Prompt,
4
+ } from "@medusajs/ui";
5
+ import { AdminProductCategory } from "@medusajs/types";
6
+ import MultiSelectCategory from "../create/components/MultiSelectCategory";
7
+ import { medusaClient } from "../../../lib/config";
8
+
9
+ interface CategorySelectionModalProps {
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ onConfirm: (selectedCategories: string[]) => void;
13
+ }
14
+
15
+ export const CategorySelectionModal: React.FC<CategorySelectionModalProps> = ({
16
+ open,
17
+ onOpenChange,
18
+ onConfirm,
19
+ }) => {
20
+ const [categories, setCategories] = useState<AdminProductCategory[]>([]);
21
+ const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
22
+ const [isLoading, setIsLoading] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (open) {
26
+ fetchCategories();
27
+ }
28
+ }, [open]);
29
+
30
+ const fetchCategories = async () => {
31
+ try {
32
+ setIsLoading(true);
33
+ const response = await medusaClient.admin.productCategory.list()
34
+ setCategories(response.product_categories || []);
35
+ } catch (error) {
36
+ console.error("Failed to fetch categories:", error);
37
+ } finally {
38
+ setIsLoading(false);
39
+ }
40
+ };
41
+
42
+ const handleConfirm = () => {
43
+ onConfirm(selectedCategories);
44
+ setSelectedCategories([]);
45
+ onOpenChange(false);
46
+ };
47
+
48
+ const handleCancel = () => {
49
+ setSelectedCategories([]);
50
+ onOpenChange(false);
51
+ };
52
+
53
+ return (
54
+ <Prompt open={open} variant="confirmation" onOpenChange={onOpenChange}>
55
+ <Prompt.Content>
56
+ <Prompt.Header >
57
+ <Prompt.Title className="border-ui-border-base border-b -mx-6 pl-6 pb-4 -mt-2">Select category</Prompt.Title>
58
+ <Prompt.Description className="flex flex-col gap-4 py-4">
59
+ Please select the category where this attribute applies.
60
+ <MultiSelectCategory
61
+ categories={categories}
62
+ value={selectedCategories}
63
+ onChange={setSelectedCategories}
64
+ />
65
+ </Prompt.Description>
66
+ </Prompt.Header>
67
+
68
+ <Prompt.Footer className="border-ui-border-base border-t py-4">
69
+ <Prompt.Cancel onClick={handleCancel}>
70
+ Cancel
71
+ </Prompt.Cancel>
72
+ <Prompt.Action
73
+ onClick={handleConfirm}
74
+ disabled={selectedCategories.length === 0}
75
+ >
76
+ Save
77
+ </Prompt.Action>
78
+ </Prompt.Footer>
79
+ </Prompt.Content>
80
+ </Prompt>
81
+ );
82
+ };
@@ -0,0 +1,119 @@
1
+ import { Container, Heading, Text, DataTable, createDataTableColumnHelper, DataTablePaginationState, Badge, DropdownMenu, Button } from "@medusajs/ui"
2
+ import { useState } from "react"
3
+ import { useDataTable } from "@medusajs/ui"
4
+ import { format } from "date-fns"
5
+ import { Attribute } from "../../../../types/attribute/http/attribute"
6
+ import { EllipsisHorizontal } from "@medusajs/icons"
7
+ import { useNavigate, useParams } from "react-router-dom"
8
+ import { toast } from "@medusajs/ui"
9
+
10
+ type PossibleValue = { id: string; value: string; rank: number; created_at: string }
11
+
12
+ type PossibleValuesTableProps = {
13
+ attribute: Attribute
14
+ isLoading: boolean
15
+ }
16
+
17
+ export const PossibleValuesTable = ({ attribute, isLoading }: PossibleValuesTableProps) => {
18
+ const [possibleValuesPage, setPossibleValuesPage] = useState(1)
19
+ const possibleValuesPageSize = 10
20
+ const [possibleValuesPagination, setPossibleValuesPagination] = useState<DataTablePaginationState>({
21
+ pageIndex: possibleValuesPage - 1,
22
+ pageSize: possibleValuesPageSize,
23
+ })
24
+ const [possibleValuesSearch, setPossibleValuesSearch] = useState("")
25
+
26
+ const navigate = useNavigate()
27
+ const { id: attributeId } = useParams()
28
+
29
+ const possibleValuesColumnHelper = createDataTableColumnHelper<PossibleValue>()
30
+
31
+ const possibleValuesColumns = [
32
+ possibleValuesColumnHelper.accessor("value", {
33
+ header: "Value",
34
+ cell: (info) => info.getValue(),
35
+ }),
36
+ possibleValuesColumnHelper.accessor("rank", {
37
+ header: "Rank",
38
+ cell: (info) => info.getValue(),
39
+ }),
40
+ possibleValuesColumnHelper.accessor("created_at", {
41
+ header: "Created At",
42
+ cell: (info) => format(new Date(info.getValue()), "MMM dd, yyyy p"),
43
+ }),
44
+ possibleValuesColumnHelper.display({
45
+ id: "actions",
46
+ cell: (info) => {
47
+ const possibleValue = info.row.original
48
+ return (
49
+ <div className="flex items-center justify-end">
50
+ <DropdownMenu>
51
+ <DropdownMenu.Trigger asChild>
52
+ <Button variant="transparent" size="small">
53
+ <EllipsisHorizontal />
54
+ </Button>
55
+ </DropdownMenu.Trigger>
56
+ <DropdownMenu.Content align="end">
57
+ <DropdownMenu.Item
58
+ onClick={() => {
59
+ if (attributeId) {
60
+ navigate(`edit-possible-value?possible_value_id=${possibleValue.id}`)
61
+ } else {
62
+ toast.error("Attribute ID not found.")
63
+ }
64
+ }}
65
+ >
66
+ Edit
67
+ </DropdownMenu.Item>
68
+ </DropdownMenu.Content>
69
+ </DropdownMenu>
70
+ </div>
71
+ )
72
+ },
73
+ }),
74
+ ]
75
+
76
+ const possibleValuesTable = useDataTable({
77
+ columns: possibleValuesColumns,
78
+ data: attribute?.possible_values?.filter((value) =>
79
+ value.value.toLowerCase().includes(possibleValuesSearch.toLowerCase())
80
+ ).slice(
81
+ possibleValuesPagination.pageIndex * possibleValuesPagination.pageSize,
82
+ (possibleValuesPagination.pageIndex + 1) * possibleValuesPagination.pageSize
83
+ ) || [],
84
+ getRowId: (value) => value.id,
85
+ rowCount: attribute?.possible_values?.length || 0,
86
+ pagination: {
87
+ state: possibleValuesPagination,
88
+ onPaginationChange: (newPagination) => {
89
+ setPossibleValuesPagination(newPagination)
90
+ setPossibleValuesPage(newPagination.pageIndex + 1)
91
+ },
92
+ },
93
+ search: {
94
+ state: possibleValuesSearch,
95
+ onSearchChange: setPossibleValuesSearch,
96
+ },
97
+ })
98
+
99
+ if (!attribute.possible_values || attribute.possible_values.length === 0) {
100
+ return null
101
+ }
102
+
103
+ return (
104
+ <Container className="divide-y p-0">
105
+ <div className="flex items-center justify-between px-6 py-4">
106
+ <Heading level="h2">Possible Values</Heading>
107
+ </div>
108
+ <div>
109
+ <DataTable instance={possibleValuesTable}>
110
+ <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
111
+ <DataTable.Search placeholder="Search possible values..." />
112
+ </DataTable.Toolbar>
113
+ <DataTable.Table />
114
+ <DataTable.Pagination />
115
+ </DataTable>
116
+ </div>
117
+ </Container>
118
+ )
119
+ }