@trustless-work/blocks 0.0.7 → 0.0.8

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 (66) hide show
  1. package/bin/index.js +485 -17
  2. package/package.json +1 -1
  3. package/templates/escrows/details/Actions.tsx +144 -149
  4. package/templates/escrows/details/Entities.tsx +1 -1
  5. package/templates/escrows/details/EntityCard.tsx +1 -3
  6. package/templates/escrows/details/EscrowDetailDialog.tsx +16 -16
  7. package/templates/escrows/details/GeneralInformation.tsx +19 -22
  8. package/templates/escrows/details/MilestoneCard.tsx +46 -47
  9. package/templates/escrows/details/MilestoneDetailDialog.tsx +1 -2
  10. package/templates/escrows/details/Milestones.tsx +0 -5
  11. package/templates/escrows/details/SuccessReleaseDialog.tsx +4 -6
  12. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +84 -49
  13. package/templates/escrows/escrows-by-role/cards/Filters.tsx +3 -5
  14. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +8 -26
  15. package/templates/escrows/escrows-by-role/table/Filters.tsx +3 -5
  16. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +89 -55
  17. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +3 -5
  18. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +8 -24
  19. package/templates/escrows/escrows-by-signer/table/Filters.tsx +3 -5
  20. package/templates/escrows/multi-release/dispute-milestone/button/DisputeEscrow.tsx +98 -0
  21. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +528 -0
  22. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +506 -0
  23. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +179 -0
  24. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +175 -0
  25. package/templates/escrows/multi-release/release-milestone/button/ReleaseEscrow.tsx +116 -0
  26. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +122 -0
  27. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +178 -0
  28. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +156 -0
  29. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +85 -0
  30. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +105 -0
  31. package/templates/escrows/multi-release/update-escrow/dialog/UpdateEscrow.tsx +471 -0
  32. package/templates/escrows/multi-release/update-escrow/form/UpdateEscrow.tsx +449 -0
  33. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +152 -0
  34. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +254 -0
  35. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/button/ApproveMilestone.tsx +20 -7
  36. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/dialog/ApproveMilestone.tsx +3 -3
  37. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/form/ApproveMilestone.tsx +3 -3
  38. package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/useApproveMilestone.ts +16 -16
  39. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/button/ChangeMilestoneStatus.tsx +4 -4
  40. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +4 -4
  41. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/form/ChangeMilestoneStatus.tsx +3 -3
  42. package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/useChangeMilestoneStatus.ts +1 -1
  43. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/button/FundEscrow.tsx +3 -3
  44. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/dialog/FundEscrow.tsx +3 -3
  45. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/form/FundEscrow.tsx +3 -3
  46. package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/useFundEscrow.ts +1 -1
  47. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +2 -2
  48. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +14 -6
  49. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +14 -6
  50. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -57
  51. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +42 -1
  52. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +2 -2
  53. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +3 -3
  54. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +3 -6
  55. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +2 -2
  56. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +14 -1
  57. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +2 -2
  58. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +2 -2
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +12 -7
  60. package/templates/providers/EscrowDialogsProvider.tsx +1 -3
  61. package/templates/providers/EscrowProvider.tsx +27 -4
  62. package/templates/providers/TrustlessWork.tsx +1 -1
  63. package/templates/escrows/details/ProgressEscrow.tsx +0 -191
  64. /package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/schema.ts +0 -0
  65. /package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/schema.ts +0 -0
  66. /package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/schema.ts +0 -0
@@ -0,0 +1,449 @@
1
+ import * as React from "react";
2
+ import {
3
+ Form,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormControl,
8
+ FormMessage,
9
+ } from "__UI_BASE__/form";
10
+ import { Input } from "__UI_BASE__/input";
11
+ import { Button } from "__UI_BASE__/button";
12
+ import { Card } from "__UI_BASE__/card";
13
+ import {
14
+ Select,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ SelectContent,
18
+ SelectItem,
19
+ } from "__UI_BASE__/select";
20
+ import { Textarea } from "__UI_BASE__/textarea";
21
+ import { useUpdateEscrow } from "./useUpdateEscrow";
22
+ import { Trash2, DollarSign, Percent, Loader2 } from "lucide-react";
23
+ import Link from "next/link";
24
+ import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
25
+
26
+ export const UpdateEscrowForm = () => {
27
+ const {
28
+ form,
29
+ isSubmitting,
30
+ milestones,
31
+ isAnyMilestoneEmpty,
32
+ handleSubmit,
33
+ handleAddMilestone,
34
+ handleRemoveMilestone,
35
+ handleMilestoneAmountChange,
36
+ handlePlatformFeeChange,
37
+ } = useUpdateEscrow();
38
+
39
+ return (
40
+ <Form {...form}>
41
+ <form onSubmit={handleSubmit} className="flex flex-col space-y-6">
42
+ <Card className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 p-4">
43
+ <Link
44
+ className="flex-1"
45
+ href="https://docs.trustlesswork.com/trustless-work/technology-overview/escrow-types"
46
+ target="_blank"
47
+ >
48
+ <div className="flex items-center gap-2">
49
+ <div className="h-2 w-2 rounded-full bg-primary" />
50
+ <h2 className="text-xl font-semibold">Multi Release Escrow</h2>
51
+ </div>
52
+ <p className="text-muted-foreground mt-1">
53
+ Update escrow details and milestones
54
+ </p>
55
+ </Link>
56
+ </Card>
57
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
58
+ <FormField
59
+ control={form.control}
60
+ name="title"
61
+ render={({ field }) => (
62
+ <FormItem>
63
+ <FormLabel className="flex items-center">
64
+ Title<span className="text-destructive ml-1">*</span>
65
+ </FormLabel>
66
+ <FormControl>
67
+ <Input
68
+ placeholder="Escrow title"
69
+ {...field}
70
+ onChange={(e) => {
71
+ field.onChange(e);
72
+ }}
73
+ />
74
+ </FormControl>
75
+ <FormMessage />
76
+ </FormItem>
77
+ )}
78
+ />
79
+
80
+ <FormField
81
+ control={form.control}
82
+ name="engagementId"
83
+ render={({ field }) => (
84
+ <FormItem>
85
+ <FormLabel className="flex items-center">
86
+ Engagement<span className="text-destructive ml-1">*</span>
87
+ </FormLabel>
88
+ <FormControl>
89
+ <Input
90
+ placeholder="Enter identifier"
91
+ {...field}
92
+ onChange={(e) => {
93
+ field.onChange(e);
94
+ }}
95
+ />
96
+ </FormControl>
97
+ <FormMessage />
98
+ </FormItem>
99
+ )}
100
+ />
101
+
102
+ <FormField
103
+ control={form.control}
104
+ name="trustline.address"
105
+ render={({ field }) => (
106
+ <FormItem>
107
+ <FormLabel className="flex items-center">
108
+ Trustline<span className="text-destructive ml-1">*</span>
109
+ </FormLabel>
110
+ <FormControl>
111
+ <Select
112
+ value={field.value}
113
+ onValueChange={(e) => {
114
+ field.onChange(e);
115
+ }}
116
+ >
117
+ <SelectTrigger className="w-full">
118
+ <SelectValue placeholder="Select trustline" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ {trustlineOptions
122
+ .filter((option) => option.value)
123
+ .map((option, index) => (
124
+ <SelectItem
125
+ key={`${option.value}-${index}`}
126
+ value={option.value}
127
+ >
128
+ {option.label}
129
+ </SelectItem>
130
+ ))}
131
+ </SelectContent>
132
+ </Select>
133
+ </FormControl>
134
+ <FormMessage />
135
+ </FormItem>
136
+ )}
137
+ />
138
+ </div>
139
+
140
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
141
+ <FormField
142
+ control={form.control}
143
+ name="roles.approver"
144
+ render={({ field }) => (
145
+ <FormItem>
146
+ <FormLabel className="flex items-center justify-between">
147
+ <span className="flex items-center">
148
+ Approver<span className="text-destructive ml-1">*</span>
149
+ </span>
150
+ </FormLabel>
151
+
152
+ <FormControl>
153
+ <Input
154
+ placeholder="Enter approver address"
155
+ {...field}
156
+ onChange={(e) => {
157
+ field.onChange(e);
158
+ }}
159
+ />
160
+ </FormControl>
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+
166
+ <FormField
167
+ control={form.control}
168
+ name="roles.serviceProvider"
169
+ render={({ field }) => (
170
+ <FormItem>
171
+ <FormLabel className="flex items-center justify-between">
172
+ <span className="flex items-center">
173
+ Service Provider
174
+ <span className="text-destructive ml-1">*</span>
175
+ </span>
176
+ </FormLabel>
177
+
178
+ <FormControl>
179
+ <Input
180
+ placeholder="Enter service provider address"
181
+ {...field}
182
+ onChange={(e) => {
183
+ field.onChange(e);
184
+ }}
185
+ />
186
+ </FormControl>
187
+ <FormMessage />
188
+ </FormItem>
189
+ )}
190
+ />
191
+ </div>
192
+
193
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
194
+ <FormField
195
+ control={form.control}
196
+ name="roles.releaseSigner"
197
+ render={({ field }) => (
198
+ <FormItem>
199
+ <FormLabel className="flex items-center justify-between">
200
+ <span className="flex items-center">
201
+ Release Signer
202
+ <span className="text-destructive ml-1">*</span>
203
+ </span>
204
+ </FormLabel>
205
+
206
+ <FormControl>
207
+ <Input
208
+ placeholder="Enter release signer address"
209
+ {...field}
210
+ onChange={(e) => {
211
+ field.onChange(e);
212
+ }}
213
+ />
214
+ </FormControl>
215
+ <FormMessage />
216
+ </FormItem>
217
+ )}
218
+ />
219
+
220
+ <FormField
221
+ control={form.control}
222
+ name="roles.disputeResolver"
223
+ render={({ field }) => (
224
+ <FormItem>
225
+ <FormLabel className="flex items-center justify-between">
226
+ <span className="flex items-center">
227
+ Dispute Resolver
228
+ <span className="text-destructive ml-1">*</span>
229
+ </span>
230
+ </FormLabel>
231
+
232
+ <FormControl>
233
+ <Input
234
+ placeholder="Enter dispute resolver address"
235
+ {...field}
236
+ onChange={(e) => {
237
+ field.onChange(e);
238
+ }}
239
+ />
240
+ </FormControl>
241
+ <FormMessage />
242
+ </FormItem>
243
+ )}
244
+ />
245
+ </div>
246
+
247
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
248
+ <FormField
249
+ control={form.control}
250
+ name="roles.platformAddress"
251
+ render={({ field }) => (
252
+ <FormItem>
253
+ <FormLabel className="flex items-center justify-between">
254
+ <span className="flex items-center">
255
+ Platform Address
256
+ <span className="text-destructive ml-1">*</span>
257
+ </span>
258
+ </FormLabel>
259
+
260
+ <FormControl>
261
+ <Input
262
+ placeholder="Enter platform address"
263
+ {...field}
264
+ onChange={(e) => {
265
+ field.onChange(e);
266
+ }}
267
+ />
268
+ </FormControl>
269
+ <FormMessage />
270
+ </FormItem>
271
+ )}
272
+ />
273
+ <FormField
274
+ control={form.control}
275
+ name="roles.receiver"
276
+ render={({ field }) => (
277
+ <FormItem>
278
+ <FormLabel className="flex items-center justify-between">
279
+ <span className="flex items-center">
280
+ Receiver<span className="text-destructive ml-1">*</span>
281
+ </span>
282
+ </FormLabel>
283
+
284
+ <FormControl>
285
+ <Input
286
+ placeholder="Enter receiver address"
287
+ {...field}
288
+ onChange={(e) => {
289
+ field.onChange(e);
290
+ }}
291
+ />
292
+ </FormControl>
293
+ <FormMessage />
294
+ </FormItem>
295
+ )}
296
+ />
297
+ </div>
298
+
299
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
300
+ <FormField
301
+ control={form.control}
302
+ name="platformFee"
303
+ render={() => (
304
+ <FormItem>
305
+ <FormLabel className="flex items-center">
306
+ Platform Fee<span className="text-destructive ml-1">*</span>
307
+ </FormLabel>
308
+ <FormControl>
309
+ <div className="relative">
310
+ <Percent
311
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
312
+ size={18}
313
+ />
314
+ <Input
315
+ placeholder="Enter platform fee"
316
+ className="pl-10"
317
+ value={form.watch("platformFee")?.toString() || ""}
318
+ onChange={handlePlatformFeeChange}
319
+ />
320
+ </div>
321
+ </FormControl>
322
+ <FormMessage />
323
+ </FormItem>
324
+ )}
325
+ />
326
+
327
+ <FormField
328
+ control={form.control}
329
+ name="receiverMemo"
330
+ render={({ field }) => (
331
+ <FormItem>
332
+ <FormLabel className="flex items-center">
333
+ Receiver Memo (opcional)
334
+ </FormLabel>
335
+ <FormControl>
336
+ <Input
337
+ type="text"
338
+ placeholder="Enter the escrow receiver Memo"
339
+ {...field}
340
+ onChange={(e) => {
341
+ field.onChange(e);
342
+ }}
343
+ />
344
+ </FormControl>
345
+ <FormMessage />
346
+ </FormItem>
347
+ )}
348
+ />
349
+ </div>
350
+
351
+ <FormField
352
+ control={form.control}
353
+ name="description"
354
+ render={({ field }) => (
355
+ <FormItem>
356
+ <FormLabel className="flex items-center">
357
+ Description<span className="text-destructive ml-1">*</span>
358
+ </FormLabel>
359
+ <FormControl>
360
+ <Textarea
361
+ placeholder="Escrow description"
362
+ {...field}
363
+ onChange={(e) => {
364
+ field.onChange(e);
365
+ }}
366
+ />
367
+ </FormControl>
368
+ <FormMessage />
369
+ </FormItem>
370
+ )}
371
+ />
372
+
373
+ <div className="space-y-4">
374
+ <FormLabel className="flex items-center">
375
+ Milestones<span className="text-destructive ml-1">*</span>
376
+ </FormLabel>
377
+ {milestones.map((milestone, index) => (
378
+ <div key={index} className="space-y-4">
379
+ <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
380
+ <Input
381
+ placeholder="Milestone Description"
382
+ value={milestone.description}
383
+ className="w-full sm:w-3/5"
384
+ onChange={(e) => {
385
+ const updatedMilestones = [...milestones];
386
+ updatedMilestones[index].description = e.target.value;
387
+ form.setValue("milestones", updatedMilestones);
388
+ }}
389
+ />
390
+
391
+ <div className="relative w-full sm:w-2/5">
392
+ <DollarSign
393
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"
394
+ size={18}
395
+ />
396
+ <Input
397
+ className="pl-10"
398
+ placeholder="Enter amount"
399
+ value={milestone.amount?.toString() || ""}
400
+ onChange={(e) => handleMilestoneAmountChange(index, e)}
401
+ />
402
+ </div>
403
+
404
+ <Button
405
+ onClick={() => handleRemoveMilestone(index)}
406
+ className="p-2 bg-transparent text-red-500 rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-red-500 focus:ring-0 active:ring-0 self-start sm:self-center"
407
+ disabled={milestones.length === 1}
408
+ >
409
+ <Trash2 className="h-5 w-5" />
410
+ </Button>
411
+ </div>
412
+
413
+ {index === milestones.length - 1 && (
414
+ <div className="flex justify-end mt-4">
415
+ <Button
416
+ disabled={isAnyMilestoneEmpty}
417
+ className="w-full md:w-1/4"
418
+ variant="outline"
419
+ onClick={handleAddMilestone}
420
+ type="button"
421
+ >
422
+ Add Item
423
+ </Button>
424
+ </div>
425
+ )}
426
+ </div>
427
+ ))}
428
+ </div>
429
+
430
+ <div className="flex justify-start">
431
+ <Button
432
+ className="w-full md:w-1/4 cursor-pointer"
433
+ type="submit"
434
+ disabled={isAnyMilestoneEmpty || isSubmitting}
435
+ >
436
+ {isSubmitting ? (
437
+ <div className="flex items-center">
438
+ <Loader2 className="h-5 w-5 animate-spin" />
439
+ <span className="ml-2">Updating...</span>
440
+ </div>
441
+ ) : (
442
+ "Update"
443
+ )}
444
+ </Button>
445
+ </div>
446
+ </form>
447
+ </Form>
448
+ );
449
+ };
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+ import { isValidWallet } from "../../../../wallet-kit/validators";
3
+
4
+ export const useUpdateEscrowSchema = () => {
5
+ const getBaseSchema = () => {
6
+ return z.object({
7
+ trustline: z.object({
8
+ address: z.string().min(1, {
9
+ message: "Trustline address is required.",
10
+ }),
11
+ decimals: z.number().default(10000000),
12
+ }),
13
+ roles: z.object({
14
+ approver: z
15
+ .string()
16
+ .min(1, { message: "Approver is required." })
17
+ .refine((value) => isValidWallet(value), {
18
+ message: "Approver must be a valid wallet.",
19
+ }),
20
+ serviceProvider: z
21
+ .string()
22
+ .min(1, { message: "Service provider is required." })
23
+ .refine((value) => isValidWallet(value), {
24
+ message: "Service provider must be a valid wallet.",
25
+ }),
26
+ platformAddress: z
27
+ .string()
28
+ .min(1, { message: "Platform address is required." })
29
+ .refine((value) => isValidWallet(value), {
30
+ message: "Platform address must be a valid wallet.",
31
+ }),
32
+ releaseSigner: z
33
+ .string()
34
+ .min(1, { message: "Release signer is required." })
35
+ .refine((value) => isValidWallet(value), {
36
+ message: "Release signer must be a valid wallet.",
37
+ }),
38
+ disputeResolver: z
39
+ .string()
40
+ .min(1, { message: "Dispute resolver is required." })
41
+ .refine((value) => isValidWallet(value), {
42
+ message: "Dispute resolver must be a valid wallet.",
43
+ }),
44
+ receiver: z
45
+ .string()
46
+ .min(1, { message: "Receiver address is required." })
47
+ .refine((value) => isValidWallet(value), {
48
+ message: "Receiver address must be a valid wallet.",
49
+ }),
50
+ }),
51
+ engagementId: z.string().min(1, { message: "Engagement is required." }),
52
+ title: z.string().min(1, { message: "Title is required." }),
53
+ description: z.string().min(10, {
54
+ message: "Description must be at least 10 characters long.",
55
+ }),
56
+ platformFee: z
57
+ .union([z.string(), z.number()])
58
+ .refine(
59
+ (val) => {
60
+ if (typeof val === "string") {
61
+ if (val === "" || val === "." || val.endsWith(".")) return true;
62
+ const n = Number(val);
63
+ return !isNaN(n) && n > 0;
64
+ }
65
+ return val > 0;
66
+ },
67
+ { message: "Platform fee must be greater than 0." }
68
+ )
69
+ .refine(
70
+ (val) => {
71
+ if (typeof val === "string") {
72
+ if (val === "" || val === "." || val.endsWith(".")) return true;
73
+ const n = Number(val);
74
+ if (isNaN(n)) return false;
75
+ const dp = (n.toString().split(".")[1] || "").length;
76
+ return dp <= 2;
77
+ }
78
+ const dp = (val.toString().split(".")[1] || "").length;
79
+ return dp <= 2;
80
+ },
81
+ { message: "Platform fee can have a maximum of 2 decimal places." }
82
+ ),
83
+ receiverMemo: z
84
+ .string()
85
+ .optional()
86
+ .refine((val) => !val || val.length >= 1, {
87
+ message: "Receiver Memo must be at least 1.",
88
+ })
89
+ .refine((val) => !val || /^[1-9][0-9]*$/.test(val), {
90
+ message:
91
+ "Receiver Memo must be a whole number greater than 0 (no decimals).",
92
+ }),
93
+ });
94
+ };
95
+
96
+ const getMultiReleaseFormSchema = () => {
97
+ const baseSchema = getBaseSchema();
98
+
99
+ return baseSchema.extend({
100
+ milestones: z
101
+ .array(
102
+ z.object({
103
+ description: z.string().min(1, {
104
+ message: "Milestone description is required.",
105
+ }),
106
+ amount: z
107
+ .union([z.string(), z.number()])
108
+ .refine(
109
+ (val) => {
110
+ if (typeof val === "string") {
111
+ if (val === "" || val === "." || val.endsWith(".")) {
112
+ return true;
113
+ }
114
+ const numVal = Number(val);
115
+ return !isNaN(numVal) && numVal > 0;
116
+ }
117
+ return val > 0;
118
+ },
119
+ {
120
+ message: "Milestone amount must be greater than 0.",
121
+ }
122
+ )
123
+ .refine(
124
+ (val) => {
125
+ if (typeof val === "string") {
126
+ if (val === "" || val === "." || val.endsWith(".")) {
127
+ return true;
128
+ }
129
+ const numVal = Number(val);
130
+ if (isNaN(numVal)) return false;
131
+ const decimalPlaces = (
132
+ numVal.toString().split(".")[1] || ""
133
+ ).length;
134
+ return decimalPlaces <= 2;
135
+ }
136
+ const decimalPlaces = (val.toString().split(".")[1] || "")
137
+ .length;
138
+ return decimalPlaces <= 2;
139
+ },
140
+ {
141
+ message:
142
+ "Milestone amount can have a maximum of 2 decimal places.",
143
+ }
144
+ ),
145
+ })
146
+ )
147
+ .min(1, { message: "At least one milestone is required." }),
148
+ });
149
+ };
150
+
151
+ return { getMultiReleaseFormSchema };
152
+ };