@kahitsan/ksui 0.38.0 → 0.38.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -14,7 +14,7 @@ function mockPagedFetch(rows: VoucherOption[]) {
|
|
|
14
14
|
const limit = Number(parsed.searchParams.get("limit") ?? "25");
|
|
15
15
|
const search = (parsed.searchParams.get("search") ?? "").toLowerCase();
|
|
16
16
|
const matched = search
|
|
17
|
-
? rows.filter((r) => r.code.toLowerCase().includes(search))
|
|
17
|
+
? rows.filter((r) => `${r.code} ${r.notes ?? ""}`.toLowerCase().includes(search))
|
|
18
18
|
: rows;
|
|
19
19
|
const start = (page - 1) * limit;
|
|
20
20
|
return {
|
|
@@ -175,6 +175,67 @@ describe("VoucherPicker dialog", () => {
|
|
|
175
175
|
expect(mark!.textContent?.toLowerCase()).toBe("partner");
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
it("shows the voucher description in the row and the staged summary", async () => {
|
|
179
|
+
mockPagedFetch([
|
|
180
|
+
voucher({ id: 50, code: "PARTNER_ACES", notes: "BISCAST ACES outreach discount" }),
|
|
181
|
+
]);
|
|
182
|
+
const { getByTestId } = render(() => (
|
|
183
|
+
<VoucherPicker selected={null} onChange={vi.fn()} subtotal={1000} packageIds={[]} />
|
|
184
|
+
));
|
|
185
|
+
fireEvent.click(getByTestId("voucher-picker-trigger"));
|
|
186
|
+
|
|
187
|
+
await waitFor(() => expect(getByTestId("voucher-picker-result-50")).toBeTruthy());
|
|
188
|
+
expect(getByTestId("voucher-picker-result-50").textContent).toContain(
|
|
189
|
+
"BISCAST ACES outreach discount",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
fireEvent.click(getByTestId("voucher-picker-result-50"));
|
|
193
|
+
expect(getByTestId("voucher-picker-draft-summary").textContent).toContain(
|
|
194
|
+
"BISCAST ACES outreach discount",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("finds a voucher by its description and highlights the match in it", async () => {
|
|
199
|
+
const { calls } = mockPagedFetch([
|
|
200
|
+
voucher({ id: 51, code: "PARTNER_UAPGA", notes: "UAPGA Camarines Chapter discount" }),
|
|
201
|
+
voucher({ id: 52, code: "GENERIC" }),
|
|
202
|
+
]);
|
|
203
|
+
const { getByTestId, queryByTestId } = render(() => (
|
|
204
|
+
<VoucherPicker selected={null} onChange={vi.fn()} subtotal={1000} packageIds={[]} />
|
|
205
|
+
));
|
|
206
|
+
fireEvent.click(getByTestId("voucher-picker-trigger"));
|
|
207
|
+
await waitFor(() => expect(getByTestId("voucher-picker-result-51")).toBeTruthy());
|
|
208
|
+
|
|
209
|
+
fireEvent.input(getByTestId("voucher-picker-search"), {
|
|
210
|
+
target: { value: "camarines" },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await waitFor(() => expect(calls.some((u) => u.includes("search=camarines"))).toBe(true));
|
|
214
|
+
await waitFor(() => expect(queryByTestId("voucher-picker-result-52")).toBeNull());
|
|
215
|
+
|
|
216
|
+
const row = getByTestId("voucher-picker-result-51");
|
|
217
|
+
const mark = row.querySelector("mark");
|
|
218
|
+
expect(mark).not.toBeNull();
|
|
219
|
+
expect(mark!.textContent?.toLowerCase()).toBe("camarines");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("skips the description line for a blank description", async () => {
|
|
223
|
+
mockPagedFetch([
|
|
224
|
+
voucher({ id: 53, code: "PLAIN", notes: " " }),
|
|
225
|
+
voucher({ id: 54, code: "PLAIN" }),
|
|
226
|
+
]);
|
|
227
|
+
const { getByTestId } = render(() => (
|
|
228
|
+
<VoucherPicker selected={null} onChange={vi.fn()} subtotal={1000} packageIds={[]} />
|
|
229
|
+
));
|
|
230
|
+
fireEvent.click(getByTestId("voucher-picker-trigger"));
|
|
231
|
+
|
|
232
|
+
await waitFor(() => expect(getByTestId("voucher-picker-result-53")).toBeTruthy());
|
|
233
|
+
// Identical rows — a whitespace-only description must render nothing extra.
|
|
234
|
+
expect(getByTestId("voucher-picker-result-53").textContent).toBe(
|
|
235
|
+
getByTestId("voucher-picker-result-54").textContent,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
178
239
|
it("shows when a voucher expires", async () => {
|
|
179
240
|
const soon = new Date(Date.now() + 3 * 86400000).toISOString().slice(0, 10);
|
|
180
241
|
mockPagedFetch([voucher({ id: 9, code: "ENDINGSOON", valid_until: soon })]);
|
|
@@ -31,6 +31,8 @@ export interface VoucherOption {
|
|
|
31
31
|
usage_count?: number | null;
|
|
32
32
|
/** Total redemptions allowed; null/absent means unlimited. */
|
|
33
33
|
usage_limit_total?: number | null;
|
|
34
|
+
/** Free-text description; absent on endpoints that don't expose it. */
|
|
35
|
+
notes?: string | null;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const DEFAULT_FETCH_URL = "/api/vouchers?status=active&limit=200";
|
|
@@ -372,6 +374,13 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
372
374
|
.filter(Boolean)
|
|
373
375
|
.join(" · ");
|
|
374
376
|
|
|
377
|
+
// Staged description, trimmed — the footer read it twice, and a signal call
|
|
378
|
+
// in JSX doesn't narrow across re-evaluations.
|
|
379
|
+
const draftNotes = createMemo(() => {
|
|
380
|
+
const n = draft()?.notes?.trim();
|
|
381
|
+
return n ? n : null;
|
|
382
|
+
});
|
|
383
|
+
|
|
375
384
|
const expiresSoon = (v: VoucherOption): boolean => {
|
|
376
385
|
if (!v.valid_until) return false;
|
|
377
386
|
const days = daysUntil(toDay(v.valid_until), today());
|
|
@@ -522,6 +531,11 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
522
531
|
{metaLine(v)}
|
|
523
532
|
</span>
|
|
524
533
|
</span>
|
|
534
|
+
<Show when={v.notes && v.notes.trim()}>
|
|
535
|
+
<span class="block text-xs text-[var(--ks-fg-subtle,#71717a)] truncate">
|
|
536
|
+
{highlightMatch(v.notes!, debouncedQuery())}
|
|
537
|
+
</span>
|
|
538
|
+
</Show>
|
|
525
539
|
</span>
|
|
526
540
|
<span class="text-sm text-[var(--ks-success-fg,#34d399)] shrink-0 font-mono">
|
|
527
541
|
{discountLabel(v)}
|
|
@@ -556,6 +570,11 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
556
570
|
<span class="block text-xs text-[var(--ks-fg-subtle,#71717a)] truncate">
|
|
557
571
|
{metaLine(entry.voucher)}
|
|
558
572
|
</span>
|
|
573
|
+
<Show when={entry.voucher.notes && entry.voucher.notes.trim()}>
|
|
574
|
+
<span class="block text-xs text-[var(--ks-fg-subtle,#71717a)] truncate">
|
|
575
|
+
{highlightMatch(entry.voucher.notes!, debouncedQuery())}
|
|
576
|
+
</span>
|
|
577
|
+
</Show>
|
|
559
578
|
</span>
|
|
560
579
|
<span class="text-xs text-[var(--ks-warning-fg,#fbbf24)] shrink-0 text-right max-w-[45%] truncate">
|
|
561
580
|
{entry.reason}
|
|
@@ -592,6 +611,10 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
592
611
|
>
|
|
593
612
|
<Show when={draft()} fallback="No voucher selected">
|
|
594
613
|
<span class="text-[var(--ks-fg,#ffffff)]">{draft()!.code}</span>
|
|
614
|
+
<Show when={draftNotes()}>
|
|
615
|
+
{" · "}
|
|
616
|
+
<span class="text-[var(--ks-fg-muted,#a1a1aa)]">{draftNotes()}</span>
|
|
617
|
+
</Show>
|
|
595
618
|
{" · "}
|
|
596
619
|
{discountLabel(draft()!).replace("−", "")} off
|
|
597
620
|
</Show>
|