@haus-tech/badge-plugin 4.0.5 → 4.0.6

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 4.0.6-0
2
+
3
+ ### 🚀 Features
4
+
5
+ - **badge-plugin:** add dashboard integration and enhance badge management ([2603563](https://github.com/WeAreHausTech/haus-tech-vendure-plugins/commit/2603563))
6
+
7
+ ## 4.0.5
8
+
9
+ This was a version bump only for badge-plugin to align it with other projects, there were no code changes.
10
+
1
11
  ## 4.0.4
2
12
 
3
13
  This was a version bump only for badge-plugin to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
  name: badge-plugin
3
3
  title: Badge Plugin
4
4
  description: Vendure plugin that allows you to manage and display badges for products in your e-commerce store.
5
- version: 4.0.3
5
+ version: 4.0.6-0
6
+ tags: [vendure, plugin, badge]
6
7
  ---
7
8
 
8
9
  # Badge Plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/badge-plugin",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "description": "Adds a badge to product images",
5
5
  "author": "Haus Tech",
6
6
  "repository": "https://github.com/WeAreHausTech/haus-tech-vendure-plugins",
@@ -8,6 +8,7 @@ export declare class BadgeAdminResolver {
8
8
  private badgeService;
9
9
  constructor(config: BadgePluginOptions, badgeService: BadgeService);
10
10
  badges(ctx: RequestContext, args: any): Promise<PaginatedList<Badge>>;
11
+ badge(ctx: RequestContext, id: string): Promise<Badge | null>;
11
12
  createBadge(ctx: RequestContext, input: CreateBadgeInput): Promise<Badge>;
12
13
  updateBadge(ctx: RequestContext, input: UpdateBadgeInput): Promise<Badge | ErrorResult>;
13
14
  deleteBadge(ctx: RequestContext, ids: string[]): Promise<DeletionResponse>;
@@ -29,6 +29,9 @@ let BadgeAdminResolver = class BadgeAdminResolver {
29
29
  async badges(ctx, args) {
30
30
  return this.badgeService.findAll(ctx, args.options || undefined);
31
31
  }
32
+ async badge(ctx, id) {
33
+ return this.badgeService.findOne(ctx, parseInt(id, 10));
34
+ }
32
35
  async createBadge(ctx, input) {
33
36
  return this.badgeService.create(ctx, input);
34
37
  }
@@ -62,6 +65,14 @@ __decorate([
62
65
  __metadata("design:paramtypes", [core_1.RequestContext, Object]),
63
66
  __metadata("design:returntype", Promise)
64
67
  ], BadgeAdminResolver.prototype, "badges", null);
68
+ __decorate([
69
+ (0, graphql_1.Query)(),
70
+ __param(0, (0, core_1.Ctx)()),
71
+ __param(1, (0, graphql_1.Args)('id')),
72
+ __metadata("design:type", Function),
73
+ __metadata("design:paramtypes", [core_1.RequestContext, String]),
74
+ __metadata("design:returntype", Promise)
75
+ ], BadgeAdminResolver.prototype, "badge", null);
65
76
  __decorate([
66
77
  (0, graphql_1.Mutation)(),
67
78
  __param(0, (0, core_1.Ctx)()),
@@ -1 +1 @@
1
- {"version":3,"file":"admin.resolver.js","sourceRoot":"","sources":["../../../../../packages/badge-plugin/src/api/admin.resolver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6CAAiE;AACjE,wCAAuF;AACvF,4DAAuD;AAEvD,gDAKyB;AACzB,4CAA6D;AAC7D,2CAAuC;AAIhC,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEU;IAC7B;IAFV,YACuC,MAA0B,EACvD,YAA0B;QADG,WAAM,GAAN,MAAM,CAAoB;QACvD,iBAAY,GAAZ,YAAY,CAAc;IACjC,CAAC;IAGE,AAAN,KAAK,CAAC,MAAM,CAAQ,GAAmB,EAAU,IAAS;QACxD,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,CAAC,CAAA;IAClE,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CACR,GAAmB,EACX,KAAuB;QAEtC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC7C,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CACR,GAAmB,EACX,KAAuB;QAEtC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC7C,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CACR,GAAmB,EACb,GAAa;QAE1B,IAAI,CAAC;YACH,KAAK,MAAM,OAAO,IAAI,GAAG,EAAE,CAAC;gBAC1B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;gBACzE,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;gBAC5C,CAAC;YACH,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,0BAAc,CAAC,OAAO,EAAE,CAAA;QAC3C,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,EAAE,MAAM,EAAE,0BAAc,CAAC,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;QACnE,CAAC;IACH,CAAC;IAGD,oBAAoB;QAClB,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;CACF,CAAA;AAjDY,gDAAkB;AAOvB;IADL,IAAA,eAAK,GAAE;IACM,WAAA,IAAA,UAAG,GAAE,CAAA;IAAuB,WAAA,IAAA,cAAI,GAAE,CAAA;;qCAAvB,qBAAc;;gDAEtC;AAGK;IADL,IAAA,kBAAQ,GAAE;IAER,WAAA,IAAA,UAAG,GAAE,CAAA;IACL,WAAA,IAAA,cAAI,EAAC,OAAO,CAAC,CAAA;;qCADF,qBAAc;;qDAI3B;AAGK;IADL,IAAA,kBAAQ,GAAE;IAER,WAAA,IAAA,UAAG,GAAE,CAAA;IACL,WAAA,IAAA,cAAI,EAAC,OAAO,CAAC,CAAA;;qCADF,qBAAc;;qDAI3B;AAGK;IADL,IAAA,kBAAQ,GAAE;IAER,WAAA,IAAA,UAAG,GAAE,CAAA;IACL,WAAA,IAAA,cAAI,EAAC,KAAK,CAAC,CAAA;;qCADA,qBAAc;;qDAc3B;AAGD;IADC,IAAA,eAAK,GAAE;;;;8DAGP;6BAhDU,kBAAkB;IAD9B,IAAA,kBAAQ,GAAE;IAGN,WAAA,IAAA,eAAM,EAAC,+BAAmB,CAAC,CAAA;6CACN,4BAAY;GAHzB,kBAAkB,CAiD9B"}
1
+ {"version":3,"file":"admin.resolver.js","sourceRoot":"","sources":["../../../../../packages/badge-plugin/src/api/admin.resolver.ts"],"names":[],"mappings":"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"}
@@ -24,11 +24,13 @@ exports.adminApiExtensions = (0, graphql_tag_1.gql) `
24
24
 
25
25
  extend type Query {
26
26
  badges(options: BadgeListOptions): BadgeList!
27
+ badge(id: ID!): Badge
27
28
  }
28
29
 
29
30
  input CreateBadgeInput {
30
31
  assetId: ID!
31
32
  position: String
33
+ collectionId: ID
32
34
  }
33
35
 
34
36
  extend type Mutation {
@@ -43,6 +45,7 @@ exports.adminApiExtensions = (0, graphql_tag_1.gql) `
43
45
  id: ID!
44
46
  collectionId: ID
45
47
  position: String
48
+ assetId: ID
46
49
  }
47
50
 
48
51
  extend type Mutation {
@@ -1 +1 @@
1
- {"version":3,"file":"api-extensions.js","sourceRoot":"","sources":["../../../../../packages/badge-plugin/src/api/api-extensions.ts"],"names":[],"mappings":";;;AAAA,6CAAiC;AAEpB,QAAA,kBAAkB,GAAG,IAAA,iBAAG,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDpC,CAAA;AAEY,QAAA,iBAAiB,GAAG,IAAA,iBAAG,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CnC,CAAA"}
1
+ {"version":3,"file":"api-extensions.js","sourceRoot":"","sources":["../../../../../packages/badge-plugin/src/api/api-extensions.ts"],"names":[],"mappings":";;;AAAA,6CAAiC;AAEpB,QAAA,kBAAkB,GAAG,IAAA,iBAAG,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDpC,CAAA;AAEY,QAAA,iBAAiB,GAAG,IAAA,iBAAG,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CnC,CAAA"}
@@ -75,6 +75,7 @@ exports.BadgePlugin = BadgePlugin = BadgePlugin_1 = __decorate([
75
75
  resolvers: [admin_resolver_1.BadgeAdminResolver],
76
76
  },
77
77
  entities: [badge_entity_1.Badge],
78
+ dashboard: './dashboard/index.tsx',
78
79
  compatibility: '^3.0.0',
79
80
  })
80
81
  ], BadgePlugin);
@@ -1 +1 @@
1
- {"version":3,"file":"badge.plugin.js","sourceRoot":"","sources":["../../../../packages/badge-plugin/src/badge.plugin.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wCAAiE;AACjE,yDAA4E;AAC5E,yDAAyD;AAEzD,2CAAiD;AACjD,wDAA6C;AAE7C,+BAA2B;AAC3B,2DAAsD;AACtD,uDAK4B;AA+BrB,IAAM,WAAW,GAAjB,MAAM,WAAW;;IACtB,MAAM,CAAC,OAAO,GAAuB;QACnC,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,CAAC;KAC7E,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,OAA4B;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACxB,CAAC;QACD,OAAO,aAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,EAAE,GAAqB;QAC5B,aAAa,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,CAAC;QACpC,YAAY,EAAE;YACZ,EAAE,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,yBAAyB,CAAC;YAC9C,EAAE,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,yBAAyB,CAAC;SAC/C;QACD,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QACpD,SAAS,EAAE,CAAC,cAAc,CAAC;QAC3B,iBAAiB;QACjB,QAAQ;QACR,sBAAsB;QACtB,yBAAyB;QACzB,6CAA6C;QAC7C,qCAAqC;QACrC,SAAS;QACT,QAAQ;QACR,wBAAwB;QACxB,iDAAiD;QACjD,yCAAyC;QACzC,SAAS;QACT,OAAO;KACR,CAAA;;AAjCU,kCAAW;sBAAX,WAAW;IAzBvB,IAAA,oBAAa,EAAC;QACb,OAAO,EAAE,CAAC,yBAAkB,CAAC;QAC7B,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,+BAAmB;gBAC5B,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO;aACtC;YACD,4BAAY;SACb;QACD,iBAAiB,EAAE;YACjB,MAAM,EAAE,kCAAiB;YACzB,SAAS,EAAE;gBACT,iCAAiB;gBACjB,qCAAqB;gBACrB,0CAA0B;gBAC1B,4CAA4B;aAC7B;SACF;QACD,kBAAkB,EAAE;YAClB,MAAM,EAAE,mCAAkB;YAC1B,SAAS,EAAE,CAAC,mCAAkB,CAAC;SAChC;QACD,QAAQ,EAAE,CAAC,oBAAK,CAAC;QACjB,aAAa,EAAE,QAAQ;KACxB,CAAC;GACW,WAAW,CAkCvB"}
1
+ {"version":3,"file":"badge.plugin.js","sourceRoot":"","sources":["../../../../packages/badge-plugin/src/badge.plugin.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wCAAiE;AACjE,yDAA4E;AAC5E,yDAAyD;AAEzD,2CAAiD;AACjD,wDAA6C;AAE7C,+BAA2B;AAC3B,2DAAsD;AACtD,uDAK4B;AAiCrB,IAAM,WAAW,GAAjB,MAAM,WAAW;;IACtB,MAAM,CAAC,OAAO,GAAuB;QACnC,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,CAAC;KAC7E,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,OAA4B;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACxB,CAAC;QACD,OAAO,aAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,EAAE,GAAqB;QAC5B,aAAa,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,CAAC;QACpC,YAAY,EAAE;YACZ,EAAE,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,yBAAyB,CAAC;YAC9C,EAAE,EAAE,IAAA,WAAI,EAAC,SAAS,EAAE,yBAAyB,CAAC;SAC/C;QACD,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QACpD,SAAS,EAAE,CAAC,cAAc,CAAC;QAC3B,iBAAiB;QACjB,QAAQ;QACR,sBAAsB;QACtB,yBAAyB;QACzB,6CAA6C;QAC7C,qCAAqC;QACrC,SAAS;QACT,QAAQ;QACR,wBAAwB;QACxB,iDAAiD;QACjD,yCAAyC;QACzC,SAAS;QACT,OAAO;KACR,CAAA;;AAjCU,kCAAW;sBAAX,WAAW;IA3BvB,IAAA,oBAAa,EAAC;QACb,OAAO,EAAE,CAAC,yBAAkB,CAAC;QAC7B,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,+BAAmB;gBAC5B,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO;aACtC;YACD,4BAAY;SACb;QACD,iBAAiB,EAAE;YACjB,MAAM,EAAE,kCAAiB;YACzB,SAAS,EAAE;gBACT,iCAAiB;gBACjB,qCAAqB;gBACrB,0CAA0B;gBAC1B,4CAA4B;aAC7B;SACF;QACD,kBAAkB,EAAE;YAClB,MAAM,EAAE,mCAAkB;YAC1B,SAAS,EAAE,CAAC,mCAAkB,CAAC;SAChC;QACD,QAAQ,EAAE,CAAC,oBAAK,CAAC;QACjB,SAAS,EAAE,uBAAuB;QAElC,aAAa,EAAE,QAAQ;KACxB,CAAC;GACW,WAAW,CAkCvB"}
@@ -0,0 +1,470 @@
1
+ import {
2
+ DashboardRouteDefinition,
3
+ detailPageRouteLoader,
4
+ useDetailPage,
5
+ Page,
6
+ PageTitle,
7
+ PageActionBar,
8
+ PageActionBarRight,
9
+ PageLayout,
10
+ PageBlock,
11
+ FormFieldWrapper,
12
+ Button,
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ Label,
19
+ VendureImage,
20
+ } from '@vendure/dashboard'
21
+ import { AnyRoute, useNavigate } from '@tanstack/react-router'
22
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
23
+ import { useCallback, useState, useRef, useEffect } from 'react'
24
+ import { toast } from 'sonner'
25
+ import { api } from '@vendure/dashboard'
26
+ import { graphql } from '@/gql'
27
+ import { UploadIcon } from 'lucide-react'
28
+ import type { ComponentProps } from 'react'
29
+
30
+ const getBadgeDetailDocument = graphql(`
31
+ query GetBadgeDetail($id: ID!) {
32
+ badge(id: $id) {
33
+ id
34
+ createdAt
35
+ updatedAt
36
+ collection {
37
+ id
38
+ name
39
+ }
40
+ collectionId
41
+ position
42
+ assetId
43
+ asset {
44
+ id
45
+ name
46
+ type
47
+ mimeType
48
+ width
49
+ height
50
+ fileSize
51
+ source
52
+ preview
53
+ }
54
+ }
55
+ }
56
+ `)
57
+
58
+ const createBadgeDocument = graphql(`
59
+ mutation CreateBadge($input: CreateBadgeInput!) {
60
+ createBadge(input: $input) {
61
+ id
62
+ }
63
+ }
64
+ `)
65
+
66
+ const updateBadgeDocument = graphql(`
67
+ mutation UpdateBadge($input: UpdateBadgeInput!) {
68
+ updateBadge(input: $input) {
69
+ id
70
+ }
71
+ }
72
+ `)
73
+
74
+ const getBadgePluginConfigDocument = graphql(`
75
+ query GetBadgePluginConfig {
76
+ getBadgePluginConfig {
77
+ availablePositions
78
+ }
79
+ }
80
+ `)
81
+
82
+ const getCollectionsDocument = graphql(`
83
+ query GetCollections {
84
+ collections {
85
+ items {
86
+ id
87
+ name
88
+ slug
89
+ }
90
+ }
91
+ }
92
+ `)
93
+
94
+ const createAssetsDocument = graphql(`
95
+ mutation CreateAssets($input: [CreateAssetInput!]!) {
96
+ createAssets(input: $input) {
97
+ ... on Asset {
98
+ id
99
+ name
100
+ source
101
+ preview
102
+ }
103
+ ... on MimeTypeError {
104
+ message
105
+ }
106
+ }
107
+ }
108
+ `)
109
+
110
+ export const badgeDetailRoute: DashboardRouteDefinition = {
111
+ path: '/badges/$id',
112
+ loader: detailPageRouteLoader({
113
+ queryDocument: getBadgeDetailDocument,
114
+ breadcrumb: (isNew, entity) => [
115
+ { path: '/badges', label: 'Badges' },
116
+ isNew ? 'New Badge' : `Badge ${entity?.id || ''}`,
117
+ ],
118
+ }),
119
+ component: (route) => {
120
+ return <BadgeDetailPage route={route as any} />
121
+ },
122
+ }
123
+
124
+ function BadgeDetailPage({ route }: { route: AnyRoute }) {
125
+ const params = route.useParams()
126
+ const navigate = useNavigate()
127
+ const queryClient = useQueryClient()
128
+ const creatingNewEntity = params.id === 'new'
129
+ const fileInputRef = useRef<HTMLInputElement>(null)
130
+ const [uploadedAsset, setUploadedAsset] = useState<{
131
+ id: string
132
+ name: string
133
+ preview: string
134
+ source: string
135
+ } | null>(null)
136
+ const [uploading, setUploading] = useState(false)
137
+
138
+ const { data: configData } = useQuery({
139
+ queryKey: ['badge-plugin-config'],
140
+ queryFn: () => api.query(getBadgePluginConfigDocument),
141
+ })
142
+
143
+ const { data: collectionsData } = useQuery({
144
+ queryKey: ['collections'],
145
+ queryFn: () => api.query(getCollectionsDocument),
146
+ })
147
+
148
+ const availablePositions = configData?.getBadgePluginConfig?.availablePositions || []
149
+
150
+ const collections = collectionsData?.collections?.items || []
151
+
152
+ const setValuesForUpdate = useCallback(
153
+ (
154
+ badge:
155
+ | {
156
+ id: string
157
+ position: string
158
+ collectionId: string | null
159
+ assetId: string
160
+ asset: {
161
+ id: string
162
+ name: string
163
+ preview: string
164
+ }
165
+ }
166
+ | null
167
+ | undefined,
168
+ ) => {
169
+ const position =
170
+ badge?.position && badge.position.trim() !== ''
171
+ ? badge.position
172
+ : availablePositions?.length > 0
173
+ ? availablePositions[0]
174
+ : ''
175
+
176
+ return {
177
+ id: badge?.id ?? '',
178
+ position: position || 'top-left',
179
+ collectionId: badge?.collectionId ?? null,
180
+ assetId: badge?.assetId ?? '',
181
+ }
182
+ },
183
+ [availablePositions],
184
+ )
185
+
186
+ const { form, submitHandler, entity, isPending, resetForm, refreshEntity } = useDetailPage({
187
+ queryDocument: getBadgeDetailDocument,
188
+ createDocument: creatingNewEntity ? createBadgeDocument : undefined,
189
+ updateDocument: creatingNewEntity ? undefined : updateBadgeDocument,
190
+ setValuesForUpdate,
191
+ params: creatingNewEntity ? { id: 'new' } : { id: params.id },
192
+ onSuccess: async (data) => {
193
+ toast.success(creatingNewEntity ? 'Successfully created badge' : 'Successfully saved badge')
194
+ resetForm()
195
+ if (creatingNewEntity) {
196
+ await navigate({ to: `/badges/${data.id}` })
197
+ } else {
198
+ await refreshEntity()
199
+ }
200
+ queryClient.invalidateQueries({ queryKey: ['badges'] })
201
+ },
202
+ onError: (err) => {
203
+ toast.error(creatingNewEntity ? 'Failed to create badge' : 'Failed to save badge', {
204
+ description: err instanceof Error ? err.message : 'Unknown error',
205
+ })
206
+ },
207
+ })
208
+
209
+ const createAssetMutation = useMutation({
210
+ mutationFn: async (file: File) => {
211
+ const input = [{ file }]
212
+ const result: any = await api.mutate(createAssetsDocument, { input })
213
+ const assetResult = result.createAssets[0]
214
+ if ('id' in assetResult) {
215
+ return assetResult
216
+ } else if ('message' in assetResult) {
217
+ throw new Error(assetResult.message)
218
+ }
219
+ throw new Error('Failed to upload asset')
220
+ },
221
+ onSuccess: (asset) => {
222
+ setUploadedAsset({
223
+ id: asset.id,
224
+ name: asset.name || 'Uploaded image',
225
+ preview: asset.preview || '',
226
+ source: asset.source || asset.preview || '',
227
+ })
228
+ form.setValue('assetId', asset.id, { shouldDirty: true, shouldValidate: true })
229
+ setUploading(false)
230
+ toast.success('Image uploaded successfully')
231
+ },
232
+ onError: (error) => {
233
+ toast.error('Failed to upload image', {
234
+ description: error instanceof Error ? error.message : 'Unknown error',
235
+ })
236
+ setUploading(false)
237
+ },
238
+ })
239
+
240
+ // Track synced entity ID to prevent re-syncing
241
+ const syncedEntityIdRef = useRef<string | null>(null)
242
+ const entityIdRef = useRef<string | null>(null)
243
+
244
+ // Sync uploadedAsset state when entity changes (only when entity.id changes)
245
+ useEffect(() => {
246
+ if (entity?.id && entity.id !== entityIdRef.current) {
247
+ entityIdRef.current = entity.id
248
+ syncedEntityIdRef.current = null // Reset sync tracking when entity changes
249
+ if (entity?.asset) {
250
+ setUploadedAsset({
251
+ id: entity.asset.id,
252
+ name: entity.asset.name,
253
+ preview: entity.asset.preview,
254
+ source: entity.asset.source,
255
+ })
256
+ }
257
+ }
258
+ }, [entity?.id, entity?.asset])
259
+
260
+ // Sync form values when entity loads (for edit mode)
261
+ // Run whenever entity data or availablePositions changes
262
+ useEffect(() => {
263
+ const badge = entity
264
+ if (!creatingNewEntity && badge?.id) {
265
+ const entityPosition = badge.position
266
+ const entityCollectionId = badge.collectionId
267
+
268
+ // Set position - use entity value if available, otherwise keep what's there
269
+ if (entityPosition) {
270
+ if (availablePositions.length > 0 && availablePositions.includes(entityPosition)) {
271
+ form.setValue('position', entityPosition, { shouldDirty: false })
272
+ } else if (availablePositions.length === 0) {
273
+ // If positions not loaded yet, set it anyway (will be validated later)
274
+ form.setValue('position', entityPosition, { shouldDirty: false })
275
+ }
276
+ }
277
+
278
+ // Always sync collection - set it immediately
279
+ form.setValue('collectionId', entityCollectionId ?? null, { shouldDirty: false })
280
+ form.setValue('assetId', badge.assetId ?? '', { shouldDirty: false })
281
+ }
282
+
283
+ // Initialize default values for new badges
284
+ if (creatingNewEntity && availablePositions.length > 0) {
285
+ form.setValue('position', availablePositions[0])
286
+ form.setValue('collectionId', null)
287
+ }
288
+ }, [entity, availablePositions, creatingNewEntity, form])
289
+
290
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
291
+ const file = event.target.files?.[0]
292
+ if (file) {
293
+ if (!file.type.startsWith('image/')) {
294
+ toast.error('Please select an image file')
295
+ return
296
+ }
297
+ setUploading(true)
298
+ createAssetMutation.mutate(file)
299
+ }
300
+ if (fileInputRef.current) {
301
+ fileInputRef.current.value = ''
302
+ }
303
+ }
304
+
305
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
306
+ e.preventDefault()
307
+
308
+ if (creatingNewEntity) {
309
+ if (!uploadedAsset?.id) {
310
+ toast.error('Please upload an image first')
311
+ return
312
+ }
313
+
314
+ const formValues = form.getValues()
315
+ try {
316
+ const result = await api.mutate(createBadgeDocument, {
317
+ input: {
318
+ assetId: uploadedAsset.id,
319
+ position: formValues.position || availablePositions[0] || 'top-left',
320
+ collectionId: formValues.collectionId || null,
321
+ },
322
+ })
323
+
324
+ toast.success('Badge created successfully')
325
+ await navigate({ to: `/badges/${result.createBadge.id}` })
326
+ queryClient.invalidateQueries({ queryKey: ['badges'] })
327
+ } catch (error: any) {
328
+ toast.error('Failed to create badge', {
329
+ description: error instanceof Error ? error.message : 'Unknown error',
330
+ })
331
+ }
332
+ return
333
+ }
334
+
335
+ await submitHandler(e)
336
+ }
337
+
338
+ const currentAsset =
339
+ uploadedAsset ||
340
+ (entity?.asset
341
+ ? {
342
+ id: entity.asset.id,
343
+ name: entity.asset.name,
344
+ preview: entity.asset.preview,
345
+ source: entity.asset.source,
346
+ }
347
+ : null)
348
+ const currentAssetForPreview =
349
+ currentAsset == null
350
+ ? null
351
+ : (entity?.asset && currentAsset.id === entity.asset.id
352
+ ? entity.asset
353
+ : { ...currentAsset, type: 'IMAGE' }) as ComponentProps<typeof VendureImage>['asset']
354
+ const hasAssetChanged = Boolean(
355
+ !creatingNewEntity && uploadedAsset?.id && uploadedAsset.id !== entity?.assetId,
356
+ )
357
+
358
+ return (
359
+ <Page pageId="badge-detail" form={form} submitHandler={handleSubmit}>
360
+ <PageTitle>
361
+ {creatingNewEntity ? 'New Badge' : entity?.id ? `Badge ${entity.id}` : 'Edit Badge'}
362
+ </PageTitle>
363
+ <PageActionBar>
364
+ <PageActionBarRight>
365
+ <Button
366
+ type="submit"
367
+ disabled={
368
+ creatingNewEntity
369
+ ? !uploadedAsset || !form.formState.isValid || isPending || uploading
370
+ : (!form.formState.isDirty && !hasAssetChanged) || isPending || uploading
371
+ }
372
+ >
373
+ {isPending || uploading ? 'Saving...' : creatingNewEntity ? 'Create' : 'Update'}
374
+ </Button>
375
+ </PageActionBarRight>
376
+ </PageActionBar>
377
+ <PageLayout>
378
+ <PageBlock column="main" blockId="main-form">
379
+ <div className="space-y-4">
380
+ <div className="space-y-2" style={{ maxWidth: '400px' }}>
381
+ <Label>Image</Label>
382
+ <div className="space-y-4">
383
+ {currentAsset ? (
384
+ <div className="space-y-2">
385
+ <VendureImage asset={currentAssetForPreview} alt={currentAsset.name} preset="full" />
386
+ <div className="text-sm text-muted-foreground">{currentAsset.name}</div>
387
+ </div>
388
+ ) : (
389
+ <div className="border-2 border-dashed border-muted rounded-lg p-8 text-center">
390
+ <UploadIcon className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
391
+ <p className="text-sm text-muted-foreground mb-4">No image uploaded</p>
392
+ </div>
393
+ )}
394
+ <div>
395
+ <input
396
+ ref={fileInputRef}
397
+ type="file"
398
+ accept="image/*"
399
+ onChange={handleFileSelect}
400
+ className="hidden"
401
+ />
402
+ <Button
403
+ type="button"
404
+ variant="outline"
405
+ onClick={() => fileInputRef.current?.click()}
406
+ disabled={uploading}
407
+ className="w-full"
408
+ >
409
+ <UploadIcon className="mr-2 h-4 w-4" />
410
+ {uploading ? 'Uploading...' : currentAsset ? 'Change Image' : 'Upload Image'}
411
+ </Button>
412
+ </div>
413
+ </div>
414
+ </div>
415
+
416
+ <div style={{ maxWidth: '700px' }} className="space-y-4">
417
+ <FormFieldWrapper
418
+ control={form.control}
419
+ name="position"
420
+ label="Position"
421
+ render={({ field }) => {
422
+ // Ensure value is valid - use field.value if it exists and is in availablePositions, otherwise empty string
423
+ const validValue =
424
+ field.value && availablePositions.includes(field.value) ? field.value : ''
425
+ return (
426
+ <Select value={validValue} onValueChange={field.onChange}>
427
+ <SelectTrigger className="w-full">
428
+ <SelectValue placeholder="Select position" />
429
+ </SelectTrigger>
430
+ <SelectContent>
431
+ {availablePositions.map((pos) => (
432
+ <SelectItem key={pos} value={pos}>
433
+ {pos}
434
+ </SelectItem>
435
+ ))}
436
+ </SelectContent>
437
+ </Select>
438
+ )
439
+ }}
440
+ />
441
+ <FormFieldWrapper
442
+ control={form.control}
443
+ name="collectionId"
444
+ label="Collection"
445
+ render={({ field }) => (
446
+ <Select
447
+ value={field.value}
448
+ onValueChange={(val) => field.onChange(val === 'none' ? null : val)}
449
+ >
450
+ <SelectTrigger className="w-full">
451
+ <SelectValue placeholder="No collection" />
452
+ </SelectTrigger>
453
+ <SelectContent>
454
+ <SelectItem value="none">No collection</SelectItem>
455
+ {collections.map((collection) => (
456
+ <SelectItem key={collection.id} value={collection.id}>
457
+ {collection.name}
458
+ </SelectItem>
459
+ ))}
460
+ </SelectContent>
461
+ </Select>
462
+ )}
463
+ />
464
+ </div>
465
+ </div>
466
+ </PageBlock>
467
+ </PageLayout>
468
+ </Page>
469
+ )
470
+ }