@12-apps/discounts 1.3.0 → 1.5.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.
- package/README.md +38 -3
- package/package.json +2 -2
- package/src/react/api.ts +27 -3
- package/src/react/combo-slot-builder.tsx +214 -0
- package/src/react/copy.ts +76 -0
- package/src/react/discount-actions-menu.tsx +39 -15
- package/src/react/discount-form-fields.tsx +72 -1
- package/src/react/discount-form.tsx +134 -13
- package/src/react/discount-target-picker.tsx +37 -81
- package/src/react/discounts-screen.tsx +113 -39
- package/src/react/pt-BR.ts +34 -0
- package/src/react/target-picker-field.tsx +143 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CategorySelectCopy } from "@12-apps/ui/copy";
|
|
4
|
+
import { useState, type JSX } from "react";
|
|
5
|
+
|
|
6
|
+
import { Autocomplete } from "@12-apps/ui/form/Autocomplete";
|
|
7
|
+
import { CategorySelect } from "@12-apps/ui/form/CategorySelect";
|
|
8
|
+
import { FormControl, FormLabel, FormMessage } from "@12-apps/ui/form/Form";
|
|
9
|
+
|
|
10
|
+
import type { WireTarget, WireTargetGroup } from "./api";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One collection's picker, in the two shapes a collection can take.
|
|
14
|
+
*
|
|
15
|
+
* Its own module because BOTH callers need it and they are not each other's
|
|
16
|
+
* parent: the scope-driven block renders one per discount, and the combo
|
|
17
|
+
* builder renders one per collection per GROUP. Importing it from either would
|
|
18
|
+
* make the two files a cycle, and copying it is how a nesting collection ends
|
|
19
|
+
* up a flat combobox in one of the two places — the exact bug this seam exists
|
|
20
|
+
* to prevent.
|
|
21
|
+
*
|
|
22
|
+
* There is no shared multi-select in `@12-apps/ui`, so the flat case composes
|
|
23
|
+
* `Autocomplete` in multiple mode — chips plus type-to-filter, the same control
|
|
24
|
+
* `AddressAutocomplete` is built on. A collection that NESTS gets
|
|
25
|
+
* `CategorySelect` instead: a flat combobox would put a subcategory beside its
|
|
26
|
+
* parent with nothing to tell them apart.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** One labelled multi-select of targets, with its own input-text state. */
|
|
30
|
+
function TargetField({
|
|
31
|
+
label,
|
|
32
|
+
placeholder,
|
|
33
|
+
requiredMessage,
|
|
34
|
+
options,
|
|
35
|
+
ids,
|
|
36
|
+
onChange,
|
|
37
|
+
dataTestId,
|
|
38
|
+
}: {
|
|
39
|
+
label: string;
|
|
40
|
+
placeholder: string;
|
|
41
|
+
requiredMessage: string | undefined;
|
|
42
|
+
options: readonly WireTarget[];
|
|
43
|
+
ids: readonly string[];
|
|
44
|
+
onChange: (next: string[]) => void;
|
|
45
|
+
dataTestId: string;
|
|
46
|
+
}): JSX.Element {
|
|
47
|
+
const [query, setQuery] = useState("");
|
|
48
|
+
const byId = new Map(options.map((option) => [option.id, option]));
|
|
49
|
+
// An id whose option is missing — a target archived since the rule was saved
|
|
50
|
+
// — still renders as a chip, so saving cannot silently drop it.
|
|
51
|
+
const selected = ids.map((id) => byId.get(id) ?? { id, name: id });
|
|
52
|
+
|
|
53
|
+
const missing = requiredMessage !== undefined && ids.length === 0;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<FormControl fullWidth>
|
|
57
|
+
<FormLabel error={missing}>{label}</FormLabel>
|
|
58
|
+
<div data-testid={dataTestId}>
|
|
59
|
+
<Autocomplete<WireTarget>
|
|
60
|
+
multiple
|
|
61
|
+
value={query}
|
|
62
|
+
onChange={setQuery}
|
|
63
|
+
suggestions={[...options]}
|
|
64
|
+
selectedItems={selected}
|
|
65
|
+
onSelectedItemsChange={(items) => onChange(items.map((item) => item.id))}
|
|
66
|
+
getKey={(option) => option.id}
|
|
67
|
+
getLabel={(option) => option.name}
|
|
68
|
+
placeholder={placeholder}
|
|
69
|
+
inputAriaLabel={label}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
{missing && (
|
|
73
|
+
<FormMessage error dataTestId={`${dataTestId}-message`}>
|
|
74
|
+
{requiredMessage}
|
|
75
|
+
</FormMessage>
|
|
76
|
+
)}
|
|
77
|
+
</FormControl>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One collection's picker, for whoever is asking.
|
|
83
|
+
*
|
|
84
|
+
* Extracted because the COMBO builder needs the same control per GROUP, and a
|
|
85
|
+
* second copy is how a nesting collection ends up rendered as a flat combobox
|
|
86
|
+
* in one of the two places — the bug this seam exists to prevent. The caller
|
|
87
|
+
* supplies the label and the test id, because "Categorias com desconto" and
|
|
88
|
+
* "Categorias deste grupo" are different sentences about the same control.
|
|
89
|
+
*
|
|
90
|
+
* `requiredMessage` is OPTIONAL, and that is the difference between the two
|
|
91
|
+
* callers rather than an oversight. At CATEGORY/ITEM scope an empty picker IS
|
|
92
|
+
* the error, so the message hangs on it. Inside a combo group it is not: a
|
|
93
|
+
* group naming two categories and no items is complete, so the group as a whole
|
|
94
|
+
* carries the refusal and neither picker does.
|
|
95
|
+
*/
|
|
96
|
+
export function TargetPickerField({
|
|
97
|
+
group,
|
|
98
|
+
label,
|
|
99
|
+
placeholder,
|
|
100
|
+
requiredMessage,
|
|
101
|
+
ids,
|
|
102
|
+
onChange,
|
|
103
|
+
dataTestId,
|
|
104
|
+
copy,
|
|
105
|
+
}: {
|
|
106
|
+
group: WireTargetGroup;
|
|
107
|
+
label: string;
|
|
108
|
+
placeholder: string;
|
|
109
|
+
requiredMessage: string | undefined;
|
|
110
|
+
ids: readonly string[];
|
|
111
|
+
onChange: (next: string[]) => void;
|
|
112
|
+
dataTestId: string;
|
|
113
|
+
/** `CategorySelect`'s own words — required since FUT-760, never defaulted. */
|
|
114
|
+
copy: CategorySelectCopy;
|
|
115
|
+
}): JSX.Element {
|
|
116
|
+
if (group.nests) {
|
|
117
|
+
return (
|
|
118
|
+
<CategorySelect
|
|
119
|
+
label={label}
|
|
120
|
+
fullWidth
|
|
121
|
+
allowParentSelection
|
|
122
|
+
options={[...group.targets]}
|
|
123
|
+
value={[...ids]}
|
|
124
|
+
onChange={onChange}
|
|
125
|
+
error={requiredMessage !== undefined && ids.length === 0 ? requiredMessage : undefined}
|
|
126
|
+
copy={copy}
|
|
127
|
+
dataTestId={dataTestId}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
<TargetField
|
|
133
|
+
label={label}
|
|
134
|
+
placeholder={placeholder}
|
|
135
|
+
requiredMessage={requiredMessage}
|
|
136
|
+
options={group.targets}
|
|
137
|
+
ids={ids}
|
|
138
|
+
onChange={onChange}
|
|
139
|
+
dataTestId={dataTestId}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|