@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,346 @@
1
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
2
+ import {
3
+ Container,
4
+ Heading,
5
+ Button,
6
+ DataTable,
7
+ useDataTable,
8
+ DataTablePaginationState,
9
+ Badge,
10
+ DropdownMenu,
11
+ IconButton,
12
+ } from "@medusajs/ui";
13
+ import { ListBullet, XMark, DescendingSorting } from "@medusajs/icons";
14
+ import { useState, useMemo, useEffect } from "react";
15
+ import { useNavigate } from "react-router-dom";
16
+ import { SingleColumnLayout } from "../../layouts/single-column";
17
+ import { useAttributeTableColumns } from "../../hooks/table/columns/use-attribute-table-columns";
18
+
19
+ import { useAttributes } from "../../hooks/api/attributes";
20
+ import { Attribute } from "../../../types/attribute";
21
+
22
+ const AttributesPage = () => {
23
+ const navigate = useNavigate();
24
+ const [page, setPage] = useState(1);
25
+ const pageSize = 10;
26
+
27
+ // Filter state
28
+ const [filters, setFilters] = useState<{
29
+ filterable?: boolean;
30
+ global?: boolean;
31
+ }>({});
32
+
33
+ // Sorting state
34
+ const [sorting, setSorting] = useState<{
35
+ field: 'name' | 'created_at' | 'updated_at' | null;
36
+ order: 'asc' | 'desc';
37
+ }>({
38
+ field: null,
39
+ order: 'asc'
40
+ });
41
+
42
+ // Fetch all attributes for client-side filtering and sorting
43
+ const { attributes: allAttributes, isLoading } = useAttributes({
44
+ limit: 1000, // Get all records
45
+ offset: 0,
46
+ fields: "id,name,description,handle,ui_component,is_filterable,is_required,product_categories.id,product_categories.name,possible_values.*,created_at,updated_at",
47
+ })
48
+
49
+ const { columns, modal } = useAttributeTableColumns()
50
+
51
+ const [pagination, setPagination] = useState<DataTablePaginationState>({
52
+ pageIndex: page - 1,
53
+ pageSize,
54
+ });
55
+
56
+ const [search, setSearch] = useState("");
57
+
58
+ useEffect(() => {
59
+ setPagination(prev => ({ ...prev, pageIndex: 0 }));
60
+ }, [search]);
61
+
62
+ const addFilter = (key: 'filterable' | 'global', value: boolean) => {
63
+ setFilters(prev => ({ ...prev, [key]: value }));
64
+ setPagination(prev => ({ ...prev, pageIndex: 0 }));
65
+ };
66
+
67
+ const removeFilter = (key: 'filterable' | 'global') => {
68
+ setFilters(prev => {
69
+ const newFilters = { ...prev };
70
+ delete newFilters[key];
71
+ return newFilters;
72
+ });
73
+ setPagination(prev => ({ ...prev, pageIndex: 0 }));
74
+ };
75
+
76
+ const clearAllFilters = () => {
77
+ setFilters({});
78
+ setPagination(prev => ({ ...prev, pageIndex: 0 }));
79
+ };
80
+
81
+ const handleSortFieldChange = (field: 'name' | 'created_at' | 'updated_at') => {
82
+ setSorting(prev => ({
83
+ ...prev,
84
+ field: prev.field === field ? null : field
85
+ }));
86
+ };
87
+
88
+ const handleSortOrderChange = (order: 'asc' | 'desc') => {
89
+ setSorting(prev => ({ ...prev, order }));
90
+ };
91
+
92
+ // Client-side filtering and sorting logic
93
+ const processedAttributes = useMemo(() => {
94
+ if (!allAttributes) return [];
95
+
96
+ let filtered = [...allAttributes];
97
+
98
+ if (filters.filterable !== undefined) {
99
+ filtered = filtered.filter(attr => attr.is_filterable === filters.filterable);
100
+ }
101
+
102
+ if (filters.global !== undefined) {
103
+ filtered = filtered.filter(attr => {
104
+ const isGlobal = !attr.product_categories?.length;
105
+ return isGlobal === filters.global;
106
+ });
107
+ }
108
+
109
+ if (search.trim()) {
110
+ const searchLower = search.toLowerCase();
111
+ filtered = filtered.filter(attr =>
112
+ attr.name?.toLowerCase().includes(searchLower) ||
113
+ attr.description?.toLowerCase().includes(searchLower)
114
+ );
115
+ }
116
+
117
+ if (sorting.field) {
118
+ filtered.sort((a, b) => {
119
+ let aValue: any;
120
+ let bValue: any;
121
+
122
+ switch (sorting.field) {
123
+ case 'name':
124
+ aValue = (a.name || '').toLowerCase().trim();
125
+ bValue = (b.name || '').toLowerCase().trim();
126
+ break;
127
+ case 'created_at':
128
+ aValue = (a.created_at || '').trim();
129
+ bValue = (b.created_at || '').trim();
130
+ break;
131
+ case 'updated_at':
132
+ aValue = (a.updated_at || '').trim();
133
+ bValue = (b.updated_at || '').trim();
134
+ break;
135
+ default:
136
+ return 0;
137
+ }
138
+
139
+ if (aValue < bValue) return sorting.order === 'asc' ? -1 : 1;
140
+ if (aValue > bValue) return sorting.order === 'asc' ? 1 : -1;
141
+ return 0;
142
+ });
143
+ }
144
+
145
+ return filtered;
146
+ }, [allAttributes, filters, search, sorting]);
147
+
148
+ // Pagination logic
149
+ const paginatedAttributes = useMemo(() => {
150
+ const startIndex = pagination.pageIndex * pagination.pageSize;
151
+ const endIndex = startIndex + pagination.pageSize;
152
+ return processedAttributes.slice(startIndex, endIndex);
153
+ }, [processedAttributes, pagination]);
154
+
155
+ const table = useDataTable({
156
+ columns,
157
+ data: paginatedAttributes || [],
158
+ getRowId: (attribute: Attribute) => attribute.id,
159
+ rowCount: processedAttributes.length,
160
+ isLoading,
161
+ pagination: {
162
+ state: pagination,
163
+ onPaginationChange: (newPagination) => {
164
+ setPagination(newPagination);
165
+ setPage(newPagination.pageIndex + 1);
166
+ },
167
+ },
168
+ search: {
169
+ state: search,
170
+ onSearchChange: setSearch,
171
+ },
172
+ onRowClick: (_event, row: Attribute) => {
173
+ navigate(`/attributes/${row.id}`);
174
+ },
175
+ });
176
+
177
+ return (
178
+ <SingleColumnLayout>
179
+ <Container className="divide-y p-0">
180
+ <div className="flex items-center justify-between px-6 py-4">
181
+ <Heading level="h2">Product Attributes</Heading>
182
+ <Button
183
+ variant="primary"
184
+ size="small"
185
+ onClick={() => navigate("/attributes/create")}
186
+ >
187
+ Create
188
+ </Button>
189
+ </div>
190
+
191
+ <div>
192
+ <DataTable instance={table}>
193
+ <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
194
+ <div className="flex flex-wrap items-center gap-2">
195
+ {/* Active Filters */}
196
+ {filters.filterable !== undefined && (
197
+ <Badge size="small" className="flex items-center gap-1 bg-ui-bg-subtle text-ui-fg-subtle">
198
+ Filterable
199
+ <DropdownMenu>
200
+ <DropdownMenu.Trigger asChild>
201
+ <button className="hover:bg-ui-bg-subtle-hover px-2 h-7 border border-ui-border-base">
202
+ {filters.filterable ? "Yes" : "No"}
203
+ </button>
204
+ </DropdownMenu.Trigger>
205
+ <DropdownMenu.Content align="start">
206
+ <DropdownMenu.Item onClick={() => addFilter('filterable', true)}>
207
+ {filters.filterable === true ? <span className="mr-2">•</span> : <span className="ml-4" />}
208
+ Yes
209
+ </DropdownMenu.Item>
210
+ <DropdownMenu.Item onClick={() => addFilter('filterable', false)}>
211
+ {filters.filterable === false ? <span className="mr-2">•</span> : <span className="ml-4" />}
212
+ No
213
+ </DropdownMenu.Item>
214
+ </DropdownMenu.Content>
215
+ </DropdownMenu>
216
+ <button
217
+ onClick={() => removeFilter('filterable')}
218
+ className="hover:bg-ui-bg-subtle-hover px-2 h-7 border border-ui-border-base bg-ui-bg-subtle flex items-center justify-center rounded-e-md border-x-0 -ml-1 -mr-2"
219
+ >
220
+ <XMark />
221
+ </button>
222
+ </Badge>
223
+ )}
224
+ {filters.global !== undefined && (
225
+ <Badge size="small" className="flex items-center gap-1 bg-ui-bg-subtle text-ui-fg-subtle">
226
+ Global
227
+ <DropdownMenu>
228
+ <DropdownMenu.Trigger asChild>
229
+ <button className="hover:bg-ui-bg-subtle-hover px-2 h-7 border border-ui-border-base">
230
+ {filters.global ? "Yes" : "No"}
231
+ </button>
232
+ </DropdownMenu.Trigger>
233
+ <DropdownMenu.Content align="start">
234
+ <DropdownMenu.Item onClick={() => addFilter('global', true)}>
235
+ {filters.global === true ? <span className="mr-2">•</span> : <span className="ml-4" />}
236
+ Yes
237
+ </DropdownMenu.Item>
238
+ <DropdownMenu.Item onClick={() => addFilter('global', false)}>
239
+ {filters.global === false ? <span className="mr-2">•</span> : <span className="ml-4" />}
240
+ No
241
+ </DropdownMenu.Item>
242
+ </DropdownMenu.Content>
243
+ </DropdownMenu>
244
+ <button
245
+ onClick={() => removeFilter('global')}
246
+ className="hover:bg-ui-bg-subtle-hover px-2 h-7 border border-ui-border-base bg-ui-bg-subtle flex items-center justify-center rounded-e-md border-x-0 -ml-1 -mr-2"
247
+ >
248
+ <XMark />
249
+ </button>
250
+ </Badge>
251
+ )}
252
+
253
+ {/* Add Filter Button */}
254
+ <DropdownMenu>
255
+ <DropdownMenu.Trigger asChild disabled={filters.filterable !== undefined && filters.global !== undefined}>
256
+ <Button
257
+ variant="secondary"
258
+ size="small"
259
+ disabled={filters.filterable !== undefined && filters.global !== undefined}
260
+ >
261
+ Add filter
262
+ </Button>
263
+ </DropdownMenu.Trigger>
264
+ <DropdownMenu.Content align="start">
265
+ {filters.filterable === undefined && (
266
+ <DropdownMenu.Item onClick={() => addFilter('filterable', true)}>
267
+ Filterable
268
+ </DropdownMenu.Item>
269
+ )}
270
+ {filters.global === undefined && (
271
+ <DropdownMenu.Item onClick={() => addFilter('global', true)}>
272
+ Global
273
+ </DropdownMenu.Item>
274
+ )}
275
+ </DropdownMenu.Content>
276
+ </DropdownMenu>
277
+
278
+ {/* Clear All Button */}
279
+ {Object.keys(filters).length > 0 && (
280
+ <Button
281
+ className="text-ui-fg-muted hover:text-ui-fg-subtle"
282
+ variant="transparent"
283
+ size="small"
284
+ onClick={clearAllFilters}
285
+ >
286
+ Clear all
287
+ </Button>
288
+ )}
289
+ </div>
290
+
291
+ <div className="flex items-center gap-2">
292
+ <DataTable.Search placeholder="Search table" />
293
+
294
+ {/* Sorting Dropdown */}
295
+ <DropdownMenu>
296
+ <DropdownMenu.Trigger asChild>
297
+ <IconButton size="small">
298
+ <DescendingSorting />
299
+ </IconButton>
300
+ </DropdownMenu.Trigger>
301
+ <DropdownMenu.Content align="end">
302
+ <div className="px-2 py-1">
303
+ <DropdownMenu.Item onClick={() => handleSortFieldChange('name')}>
304
+ {sorting.field === 'name' ? <span className="mr-2">•</span> : <span className="ml-4" />}
305
+ Name
306
+ </DropdownMenu.Item>
307
+ <DropdownMenu.Item onClick={() => handleSortFieldChange('created_at')}>
308
+ {sorting.field === 'created_at' ? <span className="mr-2">•</span> : <span className="ml-4" />}
309
+ Created At
310
+ </DropdownMenu.Item>
311
+ <DropdownMenu.Item onClick={() => handleSortFieldChange('updated_at')}>
312
+ {sorting.field === 'updated_at' ? <span className="mr-2">•</span> : <span className="ml-4" />}
313
+ Updated At
314
+ </DropdownMenu.Item>
315
+ </div>
316
+ <DropdownMenu.Separator />
317
+ <div className="px-2 py-1">
318
+ <DropdownMenu.Item onClick={() => handleSortOrderChange('asc')}>
319
+ {sorting.order === 'asc' ? <span className="mr-2">•</span> : <span className="ml-4" />}
320
+ Ascending (1 → 30)
321
+ </DropdownMenu.Item>
322
+ <DropdownMenu.Item onClick={() => handleSortOrderChange('desc')}>
323
+ {sorting.order === 'desc' ? <span className="mr-2">•</span> : <span className="ml-4" />}
324
+ Descending (30 → 1)
325
+ </DropdownMenu.Item>
326
+ </div>
327
+ </DropdownMenu.Content>
328
+ </DropdownMenu>
329
+ </div>
330
+ </DataTable.Toolbar>
331
+ <DataTable.Table />
332
+ <DataTable.Pagination />
333
+ </DataTable>
334
+ </div>
335
+ </Container>
336
+ {modal}
337
+ </SingleColumnLayout>
338
+ );
339
+ };
340
+
341
+ export const config = defineRouteConfig({
342
+ label: "Attributes",
343
+ icon: ListBullet,
344
+ });
345
+
346
+ export default AttributesPage;
@@ -0,0 +1,35 @@
1
+ import { useParams } from 'react-router-dom'
2
+ import { AttributeSetAttributesForm } from '../components/attribute-set-attributes-form'
3
+ import { useAttributes } from '../../../../../hooks/api/attributes'
4
+ import { defineRouteConfig } from '@medusajs/admin-sdk'
5
+
6
+ const AttributeSetAttributesPage = () => {
7
+ const { id } = useParams()
8
+
9
+ if (!id) {
10
+ return (
11
+ <p>Something went wrong...</p>
12
+ )
13
+ }
14
+
15
+ const { attributes, isLoading } = useAttributes({
16
+ sets: {
17
+ id
18
+ }
19
+ })
20
+
21
+ if (isLoading) {
22
+ return (
23
+ <p>Loading...</p>
24
+ )
25
+ }
26
+
27
+ return (
28
+ <AttributeSetAttributesForm attributeSetId={id} attributes={attributes || []}/>
29
+ )
30
+ }
31
+
32
+ export const config = defineRouteConfig({});
33
+
34
+ export default AttributeSetAttributesPage
35
+
@@ -0,0 +1,114 @@
1
+ import { useMemo, useState } from "react";
2
+ import { Attribute, AttributeSet } from "../../../../../../types/attribute";
3
+ import { useAttributes } from "../../../../../hooks/api/attributes";
4
+ import {
5
+ Container,
6
+ createDataTableColumnHelper,
7
+ createDataTableCommandHelper,
8
+ DataTable,
9
+ DataTableRowSelectionState,
10
+ DropdownMenu,
11
+ Heading,
12
+ IconButton,
13
+ toast,
14
+ useDataTable,
15
+ } from "@medusajs/ui";
16
+ import { useAttributeTableColumns } from "../../../../../hooks/table/columns";
17
+ import { EllipsisHorizontal, Plus } from "@medusajs/icons";
18
+ import { useNavigate } from 'react-router-dom'
19
+ import { useBatchAttributesToSets } from "../../../../../hooks/api/attribute-set";
20
+
21
+ type Props = {
22
+ attributeSet: AttributeSet;
23
+ };
24
+
25
+ export const AttributeSetAttributesSection = ({ attributeSet }: Props) => {
26
+ const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
27
+ {}
28
+ );
29
+ const navigate = useNavigate()
30
+
31
+ const { attributes, count, isLoading } = useAttributes({
32
+ sets: {
33
+ id: attributeSet.id,
34
+ },
35
+ fields: 'id, name, handle, product_categories.id, possible_values.value',
36
+ });
37
+
38
+ console.log('Received attrs in attribute set detail -> ', attributes)
39
+
40
+ const { mutateAsync } = useBatchAttributesToSets(attributeSet.id)
41
+ const columns = useColumns();
42
+ const commands = useCommands((selection: DataTableRowSelectionState) =>
43
+ mutateAsync({
44
+ remove: Object.keys(selection)
45
+ }, {
46
+ onSuccess: () => {
47
+ toast.success('Removed attributes from set')
48
+ },
49
+ onError: (error) => {
50
+ toast.error(error.message)
51
+ }
52
+ })
53
+ );
54
+
55
+ const table = useDataTable({
56
+ data: attributes || [],
57
+ columns,
58
+ getRowId: (product) => product.id,
59
+ rowCount: count,
60
+ isLoading,
61
+ commands,
62
+ rowSelection: {
63
+ state: rowSelection,
64
+ onRowSelectionChange: setRowSelection,
65
+ },
66
+ });
67
+
68
+ return (
69
+ <Container className="divide-y p-0">
70
+ <DataTable instance={table}>
71
+ <DataTable.Toolbar className="flex justify-between items-center">
72
+ <Heading>Attributes</Heading>
73
+ <DropdownMenu>
74
+ <DropdownMenu.Trigger>
75
+ <IconButton variant="transparent">
76
+ <EllipsisHorizontal />
77
+ </IconButton>
78
+ </DropdownMenu.Trigger>
79
+ <DropdownMenu.Content>
80
+ <DropdownMenu.Item onClick={() => navigate(`/attributes/set/${attributeSet.id}/attributes`)}>
81
+ <Plus className="mr-2" />
82
+ Add
83
+ </DropdownMenu.Item>
84
+ </DropdownMenu.Content>
85
+ </DropdownMenu>
86
+ </DataTable.Toolbar>
87
+ <DataTable.Table />
88
+ <DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
89
+ </DataTable>
90
+ </Container>
91
+ );
92
+ };
93
+
94
+ const columnHelper = createDataTableColumnHelper<Attribute>();
95
+
96
+ const useColumns = () => {
97
+ const base = useAttributeTableColumns();
98
+
99
+ return useMemo(() => [columnHelper.select(), ...base], [base]);
100
+ };
101
+
102
+ const commandHelper = createDataTableCommandHelper();
103
+
104
+ const useCommands = (onDelete: (selection: DataTableRowSelectionState) => Promise<unknown>) => {
105
+ return [
106
+ commandHelper.command({
107
+ label: "Eliminar",
108
+ shortcut: "D",
109
+ action: async (selection) => {
110
+ await onDelete(selection)
111
+ },
112
+ }),
113
+ ];
114
+ };
@@ -0,0 +1,42 @@
1
+ import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons";
2
+ import { Container, Heading, DropdownMenu, IconButton } from "@medusajs/ui";
3
+ import { AttributeSet } from "../../../../../../types/attribute";
4
+ import { SectionRow } from "../../../../../components/section-row";
5
+ import { useNavigate } from "react-router-dom"
6
+
7
+ type Props = {
8
+ attributeSet: AttributeSet;
9
+ };
10
+
11
+ export const AttributeSetGeneralSection = ({ attributeSet }: Props) => {
12
+ const navigate = useNavigate()
13
+
14
+ return (
15
+ <Container className="divide-y p-0">
16
+ <div className="flex items-center justify-between px-6 py-4">
17
+ <Heading level="h1">{attributeSet.name}</Heading>
18
+ <DropdownMenu>
19
+ <DropdownMenu.Trigger asChild>
20
+ <IconButton variant="transparent">
21
+ <EllipsisHorizontal />
22
+ </IconButton>
23
+ </DropdownMenu.Trigger>
24
+
25
+ <DropdownMenu.Content>
26
+ <DropdownMenu.Item onClick={() => navigate(`/attributes/set/${attributeSet.id}/edit`)}>
27
+ <PencilSquare className="mr-2" />
28
+ Edit
29
+ </DropdownMenu.Item>
30
+ <DropdownMenu.Item>
31
+ <Trash className="mr-2" />
32
+ Delete
33
+ </DropdownMenu.Item>
34
+ </DropdownMenu.Content>
35
+ </DropdownMenu>
36
+ </div>
37
+ <SectionRow title="Name" value={attributeSet.name} />
38
+ <SectionRow title="Handle" value={attributeSet.handle} />
39
+ <SectionRow title="Description" value={attributeSet.description} />
40
+ </Container>
41
+ );
42
+ };
@@ -0,0 +1,143 @@
1
+ import { useMemo, useState } from "react";
2
+ import { Attribute, AttributeSet } from "../../../../../../types/attribute";
3
+ import {
4
+ Button,
5
+ createDataTableColumnHelper,
6
+ DataTable,
7
+ DataTableRowSelectionState,
8
+ FocusModal,
9
+ toast,
10
+ useDataTable,
11
+ } from "@medusajs/ui";
12
+ import { useAttributeTableColumns } from "../../../../../hooks/table/columns";
13
+ import { useForm } from "react-hook-form";
14
+ import { zodResolver } from "@hookform/resolvers/zod";
15
+ import { z } from "zod";
16
+ import { useAttributes } from "../../../../../hooks/api/attributes";
17
+ import { useNavigate } from "react-router-dom";
18
+ import { useBatchAttributesToSets } from "../../../../../hooks/api/attribute-set";
19
+
20
+ type Props = {
21
+ attributeSetId: string;
22
+ attributes: Attribute[];
23
+ };
24
+
25
+ const FormSchema = z.object({
26
+ attribute_ids: z.array(z.string()),
27
+ });
28
+ type FormValues = z.infer<typeof FormSchema>;
29
+
30
+ export const AttributeSetAttributesForm = ({
31
+ attributeSetId,
32
+ attributes = [],
33
+ }: Props) => {
34
+ const [selection, setSelection] = useState<DataTableRowSelectionState>(
35
+ attributes.reduce((acc, attr) => {
36
+ acc[attr.id] = true;
37
+ return acc;
38
+ }, {} as DataTableRowSelectionState)
39
+ );
40
+ console.log('selection -> ', selection)
41
+ const navigate = useNavigate();
42
+
43
+ const columns = useColumns();
44
+ const form = useForm<FormValues>({
45
+ resolver: zodResolver(FormSchema),
46
+ defaultValues: {
47
+ attribute_ids: [],
48
+ },
49
+ });
50
+
51
+ const { mutateAsync, isPending } = useBatchAttributesToSets(attributeSetId);
52
+
53
+ const handleSelectionChange = (selection: DataTableRowSelectionState) => {
54
+ form.setValue("attribute_ids", Object.keys(selection), {
55
+ shouldDirty: true,
56
+ shouldTouch: true,
57
+ });
58
+
59
+ setSelection(selection);
60
+ };
61
+
62
+ const handleSubmit = form.handleSubmit(async (data: FormValues) => {
63
+ await mutateAsync(
64
+ {
65
+ add: data.attribute_ids,
66
+ },
67
+ {
68
+ onSuccess: () => {
69
+ toast.success("Linked attributes to set");
70
+ navigate(-1);
71
+ },
72
+ onError: (error) => {
73
+ toast.error(error.message);
74
+ },
75
+ }
76
+ );
77
+ });
78
+
79
+ const handleClose = () => {
80
+ navigate(-1);
81
+ };
82
+
83
+ const { attributes: data, count, isLoading } = useAttributes();
84
+
85
+ const table = useDataTable({
86
+ data: data || [],
87
+ columns,
88
+ getRowId: (attribute) => attribute.id,
89
+ rowCount: count,
90
+ isLoading,
91
+ rowSelection: {
92
+ state: selection,
93
+ onRowSelectionChange: handleSelectionChange,
94
+ },
95
+ });
96
+
97
+ return (
98
+ <FocusModal
99
+ open={true}
100
+ onOpenChange={(open) => {
101
+ if (!open) handleClose();
102
+ }}
103
+ >
104
+ <FocusModal.Content>
105
+ <FocusModal.Header>
106
+ <div>
107
+ <Button
108
+ className="mr-2"
109
+ type="button"
110
+ size="small"
111
+ disabled={isPending}
112
+ >
113
+ Cancel
114
+ </Button>
115
+ <Button
116
+ type="submit"
117
+ form="attribute-set-attributes-form"
118
+ variant="secondary"
119
+ disabled={isPending}
120
+ >
121
+ Save
122
+ </Button>
123
+ </div>
124
+ </FocusModal.Header>
125
+ <FocusModal.Body>
126
+ <form id='attribute-set-attributes-form' onSubmit={handleSubmit}>
127
+ <DataTable instance={table}>
128
+ <DataTable.Table />
129
+ </DataTable>
130
+ </form>
131
+ </FocusModal.Body>
132
+ </FocusModal.Content>
133
+ </FocusModal>
134
+ );
135
+ };
136
+
137
+ const columnHelper = createDataTableColumnHelper<Attribute>();
138
+
139
+ const useColumns = () => {
140
+ const base = useAttributeTableColumns();
141
+
142
+ return useMemo(() => [columnHelper.select(), ...base], [base]);
143
+ };
@@ -0,0 +1,2 @@
1
+ export * from './AttributeSetAttributesSection'
2
+ export * from './AttributeSetGeneralSection'