From 3fb5b2f766920fa87e329173c05f130a3994db27 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 13:36:54 +0700 Subject: [PATCH 01/11] docs: add design spec for filter flags by metadata (issue #3739) Add client-side metadata filter feature design: filter popover + chips in FlagTable, pre-filter logic with AND semantics, MetadataFilterPopover component using existing Combobox/Badge/Popover primitives. Co-Authored-By: Claude Opus 4.6 --- ...6-04-04-filter-flags-by-metadata-design.md | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-04-filter-flags-by-metadata-design.md diff --git a/docs/superpowers/specs/2026-04-04-filter-flags-by-metadata-design.md b/docs/superpowers/specs/2026-04-04-filter-flags-by-metadata-design.md new file mode 100644 index 0000000000..e2e383b302 --- /dev/null +++ b/docs/superpowers/specs/2026-04-04-filter-flags-by-metadata-design.md @@ -0,0 +1,222 @@ +# Filter Flags by Metadata — Design Spec + +**Issue**: https://github.com/flipt-io/flipt/issues/3739 +**Date**: 2026-04-04 +**Scope**: UI only (client-side), no backend changes required + +--- + +## Problem + +The Flags list page (`/namespaces/:ns/flags`) only supports text search over name, key, and description. Users who tag flags with metadata key-value pairs (e.g., `team: backend`, `env: production`) have no way to filter the list by those tags. This makes managing large flag sets difficult. + +--- + +## Goals + +- Allow users to filter flags by one or more metadata key-value pairs. +- Multiple filters combine with AND logic (flag must match all active filters). +- No backend or API changes required. +- Consistent with existing Flipt UI patterns (Combobox, Badge, Popover). + +## Non-Goals + +- Server-side filtering via API query params (future enhancement). +- Special-casing a `tags` metadata key — all metadata keys are treated equally. +- Persisting filters across page navigation or sessions. + +--- + +## Architecture + +All changes are confined to `ui/src/`: + +``` +ui/src/ + components/flags/ + FlagTable.tsx ← add filter state, pre-filter logic, toolbar chips + MetadataFilterPopover.tsx ← new component: popover for adding a filter + types/ + Flag.ts ← add MetadataFilter interface (no existing types changed) +``` + +No changes to: +- Backend Go code +- Protobuf definitions +- `flagsApi.ts` API layer +- Any other component outside `components/flags/` + +--- + +## Data Model + +```ts +// ui/src/types/Flag.ts — new export +export interface MetadataFilter { + key: string; // metadata key, e.g. "team" + value: string; // partial match value, e.g. "backend" +} +``` + +--- + +## Filter Logic + +Pre-filter the flags array before passing it to `useReactTable`. This avoids hooking into TanStack Table's filter API and keeps the logic simple. + +```ts +const filteredFlags = useMemo(() => { + if (metadataFilters.length === 0) return flags; + return flags.filter((flag) => + metadataFilters.every(({ key, value }) => { + const metaVal = flag.metadata?.[key]; + if (metaVal === undefined) return false; + return String(metaVal).toLowerCase().includes(value.toLowerCase()); + }) + ); +}, [flags, metadataFilters]); +``` + +- Match is **case-insensitive substring** (consistent with the existing text search behaviour). +- A flag without the metadata key does **not** match a filter on that key. +- All filters must match (AND semantics). OR is out of scope. + +--- + +## Component Design + +### `MetadataFilterPopover` + +A self-contained popover that lets the user pick a metadata key and enter a value, then emits an `onAdd(filter: MetadataFilter)` callback. + +Props: +```ts +interface MetadataFilterPopoverProps { + availableKeys: string[]; // deduplicated keys from all current flags + onAdd: (filter: MetadataFilter) => void; +} +``` + +Internals: +- **Key input**: `Combobox` (existing component) populated with `availableKeys`. Also accepts free-text for keys not yet present in any flag. +- **Value input**: plain `` text field. +- **Add button**: disabled until both key and value are non-empty. Closes the popover on success. +- Uses the existing `Popover` / `PopoverContent` primitives from `~/components/`. + +### Changes to `FlagTable` + +**State additions:** +```ts +const [metadataFilters, setMetadataFilters] = useState([]); +``` + +**Available keys** (memoized): +```ts +const availableMetadataKeys = useMemo(() => + [...new Set(flags.flatMap((f) => Object.keys(f.metadata ?? {})))].sort(), + [flags] +); +``` + +**Toolbar** — between `` and ``: +```tsx + setMetadataFilters((prev) => [...prev, f])} +/> +``` + +**Filter chips** — rendered below the toolbar when `metadataFilters.length > 0`: +```tsx +{metadataFilters.length > 0 && ( +
+ {metadataFilters.map((f, i) => ( + + {f.key}: {f.value} + + + ))} + +
+)} +``` + +**Pass pre-filtered data to `useReactTable`**: +```ts +// Replace `data: flags` with `data: filteredFlags` +const table = useReactTable({ + data: filteredFlags, + ... +}); +``` + +--- + +## UI Layout + +Before (current toolbar): +``` +[🔍 Search...] [View ▼] +``` + +After (no active filters): +``` +[🔍 Search...] [⊕ Filter ▼] [View ▼] +``` + +After (with active filters): +``` +[🔍 Search...] [⊕ Filter ▼] [View ▼] +[×team: backend] [×env: prod] Clear all +``` + +The Filter button uses a `SlidersHorizontal` or `FilterIcon` icon from Lucide (consistent with existing `DataTableViewOptions`). + +--- + +## Empty State + +When no flags match the active filters: +- The existing "No flags matched your search" `` is reused — no new empty state needed. +- The condition that triggers this state must be updated: currently `filter.length > 0`, it must become `filter.length > 0 || metadataFilters.length > 0` so metadata-only filtering also shows this message instead of the "empty namespace" state. + +--- + +## Edge Cases + +| Case | Behaviour | +|------|-----------| +| Flag has no `metadata` field | Does not match any metadata filter | +| Metadata value is a number or boolean | Converted to string for comparison (`String(42)` → `"42"`) | +| Same key added twice with different values | Both filters apply (AND); effectively narrows to flags matching both values for that key — unlikely to find results, but not prevented | +| Metadata values that are objects or arrays | `String({...})` produces `"[object Object]"` — acceptable edge case; not filtered specially | +| Empty `availableKeys` (all flags have no metadata) | Combobox shows empty list; user can still type a key free-form | +| Text search + metadata filters both active | Both apply independently; flags must satisfy both to appear | + +--- + +## Testing + +- Unit tests for the filter logic function (pure function, easy to test). +- Component tests for `MetadataFilterPopover`: renders, adds a filter, clears input. +- Component tests for `FlagTable` active filter chips: add filter → chip appears; remove chip → filter gone; clear all → all chips gone. +- Existing `FlagTable` tests must continue to pass. + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `ui/src/types/Flag.ts` | Add `MetadataFilter` interface | +| `ui/src/components/flags/FlagTable.tsx` | Add state, pre-filter logic, toolbar button, chips | +| `ui/src/components/flags/MetadataFilterPopover.tsx` | New component | +| `ui/src/components/flags/FlagTable.test.tsx` | New/updated tests | +| `ui/src/components/flags/MetadataFilterPopover.test.tsx` | New tests | From f5bc6829be3700ab69ea1105408d962242a02a05 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 13:41:01 +0700 Subject: [PATCH 02/11] docs: add implementation plan for filter flags by metadata Task-by-task TDD plan: MetadataFilter type, applyMetadataFilters utility, MetadataFilterPopover component, FlagTable wiring, smoke test steps. Co-Authored-By: Claude Opus 4.6 --- .../2026-04-04-filter-flags-by-metadata.md | 718 ++++++++++++++++++ 1 file changed, 718 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md diff --git a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md new file mode 100644 index 0000000000..5fc9f5706e --- /dev/null +++ b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md @@ -0,0 +1,718 @@ +# Filter Flags by Metadata — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a client-side metadata filter (popover + chips) to the Flags list page so users can narrow flags by key-value metadata pairs. + +**Architecture:** Pure UI change — no backend or API modifications. A new `MetadataFilterPopover` component handles filter input; `FlagTable` gains local `metadataFilters` state and a `useMemo` pre-filter that runs before TanStack Table sees the data. Active filters render as removable `Badge` chips below the toolbar. + +**Tech Stack:** React 19, TypeScript, TanStack Table v8, Tailwind CSS, Lucide icons, Radix UI Popover (via `~/components/Popover`), `~/components/Combobox`, `~/components/Badge`, `~/components/Button`, `~/components/forms/Input`, Jest + jsdom (unit tests). + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `ui/src/types/Flag.ts` | **Modify** | Add `MetadataFilter` interface | +| `ui/src/components/flags/MetadataFilterPopover.tsx` | **Create** | Popover UI for adding one key=value filter | +| `ui/src/components/flags/FlagTable.tsx` | **Modify** | Filter state, pre-filter logic, toolbar button, chips, empty-state condition | +| `ui/src/components/flags/MetadataFilterPopover.test.tsx` | **Create** | Unit tests for the popover component | +| `ui/src/utils/flagMetadataFilter.ts` | **Create** | Pure filter function (easy to unit-test in isolation) | +| `ui/src/utils/flagMetadataFilter.test.ts` | **Create** | Unit tests for the filter function | + +--- + +## Task 1: Add `MetadataFilter` type + pure filter function + +**Files:** +- Modify: `ui/src/types/Flag.ts` +- Create: `ui/src/utils/flagMetadataFilter.ts` +- Create: `ui/src/utils/flagMetadataFilter.test.ts` + +- [ ] **Step 1.1 — Write the failing tests** + +Create `ui/src/utils/flagMetadataFilter.test.ts`: + +```ts +import { applyMetadataFilters } from './flagMetadataFilter'; +import { FlagType, IFlag, MetadataFilter } from '~/types/Flag'; + +const makeFlag = (key: string, metadata?: Record): IFlag => ({ + key, + name: key, + type: FlagType.VARIANT, + enabled: true, + description: '', + metadata +}); + +describe('applyMetadataFilters', () => { + const flags = [ + makeFlag('flag-a', { team: 'backend', env: 'production' }), + makeFlag('flag-b', { team: 'frontend', env: 'production' }), + makeFlag('flag-c', { team: 'backend', env: 'staging' }), + makeFlag('flag-d'), // no metadata + makeFlag('flag-e', { count: 42, active: true }) + ]; + + it('returns all flags when no filters are active', () => { + expect(applyMetadataFilters(flags, [])).toEqual(flags); + }); + + it('filters by a single exact-match key-value pair', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'backend' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-c']); + }); + + it('filter is case-insensitive', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'BACKEND' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-c']); + }); + + it('filter is a substring match', () => { + const filters: MetadataFilter[] = [{ key: 'env', value: 'prod' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-b']); + }); + + it('multiple filters use AND logic', () => { + const filters: MetadataFilter[] = [ + { key: 'team', value: 'backend' }, + { key: 'env', value: 'production' } + ]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a']); + }); + + it('excludes flags that do not have the filtered key', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'backend' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).not.toContain('flag-d'); + }); + + it('matches numeric metadata values converted to string', () => { + const filters: MetadataFilter[] = [{ key: 'count', value: '42' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-e']); + }); + + it('matches boolean metadata values converted to string', () => { + const filters: MetadataFilter[] = [{ key: 'active', value: 'true' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-e']); + }); +}); +``` + +- [ ] **Step 1.2 — Run tests to confirm they fail** + +```bash +cd ui && npx jest flagMetadataFilter --no-coverage +``` + +Expected: `FAIL` — cannot find module `'./flagMetadataFilter'`. + +- [ ] **Step 1.3 — Add `MetadataFilter` interface to `ui/src/types/Flag.ts`** + +Open `ui/src/types/Flag.ts` and append after the last `export` (after `FilterableFlag`): + +```ts +export interface MetadataFilter { + key: string; + value: string; +} +``` + +- [ ] **Step 1.4 — Create `ui/src/utils/flagMetadataFilter.ts`** + +```ts +import { IFlag, MetadataFilter } from '~/types/Flag'; + +/** + * Filters a list of flags by the given metadata key-value pairs. + * All filters must match (AND semantics). + * Matching is case-insensitive substring on the string-coerced metadata value. + */ +export function applyMetadataFilters( + flags: IFlag[], + filters: MetadataFilter[] +): IFlag[] { + if (filters.length === 0) return flags; + + return flags.filter((flag) => + filters.every(({ key, value }) => { + const metaVal = flag.metadata?.[key]; + if (metaVal === undefined || metaVal === null) return false; + return String(metaVal).toLowerCase().includes(value.toLowerCase()); + }) + ); +} +``` + +- [ ] **Step 1.5 — Run tests to confirm they pass** + +```bash +cd ui && npx jest flagMetadataFilter --no-coverage +``` + +Expected: `PASS` — all 8 tests green. + +- [ ] **Step 1.6 — Commit** + +```bash +cd ui && git add src/types/Flag.ts src/utils/flagMetadataFilter.ts src/utils/flagMetadataFilter.test.ts +git commit -m "feat: add MetadataFilter type and applyMetadataFilters utility" +``` + +--- + +## Task 2: Create `MetadataFilterPopover` component + +**Files:** +- Create: `ui/src/components/flags/MetadataFilterPopover.tsx` +- Create: `ui/src/components/flags/MetadataFilterPopover.test.tsx` + +This component renders a button that opens a popover. Inside: a text input for the metadata key, a text input for the value, and an "Add filter" button. On submit it calls `onAdd(filter)` and resets its own state. + +> **Note on Combobox:** `Combobox` requires items typed as `ISelectable` (`{ key, displayValue }`). We map `availableKeys` to that shape. However since Combobox is a compound Radix Popover component, testing it with jsdom/jest is fragile. We keep the component simple: a plain `` for key (with ``) is easier to unit-test; Combobox is used for the richer UX only in the real component. + +- [ ] **Step 2.1 — Write the failing tests** + +Create `ui/src/components/flags/MetadataFilterPopover.test.tsx`: + +```tsx +/** + * @jest-environment jsdom + */ +import { render, screen, fireEvent } from '@testing-library/react'; +import MetadataFilterPopover from './MetadataFilterPopover'; +import { MetadataFilter } from '~/types/Flag'; + +// Radix Popover uses portals. We mock it so the content always renders inline. +jest.mock('~/components/Popover', () => ({ + Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
+})); + +describe('MetadataFilterPopover', () => { + it('renders a "Filter" trigger button', () => { + render(); + expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument(); + }); + + it('calls onAdd with the entered key and value when Add is clicked', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).toHaveBeenCalledWith<[MetadataFilter]>({ + key: 'team', + value: 'backend' + }); + }); + + it('does not call onAdd when key is empty', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).not.toHaveBeenCalled(); + }); + + it('does not call onAdd when value is empty', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).not.toHaveBeenCalled(); + }); + + it('resets key and value inputs after a successful add', () => { + const onAdd = jest.fn(); + render(); + + const keyInput = screen.getByPlaceholderText(/key/i); + const valueInput = screen.getByPlaceholderText(/value/i); + + fireEvent.change(keyInput, { target: { value: 'team' } }); + fireEvent.change(valueInput, { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect((keyInput as HTMLInputElement).value).toBe(''); + expect((valueInput as HTMLInputElement).value).toBe(''); + }); +}); +``` + +- [ ] **Step 2.2 — Check test infra: confirm `@testing-library/react` is installed** + +```bash +cd ui && cat package.json | grep testing-library +``` + +Expected: see `"@testing-library/react"` in devDependencies. If not present: +```bash +cd ui && npm install --save-dev @testing-library/react @testing-library/jest-dom +``` + +Then check `ui/jest.config.ts` for `setupFilesAfterFramework` pointing to a setup file that calls `import '@testing-library/jest-dom'`. If absent, add to `jest.config.ts`: +```ts +setupFilesAfterFramework: ['/src/setupTests.ts'], +``` +> Note: the correct Jest config key is `setupFilesAfterFramework` (not `setupFilesAfterEach`). Verify the exact key name in your Jest version if this causes a type error — it may be `setupFilesAfterEnv` in older configs. +and create `ui/src/setupTests.ts`: +```ts +import '@testing-library/jest-dom'; +``` + +- [ ] **Step 2.3 — Run tests to confirm they fail** + +```bash +cd ui && npx jest MetadataFilterPopover --no-coverage +``` + +Expected: `FAIL` — cannot find module `'./MetadataFilterPopover'`. + +- [ ] **Step 2.4 — Create `ui/src/components/flags/MetadataFilterPopover.tsx`** + +```tsx +import { SlidersHorizontalIcon } from 'lucide-react'; +import { useState } from 'react'; + +import { Button } from '~/components/Button'; +import Input from '~/components/forms/Input'; +import { + Popover, + PopoverContent, + PopoverTrigger +} from '~/components/Popover'; + +import { MetadataFilter } from '~/types/Flag'; + +interface MetadataFilterPopoverProps { + availableKeys: string[]; + onAdd: (filter: MetadataFilter) => void; +} + +export default function MetadataFilterPopover({ + availableKeys, + onAdd +}: MetadataFilterPopoverProps) { + const [open, setOpen] = useState(false); + const [key, setKey] = useState(''); + const [value, setValue] = useState(''); + + const canAdd = key.trim().length > 0 && value.trim().length > 0; + + const handleAdd = () => { + if (!canAdd) return; + onAdd({ key: key.trim(), value: value.trim() }); + setKey(''); + setValue(''); + setOpen(false); + }; + + return ( + + + + + + +

Filter by metadata

+ + {/* Key input with datalist for autocomplete */} +
+ + setKey(e.target.value)} + list="mf-available-keys" + className="w-full" + /> + + {availableKeys.map((k) => ( + +
+ + {/* Value input */} +
+ + setValue(e.target.value)} + className="w-full" + onKeyDown={(e) => { + if (e.key === 'Enter') handleAdd(); + }} + /> +
+ + +
+
+ ); +} +``` + +- [ ] **Step 2.5 — Run tests to confirm they pass** + +```bash +cd ui && npx jest MetadataFilterPopover --no-coverage +``` + +Expected: `PASS` — all 5 tests green. + +- [ ] **Step 2.6 — Commit** + +```bash +git add ui/src/components/flags/MetadataFilterPopover.tsx \ + ui/src/components/flags/MetadataFilterPopover.test.tsx +git commit -m "feat: add MetadataFilterPopover component" +``` + +--- + +## Task 3: Wire metadata filtering into `FlagTable` + +**Files:** +- Modify: `ui/src/components/flags/FlagTable.tsx` + +This task adds: +1. `metadataFilters` state +2. `availableMetadataKeys` memo +3. `filteredFlags` memo (using `applyMetadataFilters`) +4. `MetadataFilterPopover` button in toolbar +5. Filter chips row below toolbar +6. Updated empty-state condition + +- [ ] **Step 3.1 — Read the current `FlagTable.tsx` imports section** + +Open `ui/src/components/flags/FlagTable.tsx`, lines 1–46. You will add new imports at the top. + +- [ ] **Step 3.2 — Add new imports** + +In `ui/src/components/flags/FlagTable.tsx`, extend the existing import block. Add these four import lines **after** the existing imports (after line 46): + +```ts +import { XIcon } from 'lucide-react'; + +import MetadataFilterPopover from '~/components/flags/MetadataFilterPopover'; + +import { MetadataFilter } from '~/types/Flag'; + +import { applyMetadataFilters } from '~/utils/flagMetadataFilter'; +``` + +> `XIcon` is already available from the `lucide-react` package used throughout the project. + +- [ ] **Step 3.3 — Add `metadataFilters` state and derived memos** + +Inside the `FlagTable` function body, after the existing line: + +```ts +const flags = useMemo(() => data?.flags || [], [data]); +``` + +Add: + +```ts +const [metadataFilters, setMetadataFilters] = useState([]); + +const availableMetadataKeys = useMemo( + () => + [...new Set(flags.flatMap((f) => Object.keys(f.metadata ?? {})))].sort(), + [flags] +); + +const filteredFlags = useMemo( + () => applyMetadataFilters(flags, metadataFilters), + [flags, metadataFilters] +); +``` + +- [ ] **Step 3.4 — Replace `data: flags` with `data: filteredFlags` in `useReactTable`** + +Find the line inside `useReactTable({`: +```ts + data: flags, +``` +Change it to: +```ts + data: filteredFlags, +``` + +- [ ] **Step 3.5 — Add the Filter button to the toolbar** + +Find the existing toolbar JSX block in the `return` statement: + +```tsx +
+ +
+``` + +Replace it with: + +```tsx +
+ + + setMetadataFilters((prev) => [...prev, f]) + } + /> +
+``` + +- [ ] **Step 3.6 — Add the filter chips row** + +Directly after the toolbar `
` (after the closing `
` of the `flex items-center justify-between` wrapper), add: + +```tsx + {metadataFilters.length > 0 && ( +
+ {metadataFilters.map((f, i) => ( + + + {f.key}: {f.value} + + + + ))} + +
+ )} +``` + +- [ ] **Step 3.7 — Update the empty-state condition** + +Find the existing empty-state condition: + +```tsx + {table.getRowCount() === 0 && filter.length === 0 && ( + + )} + {table.getRowCount() === 0 && filter.length > 0 && ( +``` + +Replace both conditions with: + +```tsx + {table.getRowCount() === 0 && + filter.length === 0 && + metadataFilters.length === 0 && ( + + )} + {table.getRowCount() === 0 && + (filter.length > 0 || metadataFilters.length > 0) && ( +``` + +- [ ] **Step 3.8 — Type-check** + +```bash +cd ui && npx tsc --noEmit +``` + +Expected: no errors. If you see `XIcon` not found, verify the lucide-react version supports it — alternative name is `X`. Replace `XIcon` with `X` and update the import accordingly if needed: +```ts +import { X } from 'lucide-react'; +// then use +``` + +- [ ] **Step 3.9 — Run existing tests to confirm nothing broke** + +```bash +cd ui && npx jest --no-coverage +``` + +Expected: all pre-existing tests still pass. + +- [ ] **Step 3.10 — Commit** + +```bash +git add ui/src/components/flags/FlagTable.tsx +git commit -m "feat: wire metadata filter state and UI into FlagTable" +``` + +--- + +## Task 4: Manual smoke test + +No automated test can replace a quick visual check. These steps require the Flipt dev server running. + +- [ ] **Step 4.1 — Start the dev server** + +```bash +# Terminal 1 — backend (if not already running) +cd && go run ./cmd/flipt server + +# Terminal 2 — frontend +cd ui && npm run dev +``` + +Open http://localhost:5173 in a browser. + +- [ ] **Step 4.2 — Create test flags with metadata** + +In the UI, create three flags with the following metadata (Settings → Metadata tab of the flag form): + +| Flag key | Metadata | +|----------|----------| +| `flag-backend` | `team=backend`, `env=production` | +| `flag-frontend` | `team=frontend`, `env=production` | +| `flag-staging` | `team=backend`, `env=staging` | + +- [ ] **Step 4.3 — Verify Filter button appears** + +Navigate to `/namespaces/default/flags`. Confirm a "Filter" button (with sliders icon) appears next to the search box. + +- [ ] **Step 4.4 — Add a single filter** + +Click Filter → enter key `team`, value `backend` → click "Add filter". + +Expected: +- A chip `team: backend ×` appears below the toolbar. +- Only `flag-backend` and `flag-staging` are visible. + +- [ ] **Step 4.5 — Add a second filter** + +Click Filter again → enter key `env`, value `production` → click "Add filter". + +Expected: +- Two chips shown: `team: backend ×` and `env: production ×`. +- Only `flag-backend` is visible (AND logic). + +- [ ] **Step 4.6 — Remove one filter** + +Click `×` on the `env: production` chip. + +Expected: +- Only the `team: backend` chip remains. +- Both `flag-backend` and `flag-staging` are visible again. + +- [ ] **Step 4.7 — Clear all filters** + +Click "Clear all". + +Expected: +- No chips shown. +- All flags visible. + +- [ ] **Step 4.8 — Empty state with metadata filter** + +Add a filter `key=nonexistent`, value=`x`. + +Expected: +- "No flags matched your search" well is shown (not the "Create Your First Flag" state). + +- [ ] **Step 4.9 — Combined text + metadata filter** + +In the search box type `backend`. Then add metadata filter `env=production`. + +Expected: only flags matching both text search AND metadata filter are shown. + +--- + +## Task 5: Final check and cleanup + +- [ ] **Step 5.1 — Run full test suite one more time** + +```bash +cd ui && npx jest --no-coverage +``` + +Expected: all tests pass. + +- [ ] **Step 5.2 — TypeScript check** + +```bash +cd ui && npx tsc --noEmit +``` + +Expected: no errors. + +- [ ] **Step 5.3 — Lint** + +```bash +cd ui && npm run lint +``` + +Expected: no new lint errors. Fix any that appear (usually unused imports). + +- [ ] **Step 5.4 — Final commit (if any lint fixes were needed)** + +```bash +git add -A +git commit -m "chore: fix lint warnings after metadata filter implementation" +``` From 3237cbef07688d146fa49c933c0b4be3bb5a01c5 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 13:53:55 +0700 Subject: [PATCH 03/11] fix(plan): address all Codex review issues (R1+R2) - ISSUE-1: Replace Formik Input with BaseInput in MetadataFilterPopover - ISSUE-2: Add conditional infra files to File Map - ISSUE-3: Correct Jest config key to setupFilesAfterEnv - ISSUE-4: Add FlagTable integration tests (8 cases incl. combined filter) - ISSUE-5: Document filter semantics decision explicitly - ISSUE-6: Fix FlagTable test mocks to use actual import paths Co-Authored-By: Claude Opus 4.6 --- .../2026-04-04-filter-flags-by-metadata.md | 206 ++++++++++++++++-- 1 file changed, 193 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md index 5fc9f5706e..e233d9e551 100644 --- a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md +++ b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md @@ -6,7 +6,9 @@ **Architecture:** Pure UI change — no backend or API modifications. A new `MetadataFilterPopover` component handles filter input; `FlagTable` gains local `metadataFilters` state and a `useMemo` pre-filter that runs before TanStack Table sees the data. Active filters render as removable `Badge` chips below the toolbar. -**Tech Stack:** React 19, TypeScript, TanStack Table v8, Tailwind CSS, Lucide icons, Radix UI Popover (via `~/components/Popover`), `~/components/Combobox`, `~/components/Badge`, `~/components/Button`, `~/components/forms/Input`, Jest + jsdom (unit tests). +**Tech Stack:** React 19, TypeScript, TanStack Table v8, Tailwind CSS, Lucide icons, Radix UI Popover (via `~/components/Popover`), `~/components/Badge`, `~/components/Button`, `~/components/BaseInput` (non-Formik), Jest + jsdom (unit tests). + +**Filter semantics decision:** Case-insensitive substring match on string-coerced metadata values. This is intentional UX — users benefit from "prod" matching "production", and "BACKEND" matching "backend". Exact matching would require knowing the exact capitalization. This decision is explicit and locked: do not change without product sign-off. --- @@ -18,8 +20,12 @@ | `ui/src/components/flags/MetadataFilterPopover.tsx` | **Create** | Popover UI for adding one key=value filter | | `ui/src/components/flags/FlagTable.tsx` | **Modify** | Filter state, pre-filter logic, toolbar button, chips, empty-state condition | | `ui/src/components/flags/MetadataFilterPopover.test.tsx` | **Create** | Unit tests for the popover component | +| `ui/src/components/flags/FlagTable.test.tsx` | **Create** | Unit tests for toolbar wiring, chips, AND logic, empty states | | `ui/src/utils/flagMetadataFilter.ts` | **Create** | Pure filter function (easy to unit-test in isolation) | | `ui/src/utils/flagMetadataFilter.test.ts` | **Create** | Unit tests for the filter function | +| `ui/jest.config.ts` | **Modify (conditional)** | Add `setupFilesAfterEnv` only if `@testing-library/jest-dom` is not already configured | +| `ui/src/setupTests.ts` | **Create (conditional)** | Import `@testing-library/jest-dom` — only if not already present in repo | +| `ui/package.json` | **Modify (conditional)** | Add `@testing-library/react`, `@testing-library/jest-dom` devDeps only if absent | --- @@ -177,7 +183,7 @@ git commit -m "feat: add MetadataFilter type and applyMetadataFilters utility" This component renders a button that opens a popover. Inside: a text input for the metadata key, a text input for the value, and an "Add filter" button. On submit it calls `onAdd(filter)` and resets its own state. -> **Note on Combobox:** `Combobox` requires items typed as `ISelectable` (`{ key, displayValue }`). We map `availableKeys` to that shape. However since Combobox is a compound Radix Popover component, testing it with jsdom/jest is fragile. We keep the component simple: a plain `` for key (with ``) is easier to unit-test; Combobox is used for the richer UX only in the real component. +> **Important — use `BaseInput`, NOT `Input` from `~/components/forms/Input`:** `~/components/forms/Input` calls `useField(props)` (Formik hook) and requires a Formik context. `~/components/BaseInput` is a plain React `input` wrapper with no Formik dependency — it accepts standard `React.ComponentProps<'input'>` and is the correct choice for uncontrolled/controlled inputs outside of Formik forms. - [ ] **Step 2.1 — Write the failing tests** @@ -266,7 +272,7 @@ describe('MetadataFilterPopover', () => { - [ ] **Step 2.2 — Check test infra: confirm `@testing-library/react` is installed** ```bash -cd ui && cat package.json | grep testing-library +cd ui && grep "@testing-library" package.json ``` Expected: see `"@testing-library/react"` in devDependencies. If not present: @@ -274,11 +280,15 @@ Expected: see `"@testing-library/react"` in devDependencies. If not present: cd ui && npm install --save-dev @testing-library/react @testing-library/jest-dom ``` -Then check `ui/jest.config.ts` for `setupFilesAfterFramework` pointing to a setup file that calls `import '@testing-library/jest-dom'`. If absent, add to `jest.config.ts`: +Then check `ui/jest.config.ts` for `setupFilesAfterEnv` (this is the correct Jest key — the repo already uses it): +```bash +grep "setupFilesAfterEnv" ui/jest.config.ts +``` + +If it's already present and points to a setup file that imports `@testing-library/jest-dom`, no changes needed. If `setupFilesAfterEnv` exists but the setup file does NOT import `@testing-library/jest-dom`, add the import to that file. If `setupFilesAfterEnv` is absent entirely, add to `jest.config.ts`: ```ts -setupFilesAfterFramework: ['/src/setupTests.ts'], +setupFilesAfterEnv: ['/src/setupTests.ts'], ``` -> Note: the correct Jest config key is `setupFilesAfterFramework` (not `setupFilesAfterEach`). Verify the exact key name in your Jest version if this causes a type error — it may be `setupFilesAfterEnv` in older configs. and create `ui/src/setupTests.ts`: ```ts import '@testing-library/jest-dom'; @@ -298,8 +308,8 @@ Expected: `FAIL` — cannot find module `'./MetadataFilterPopover'`. import { SlidersHorizontalIcon } from 'lucide-react'; import { useState } from 'react'; +import { BaseInput } from '~/components/BaseInput'; import { Button } from '~/components/Button'; -import Input from '~/components/forms/Input'; import { Popover, PopoverContent, @@ -353,9 +363,8 @@ export default function MetadataFilterPopover({ - Value - ({ + Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
+})); + +// ── Mock RTK Query hooks (actual import paths from FlagTable.tsx) ───────── +const mockFlags = [ + { key: 'flag-a', name: 'Flag A', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'production' } }, + { key: 'flag-b', name: 'Flag B', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'frontend', env: 'production' } }, + { key: 'flag-c', name: 'Flag C', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'staging' } } +]; + +jest.mock('~/app/flags/flagsApi', () => ({ + useListFlagsQuery: () => ({ data: { flags: mockFlags }, isLoading: false, error: null }), + selectSorting: () => [], + setSorting: (s: any) => ({ type: 'SET_SORTING', payload: s }) +})); + +jest.mock('~/app/flags/analyticsApi', () => ({ + useGetBatchFlagEvaluationCountQuery: () => ({ data: null }) +})); + +jest.mock('~/app/meta/metaSlice', () => ({ + selectInfo: () => ({ analytics: { enabled: false } }) +})); + +// ── Helpers ─────────────────────────────────────────────────────────────── +const mockEnvironment = { key: 'default', name: 'default' }; +const mockNamespace = { key: 'default', name: 'default', description: '' }; + +function renderTable() { + const store = configureStore({ reducer: { flags: (s = {}) => s, meta: (s = {}) => s } }); + return render( + + + + ); +} + +describe('FlagTable — metadata filter', () => { + it('renders all flags with no filter active', () => { + renderTable(); + expect(screen.getByText('Flag A')).toBeInTheDocument(); + expect(screen.getByText('Flag B')).toBeInTheDocument(); + expect(screen.getByText('Flag C')).toBeInTheDocument(); + }); + + it('renders Filter button in toolbar', () => { + renderTable(); + expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument(); + }); + + it('adds a chip after applying a metadata filter', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + expect(screen.getByText(/team: backend/i)).toBeInTheDocument(); + }); + + it('hides non-matching flags after filter is applied', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); + }); + + it('removes a chip when × is clicked and restores all flags', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + fireEvent.click(screen.getByRole('button', { name: /remove filter team:backend/i })); + + expect(screen.getByText('Flag B')).toBeInTheDocument(); + expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); + }); + + it('applies AND logic for two metadata filters', () => { + renderTable(); + + // Filter 1: team=backend + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Filter 2: env=production + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'env' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'production' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Only flag-a matches both + expect(screen.getByText('Flag A')).toBeInTheDocument(); + expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); + expect(screen.queryByText('Flag C')).not.toBeInTheDocument(); + }); + + it('shows "no flags matched" empty state when filter matches nothing (not create-first-flag state)', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'nonexistent' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'x' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(screen.getByText(/no flags matched/i)).toBeInTheDocument(); + }); + + it('clears all filters when Clear all is clicked', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + fireEvent.click(screen.getByText('Clear all')); + + expect(screen.getByText('Flag B')).toBeInTheDocument(); + expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); + }); + + it('applies both text search and metadata filter simultaneously (AND)', () => { + renderTable(); + + // Text search: "A" (matches Flag A only among backend flags) + fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'A' } }); + + // Metadata filter: team=backend (matches flag-a and flag-c) + fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Only Flag A matches both "A" text search AND team=backend metadata + expect(screen.getByText('Flag A')).toBeInTheDocument(); + expect(screen.queryByText('Flag C')).not.toBeInTheDocument(); + expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 4.4 — Commit** + +```bash +git add ui/src/components/flags/FlagTable.test.tsx +git commit -m "test: add FlagTable integration tests for metadata filter" +``` + +--- + +## Task 5: Manual smoke test No automated test can replace a quick visual check. These steps require the Flipt dev server running. @@ -684,7 +864,7 @@ Expected: only flags matching both text search AND metadata filter are shown. --- -## Task 5: Final check and cleanup +## Task 6: Final check and cleanup - [ ] **Step 5.1 — Run full test suite one more time** From e2d0edb1c71e6b3087730e4ab835d6d26b408d2a Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 13:56:51 +0700 Subject: [PATCH 04/11] fix(plan): address Codex Round 3 issues (R3) - ISSUE-7: Add jest.useFakeTimers()/act(advanceTimersByTime(600)) around Searchbox debounce in combined text+metadata test - ISSUE-8: Wrap FlagTable in MemoryRouter to satisfy useNavigate() context requirement from FlagListItem and EmptyFlagList child components Co-Authored-By: Claude Opus 4.6 --- .../2026-04-04-filter-flags-by-metadata.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md index e233d9e551..79400cbb11 100644 --- a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md +++ b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md @@ -633,8 +633,9 @@ Create `ui/src/components/flags/FlagTable.test.tsx`: * @jest-environment jsdom */ import { configureStore } from '@reduxjs/toolkit'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router'; import FlagTable from './FlagTable'; // ── Mock Radix Popover (portal-based, doesn't render in jsdom) ──────────── @@ -672,12 +673,18 @@ const mockNamespace = { key: 'default', name: 'default', description: '' }; function renderTable() { const store = configureStore({ reducer: { flags: (s = {}) => s, meta: (s = {}) => s } }); return render( - - - + + + + + ); } +// ── Fake timers for Searchbox debounce (defaults to 500ms) ──────────────── +beforeEach(() => jest.useFakeTimers()); +afterEach(() => jest.useRealTimers()); + describe('FlagTable — metadata filter', () => { it('renders all flags with no filter active', () => { renderTable(); @@ -762,10 +769,11 @@ describe('FlagTable — metadata filter', () => { it('applies both text search and metadata filter simultaneously (AND)', () => { renderTable(); - // Text search: "A" (matches Flag A only among backend flags) + // Type in Searchbox — then advance fake timers past 500ms debounce fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'A' } }); + act(() => { jest.advanceTimersByTime(600); }); - // Metadata filter: team=backend (matches flag-a and flag-c) + // Metadata filter: team=backend (matches flag-a and flag-c, but flag-c name doesn't contain "A") fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); fireEvent.click(screen.getByRole('button', { name: /add filter/i })); From c4e03a3da46a5ad9275f6735e7c80209289c88da Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:00:23 +0700 Subject: [PATCH 05/11] fix(plan): address Codex Round 4 issue (R4) - ISSUE-9: Replace ambiguous mockFlags data + search term. Renamed to Alpha/Beta/Gamma with distinct keys to avoid 'Flag'/'a' substring false-matches. Combined test now uses 'alpha' as search term (uniquely matches key='alpha', name='Alpha' only), making the combined text+metadata AND assertion unambiguous. Co-Authored-By: Claude Opus 4.6 --- .../2026-04-04-filter-flags-by-metadata.md | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md index 79400cbb11..e1d707d1d5 100644 --- a/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md +++ b/docs/superpowers/plans/2026-04-04-filter-flags-by-metadata.md @@ -647,9 +647,9 @@ jest.mock('~/components/Popover', () => ({ // ── Mock RTK Query hooks (actual import paths from FlagTable.tsx) ───────── const mockFlags = [ - { key: 'flag-a', name: 'Flag A', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'production' } }, - { key: 'flag-b', name: 'Flag B', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'frontend', env: 'production' } }, - { key: 'flag-c', name: 'Flag C', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'staging' } } + { key: 'alpha', name: 'Alpha', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'production' } }, + { key: 'beta', name: 'Beta', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'frontend', env: 'production' } }, + { key: 'gamma', name: 'Gamma', type: 'VARIANT_FLAG_TYPE', enabled: true, description: '', metadata: { team: 'backend', env: 'staging' } } ]; jest.mock('~/app/flags/flagsApi', () => ({ @@ -688,9 +688,9 @@ afterEach(() => jest.useRealTimers()); describe('FlagTable — metadata filter', () => { it('renders all flags with no filter active', () => { renderTable(); - expect(screen.getByText('Flag A')).toBeInTheDocument(); - expect(screen.getByText('Flag B')).toBeInTheDocument(); - expect(screen.getByText('Flag C')).toBeInTheDocument(); + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); + expect(screen.getByText('Gamma')).toBeInTheDocument(); }); it('renders Filter button in toolbar', () => { @@ -711,7 +711,7 @@ describe('FlagTable — metadata filter', () => { fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); fireEvent.click(screen.getByRole('button', { name: /add filter/i })); - expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); }); it('removes a chip when × is clicked and restores all flags', () => { @@ -722,7 +722,7 @@ describe('FlagTable — metadata filter', () => { fireEvent.click(screen.getByRole('button', { name: /remove filter team:backend/i })); - expect(screen.getByText('Flag B')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); }); @@ -739,10 +739,10 @@ describe('FlagTable — metadata filter', () => { fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'production' } }); fireEvent.click(screen.getByRole('button', { name: /add filter/i })); - // Only flag-a matches both - expect(screen.getByText('Flag A')).toBeInTheDocument(); - expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); - expect(screen.queryByText('Flag C')).not.toBeInTheDocument(); + // Only alpha matches both (gamma has team=backend but doesn't match 'alpha' text) + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); + expect(screen.queryByText('Gamma')).not.toBeInTheDocument(); }); it('shows "no flags matched" empty state when filter matches nothing (not create-first-flag state)', () => { @@ -762,26 +762,29 @@ describe('FlagTable — metadata filter', () => { fireEvent.click(screen.getByText('Clear all')); - expect(screen.getByText('Flag B')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); }); it('applies both text search and metadata filter simultaneously (AND)', () => { renderTable(); - // Type in Searchbox — then advance fake timers past 500ms debounce - fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'A' } }); - act(() => { jest.advanceTimersByTime(600); }); + // Text search: "alpha" — uniquely matches only the 'Alpha' row (key='alpha', name='Alpha'). + // 'Beta' and 'Gamma' contain no substring 'alpha' (case-insensitive). + // Alone, this text filter returns: [Alpha] + fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'alpha' } }); + act(() => { jest.advanceTimersByTime(600); }); // flush 500ms Searchbox debounce - // Metadata filter: team=backend (matches flag-a and flag-c, but flag-c name doesn't contain "A") + // Metadata filter: team=backend — alone would return [Alpha, Gamma] fireEvent.change(screen.getByPlaceholderText(/key/i), { target: { value: 'team' } }); fireEvent.change(screen.getByPlaceholderText(/value/i), { target: { value: 'backend' } }); fireEvent.click(screen.getByRole('button', { name: /add filter/i })); - // Only Flag A matches both "A" text search AND team=backend metadata - expect(screen.getByText('Flag A')).toBeInTheDocument(); - expect(screen.queryByText('Flag C')).not.toBeInTheDocument(); - expect(screen.queryByText('Flag B')).not.toBeInTheDocument(); + // Combined AND: text("alpha") ∩ metadata(team=backend) = [Alpha] only + // — proves that both filters are applied simultaneously and independently + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); + expect(screen.queryByText('Gamma')).not.toBeInTheDocument(); }); }); ``` From f43594d82f65a3db0e89ea986669a21e9a55a2d1 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:03:48 +0700 Subject: [PATCH 06/11] chore: add .worktrees/ to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ec4675b702..f075da4197 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ go.work.sum docs/ .beads + +# Git worktrees +.worktrees/ From 7d5032d34c6bb8a54bfb926a50745588712ebad4 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:09:28 +0700 Subject: [PATCH 07/11] feat: add MetadataFilter type and applyMetadataFilters utility Signed-off-by: hiepau1231 --- ui/src/types/Flag.ts | 5 ++ ui/src/utils/flagMetadataFilter.test.ts | 70 +++++++++++++++++++++++++ ui/src/utils/flagMetadataFilter.ts | 21 ++++++++ 3 files changed, 96 insertions(+) create mode 100644 ui/src/utils/flagMetadataFilter.test.ts create mode 100644 ui/src/utils/flagMetadataFilter.ts diff --git a/ui/src/types/Flag.ts b/ui/src/types/Flag.ts index a5bbe3a582..65bb4b61eb 100644 --- a/ui/src/types/Flag.ts +++ b/ui/src/types/Flag.ts @@ -46,3 +46,8 @@ export interface IFlagList extends IPageable { } export type FilterableFlag = IFlag & ISelectable; + +export interface MetadataFilter { + key: string; + value: string; +} diff --git a/ui/src/utils/flagMetadataFilter.test.ts b/ui/src/utils/flagMetadataFilter.test.ts new file mode 100644 index 0000000000..3a0e1b53f7 --- /dev/null +++ b/ui/src/utils/flagMetadataFilter.test.ts @@ -0,0 +1,70 @@ +import { applyMetadataFilters } from './flagMetadataFilter'; +import { FlagType, IFlag, MetadataFilter } from '~/types/Flag'; + +const makeFlag = (key: string, metadata?: Record): IFlag => ({ + key, + name: key, + type: FlagType.VARIANT, + enabled: true, + description: '', + metadata +}); + +describe('applyMetadataFilters', () => { + const flags = [ + makeFlag('flag-a', { team: 'backend', env: 'production' }), + makeFlag('flag-b', { team: 'frontend', env: 'production' }), + makeFlag('flag-c', { team: 'backend', env: 'staging' }), + makeFlag('flag-d'), // no metadata + makeFlag('flag-e', { count: 42, active: true }) + ]; + + it('returns all flags when no filters are active', () => { + expect(applyMetadataFilters(flags, [])).toEqual(flags); + }); + + it('filters by a single exact-match key-value pair', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'backend' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-c']); + }); + + it('filter is case-insensitive', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'BACKEND' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-c']); + }); + + it('filter is a substring match', () => { + const filters: MetadataFilter[] = [{ key: 'env', value: 'prod' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a', 'flag-b']); + }); + + it('multiple filters use AND logic', () => { + const filters: MetadataFilter[] = [ + { key: 'team', value: 'backend' }, + { key: 'env', value: 'production' } + ]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-a']); + }); + + it('excludes flags that do not have the filtered key', () => { + const filters: MetadataFilter[] = [{ key: 'team', value: 'backend' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).not.toContain('flag-d'); + }); + + it('matches numeric metadata values converted to string', () => { + const filters: MetadataFilter[] = [{ key: 'count', value: '42' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-e']); + }); + + it('matches boolean metadata values converted to string', () => { + const filters: MetadataFilter[] = [{ key: 'active', value: 'true' }]; + const result = applyMetadataFilters(flags, filters); + expect(result.map((f) => f.key)).toEqual(['flag-e']); + }); +}); diff --git a/ui/src/utils/flagMetadataFilter.ts b/ui/src/utils/flagMetadataFilter.ts new file mode 100644 index 0000000000..1663fb25a0 --- /dev/null +++ b/ui/src/utils/flagMetadataFilter.ts @@ -0,0 +1,21 @@ +import { IFlag, MetadataFilter } from '~/types/Flag'; + +/** + * Filters a list of flags by the given metadata key-value pairs. + * All filters must match (AND semantics). + * Matching is case-insensitive substring on the string-coerced metadata value. + */ +export function applyMetadataFilters( + flags: IFlag[], + filters: MetadataFilter[] +): IFlag[] { + if (filters.length === 0) return flags; + + return flags.filter((flag) => + filters.every(({ key, value }) => { + const metaVal = flag.metadata?.[key]; + if (metaVal === undefined || metaVal === null) return false; + return String(metaVal).toLowerCase().includes(value.toLowerCase()); + }) + ); +} From 81e0fe82d7b0fa8aef33cacbc6483e02c206bb91 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:17:31 +0700 Subject: [PATCH 08/11] feat: add MetadataFilterPopover component with tests - Add @testing-library/react, jest-dom, @babel/preset-react for JSX test support - Configure setupFilesAfterEnv with jest-dom matchers Signed-off-by: hiepau1231 --- ui/babel.config.cjs | 1 + ui/jest.config.ts | 2 +- ui/package-lock.json | 1487 ++++++++++------- ui/package.json | 8 +- .../flags/MetadataFilterPopover.test.tsx | 77 + .../flags/MetadataFilterPopover.tsx | 112 ++ ui/src/setupTests.ts | 1 + 7 files changed, 1103 insertions(+), 585 deletions(-) create mode 100644 ui/src/components/flags/MetadataFilterPopover.test.tsx create mode 100644 ui/src/components/flags/MetadataFilterPopover.tsx create mode 100644 ui/src/setupTests.ts diff --git a/ui/babel.config.cjs b/ui/babel.config.cjs index ca92638f5f..fb170a181b 100644 --- a/ui/babel.config.cjs +++ b/ui/babel.config.cjs @@ -1,6 +1,7 @@ module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], + ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript' ] }; diff --git a/ui/jest.config.ts b/ui/jest.config.ts index ddaa6f6e40..c7ce7ce25f 100644 --- a/ui/jest.config.ts +++ b/ui/jest.config.ts @@ -137,7 +137,7 @@ export default { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ['/src/setupTests.ts'], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/ui/package-lock.json b/ui/package-lock.json index f782794e4c..df152f8dd6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -59,10 +59,16 @@ "zod": "^3.25.76" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.2", + "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@playwright/test": "^1.58.2", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/vite": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/jest": "^29.5.14", "@types/loadable__component": "^5.13.10", @@ -110,18 +116,12 @@ "vite": "^6.4.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } + "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", @@ -145,12 +145,12 @@ "license": "ISC" }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -159,9 +159,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -169,22 +169,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -227,13 +227,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -256,13 +256,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -273,18 +273,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -295,13 +295,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", - "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -312,16 +313,17 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -351,28 +353,28 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -395,9 +397,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -405,14 +407,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -422,15 +425,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -482,40 +485,41 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -525,13 +529,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -541,12 +546,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -556,12 +562,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -571,14 +578,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -588,13 +596,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -800,12 +809,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -815,12 +825,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -854,13 +865,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1004,12 +1015,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1019,14 +1031,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1036,14 +1049,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1053,12 +1067,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1068,12 +1083,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1083,13 +1099,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1099,13 +1116,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1115,17 +1133,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1135,13 +1154,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1151,12 +1171,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1166,13 +1188,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1182,12 +1205,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1197,13 +1221,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1213,12 +1238,30 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1228,12 +1271,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1243,12 +1287,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1274,13 +1319,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1290,14 +1336,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1307,12 +1354,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1322,12 +1370,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1337,12 +1386,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1352,12 +1402,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1367,13 +1418,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1383,14 +1435,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1400,15 +1452,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1418,13 +1471,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1434,13 +1488,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1450,12 +1505,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1465,12 +1521,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1480,12 +1537,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1495,14 +1553,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1512,13 +1573,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1528,12 +1590,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1543,13 +1606,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1559,12 +1623,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1574,13 +1639,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1590,14 +1656,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1607,12 +1674,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1622,12 +1690,13 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", - "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1637,16 +1706,17 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1656,12 +1726,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", - "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.25.9" + "@babel/plugin-transform-react-jsx": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1703,13 +1774,14 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", - "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1719,13 +1791,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1735,13 +1807,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1751,12 +1824,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1786,12 +1860,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1801,13 +1876,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1817,12 +1893,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1832,12 +1909,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1847,12 +1925,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1882,12 +1961,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1897,13 +1977,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1913,13 +1994,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1929,13 +2011,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1945,79 +2028,81 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz", - "integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", + "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -2028,13 +2113,14 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2055,17 +2141,18 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz", - "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-react-display-name": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/plugin-transform-react-jsx-development": "^7.25.9", - "@babel/plugin-transform-react-pure-annotations": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2104,31 +2191,31 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -2136,9 +2223,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -6514,6 +6601,145 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@trivago/prettier-plugin-sort-imports": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", @@ -6583,6 +6809,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -7995,13 +8229,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -8022,12 +8257,13 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -8130,6 +8366,19 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", + "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bezier-easing": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", @@ -8159,9 +8408,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -8177,11 +8426,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -8328,9 +8579,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", "dev": true, "funding": [ { @@ -8345,7 +8596,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -8540,12 +8792,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.24.3" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -8622,6 +8875,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -8835,9 +9095,10 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -8919,6 +9180,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", @@ -8986,6 +9258,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -9023,10 +9303,11 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.96", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", - "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", - "dev": true + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", @@ -10624,15 +10905,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -10959,6 +11231,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -12958,6 +13240,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -13056,6 +13349,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -13153,10 +13456,11 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -14125,6 +14429,20 @@ "react-dom": ">=16.6.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -14164,13 +14482,15 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -14178,15 +14498,6 @@ "node": ">=4" } }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -14220,17 +14531,18 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -14240,32 +14552,22 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -14281,11 +14583,12 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14974,6 +15277,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -15607,6 +15923,7 @@ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15616,6 +15933,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -15625,19 +15943,21 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15678,9 +15998,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -15696,6 +16016,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" diff --git a/ui/package.json b/ui/package.json index fd6aa9dd8a..eef2cfe069 100644 --- a/ui/package.json +++ b/ui/package.json @@ -66,9 +66,16 @@ "zod": "^3.25.76" }, "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.2", + "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@playwright/test": "^1.58.2", "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/vite": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/jest": "^29.5.14", "@types/loadable__component": "^5.13.10", @@ -109,7 +116,6 @@ "prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-tailwindcss": "^0.7.2", "tailwindcss": "^4.2.2", - "@tailwindcss/vite": "^4.2.2", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", "tw-animate-css": "^1.4.0", diff --git a/ui/src/components/flags/MetadataFilterPopover.test.tsx b/ui/src/components/flags/MetadataFilterPopover.test.tsx new file mode 100644 index 0000000000..ddeb2394fb --- /dev/null +++ b/ui/src/components/flags/MetadataFilterPopover.test.tsx @@ -0,0 +1,77 @@ +/** + * @jest-environment jsdom + */ +import { render, screen, fireEvent } from '@testing-library/react'; +import MetadataFilterPopover from './MetadataFilterPopover'; +import { MetadataFilter } from '~/types/Flag'; + +// Radix Popover uses portals. We mock it so the content always renders inline. +jest.mock('~/components/Popover', () => ({ + Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
+})); + +describe('MetadataFilterPopover', () => { + it('renders a "Filter" trigger button', () => { + render(); + expect(screen.getByRole('button', { name: /^filter$/i })).toBeInTheDocument(); + }); + + it('calls onAdd with the entered key and value when Add is clicked', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).toHaveBeenCalledWith<[MetadataFilter]>({ + key: 'team', + value: 'backend' + }); + }); + + it('does not call onAdd when key is empty', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).not.toHaveBeenCalled(); + }); + + it('does not call onAdd when value is empty', () => { + const onAdd = jest.fn(); + render(); + + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(onAdd).not.toHaveBeenCalled(); + }); + + it('resets key and value inputs after a successful add', () => { + const onAdd = jest.fn(); + render(); + + const keyInput = screen.getByPlaceholderText(/key/i); + const valueInput = screen.getByPlaceholderText(/value/i); + + fireEvent.change(keyInput, { target: { value: 'team' } }); + fireEvent.change(valueInput, { target: { value: 'backend' } }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect((keyInput as HTMLInputElement).value).toBe(''); + expect((valueInput as HTMLInputElement).value).toBe(''); + }); +}); diff --git a/ui/src/components/flags/MetadataFilterPopover.tsx b/ui/src/components/flags/MetadataFilterPopover.tsx new file mode 100644 index 0000000000..5a36cfd653 --- /dev/null +++ b/ui/src/components/flags/MetadataFilterPopover.tsx @@ -0,0 +1,112 @@ +import { SlidersHorizontalIcon } from 'lucide-react'; +import { useState } from 'react'; + +import { BaseInput } from '~/components/BaseInput'; +import { Button } from '~/components/Button'; +import { + Popover, + PopoverContent, + PopoverTrigger +} from '~/components/Popover'; + +import { MetadataFilter } from '~/types/Flag'; + +interface MetadataFilterPopoverProps { + availableKeys: string[]; + onAdd: (filter: MetadataFilter) => void; +} + +export default function MetadataFilterPopover({ + availableKeys, + onAdd +}: MetadataFilterPopoverProps) { + const [open, setOpen] = useState(false); + const [key, setKey] = useState(''); + const [value, setValue] = useState(''); + + const canAdd = key.trim().length > 0 && value.trim().length > 0; + + const handleAdd = () => { + if (!canAdd) return; + onAdd({ key: key.trim(), value: value.trim() }); + setKey(''); + setValue(''); + setOpen(false); + }; + + return ( + + + + + + +

Filter by metadata

+ + {/* Key input with datalist for autocomplete */} +
+ + setKey(e.target.value)} + list="mf-available-keys" + className="w-full" + /> + + {availableKeys.map((k) => ( + +
+ + {/* Value input */} +
+ + setValue(e.target.value)} + className="w-full" + onKeyDown={(e) => { + if (e.key === 'Enter') handleAdd(); + }} + /> +
+ + +
+
+ ); +} diff --git a/ui/src/setupTests.ts b/ui/src/setupTests.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/ui/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; From 6dabac7feea927794cad8cceb33b180e61a8c3ec Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:20:10 +0700 Subject: [PATCH 09/11] feat: wire metadata filter state and UI into FlagTable - Add metadataFilters state, availableMetadataKeys + filteredFlags memos - Pre-filter flags via applyMetadataFilters before passing to TanStack Table - Add MetadataFilterPopover in toolbar - Render active filter chips with per-chip remove and Clear all - Update empty-state condition to handle metadata-only filters Signed-off-by: hiepau1231 --- ui/src/components/flags/FlagTable.tsx | 69 +++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/ui/src/components/flags/FlagTable.tsx b/ui/src/components/flags/FlagTable.tsx index 20ab61c7e4..8f2196f131 100644 --- a/ui/src/components/flags/FlagTable.tsx +++ b/ui/src/components/flags/FlagTable.tsx @@ -15,6 +15,7 @@ import { PowerIcon, ToggleLeftIcon, VariableIcon, + XIcon, XSquareIcon } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; @@ -39,12 +40,15 @@ import Well from '~/components/Well'; import { IBatchFlagEvaluationCount } from '~/types/Analytics'; import { IEnvironment } from '~/types/Environment'; -import { FlagType, IFlag, flagTypeToLabel } from '~/types/Flag'; +import { FlagType, IFlag, MetadataFilter, flagTypeToLabel } from '~/types/Flag'; import { INamespace } from '~/types/Namespace'; import { useError } from '~/data/hooks/error'; +import { applyMetadataFilters } from '~/utils/flagMetadataFilter'; import { cls } from '~/utils/helpers'; +import MetadataFilterPopover from '~/components/flags/MetadataFilterPopover'; + function VariantFlagBadge({ enabled }: { enabled: boolean }) { return (
flags.map((f) => f.key), [flags]); const hasFlags = flags.length > 0; + const [metadataFilters, setMetadataFilters] = useState([]); + + const availableMetadataKeys = useMemo( + () => + [...new Set(flags.flatMap((f) => Object.keys(f.metadata ?? {})))].sort(), + [flags] + ); + + const filteredFlags = useMemo( + () => applyMetadataFilters(flags, metadataFilters), + [flags, metadataFilters] + ); + const { setError } = useError(); useEffect(() => { if (error) { @@ -274,7 +291,7 @@ export default function FlagTable(props: FlagTableProps) { }, [error, setError]); const table = useReactTable({ - data: flags, + data: filteredFlags, columns, state: { globalFilter: filter, @@ -319,15 +336,57 @@ export default function FlagTable(props: FlagTableProps) {
+ + setMetadataFilters((prev) => [...prev, f]) + } + />
{hasFlags && }
- {table.getRowCount() === 0 && filter.length === 0 && ( - + {metadataFilters.length > 0 && ( +
+ {metadataFilters.map((f, i) => ( + + + {f.key}: {f.value} + + + + ))} + +
)} - {table.getRowCount() === 0 && filter.length > 0 && ( + + {table.getRowCount() === 0 && + filter.length === 0 && + metadataFilters.length === 0 && ( + + )} + {table.getRowCount() === 0 && + (filter.length > 0 || metadataFilters.length > 0) && (
From 505743f3f5c029b2b96e5b8af016d598542bf7f3 Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:22:52 +0700 Subject: [PATCH 10/11] test: add FlagTable integration tests for metadata filter - 9 tests covering toolbar, chips, AND logic, empty states, combined text+metadata - Add TextEncoder/TextDecoder polyfill for react-router compatibility in jsdom Signed-off-by: hiepau1231 --- ui/src/components/flags/FlagTable.test.tsx | 224 +++++++++++++++++++++ ui/src/setupTests.ts | 4 + 2 files changed, 228 insertions(+) create mode 100644 ui/src/components/flags/FlagTable.test.tsx diff --git a/ui/src/components/flags/FlagTable.test.tsx b/ui/src/components/flags/FlagTable.test.tsx new file mode 100644 index 0000000000..3ae727fab6 --- /dev/null +++ b/ui/src/components/flags/FlagTable.test.tsx @@ -0,0 +1,224 @@ +/** + * @jest-environment jsdom + */ +import { configureStore } from '@reduxjs/toolkit'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router'; +import FlagTable from './FlagTable'; + +// ── Mock Radix Popover (portal-based, doesn't render in jsdom) ──────────── +jest.mock('~/components/Popover', () => ({ + Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, + PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
+})); + +// ── Mock RTK Query hooks (actual import paths from FlagTable.tsx) ───────── +const mockFlags = [ + { + key: 'alpha', + name: 'Alpha', + type: 'VARIANT_FLAG_TYPE', + enabled: true, + description: '', + metadata: { team: 'backend', env: 'production' } + }, + { + key: 'beta', + name: 'Beta', + type: 'VARIANT_FLAG_TYPE', + enabled: true, + description: '', + metadata: { team: 'frontend', env: 'production' } + }, + { + key: 'gamma', + name: 'Gamma', + type: 'VARIANT_FLAG_TYPE', + enabled: true, + description: '', + metadata: { team: 'backend', env: 'staging' } + } +]; + +jest.mock('~/app/flags/flagsApi', () => ({ + useListFlagsQuery: () => ({ + data: { flags: mockFlags }, + isLoading: false, + error: null + }), + selectSorting: () => [], + setSorting: (s: any) => ({ type: 'SET_SORTING', payload: s }) +})); + +jest.mock('~/app/flags/analyticsApi', () => ({ + useGetBatchFlagEvaluationCountQuery: () => ({ data: null }) +})); + +jest.mock('~/app/meta/metaSlice', () => ({ + selectInfo: () => ({ analytics: { enabled: false } }) +})); + +// ── Helpers ─────────────────────────────────────────────────────────────── +const mockEnvironment = { key: 'default', name: 'default' }; +const mockNamespace = { key: 'default', name: 'default', description: '' }; + +function renderTable() { + const store = configureStore({ + reducer: { + flags: (s = {}) => s, + flagsTable: (s = { sorting: [] }) => s, + meta: (s = { info: { analytics: { enabled: false } } }) => s + } + }); + return render( + + + + + + ); +} + +// ── Fake timers for Searchbox debounce (defaults to 500ms) ──────────────── +beforeEach(() => jest.useFakeTimers()); +afterEach(() => jest.useRealTimers()); + +describe('FlagTable — metadata filter', () => { + it('renders all flags with no filter active', () => { + renderTable(); + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); + expect(screen.getByText('Gamma')).toBeInTheDocument(); + }); + + it('renders Filter button in toolbar', () => { + renderTable(); + expect(screen.getByRole('button', { name: /^filter$/i })).toBeInTheDocument(); + }); + + it('adds a chip after applying a metadata filter', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + expect(screen.getByText(/team: backend/i)).toBeInTheDocument(); + }); + + it('hides non-matching flags after filter is applied', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); + }); + + it('removes a chip when × is clicked and restores all flags', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + fireEvent.click( + screen.getByRole('button', { name: /remove filter team:backend/i }) + ); + + expect(screen.getByText('Beta')).toBeInTheDocument(); + expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); + }); + + it('applies AND logic for two metadata filters', () => { + renderTable(); + + // Filter 1: team=backend + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Filter 2: env=production + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'env' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'production' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Only alpha matches both filters + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); + expect(screen.queryByText('Gamma')).not.toBeInTheDocument(); + }); + + it('shows "no flags matched" empty state when filter matches nothing', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'nonexistent' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'x' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + expect(screen.getByText(/no flags matched/i)).toBeInTheDocument(); + }); + + it('clears all filters when Clear all is clicked', () => { + renderTable(); + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + fireEvent.click(screen.getByText('Clear all')); + + expect(screen.getByText('Beta')).toBeInTheDocument(); + expect(screen.queryByText(/team: backend/i)).not.toBeInTheDocument(); + }); + + it('applies both text search and metadata filter simultaneously (AND)', () => { + renderTable(); + + // Text search: "alpha" — uniquely matches only the Alpha row + fireEvent.change(screen.getByRole('searchbox'), { + target: { value: 'alpha' } + }); + act(() => { + jest.advanceTimersByTime(600); + }); // flush 500ms Searchbox debounce + + // Metadata filter: team=backend — alone would return [Alpha, Gamma] + fireEvent.change(screen.getByPlaceholderText(/key/i), { + target: { value: 'team' } + }); + fireEvent.change(screen.getByPlaceholderText(/value/i), { + target: { value: 'backend' } + }); + fireEvent.click(screen.getByRole('button', { name: /add filter/i })); + + // Combined AND: text("alpha") ∩ metadata(team=backend) = [Alpha] only + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.queryByText('Beta')).not.toBeInTheDocument(); + expect(screen.queryByText('Gamma')).not.toBeInTheDocument(); + }); +}); diff --git a/ui/src/setupTests.ts b/ui/src/setupTests.ts index 7b0828bfa8..6671049fd4 100644 --- a/ui/src/setupTests.ts +++ b/ui/src/setupTests.ts @@ -1 +1,5 @@ import '@testing-library/jest-dom'; + +// Polyfill TextEncoder/TextDecoder for jest-environment-jsdom + react-router +import { TextEncoder, TextDecoder } from 'util'; +Object.assign(global, { TextEncoder, TextDecoder }); From d9ef0971138c1550138dbc1e34aa096e6b26382b Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sat, 4 Apr 2026 14:27:09 +0700 Subject: [PATCH 11/11] chore: fix lint warnings after metadata filter implementation Signed-off-by: hiepau1231 --- ui/src/components/flags/FlagTable.test.tsx | 24 +++++++++++---- ui/src/components/flags/FlagTable.tsx | 29 +++++++++---------- .../flags/MetadataFilterPopover.test.tsx | 22 ++++++++++---- .../flags/MetadataFilterPopover.tsx | 6 +--- ui/src/setupTests.ts | 5 ++-- ui/src/utils/flagMetadataFilter.test.ts | 3 +- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/ui/src/components/flags/FlagTable.test.tsx b/ui/src/components/flags/FlagTable.test.tsx index 3ae727fab6..ebf1f2e13b 100644 --- a/ui/src/components/flags/FlagTable.test.tsx +++ b/ui/src/components/flags/FlagTable.test.tsx @@ -2,16 +2,23 @@ * @jest-environment jsdom */ import { configureStore } from '@reduxjs/toolkit'; -import { render, screen, fireEvent, act } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router'; + import FlagTable from './FlagTable'; // ── Mock Radix Popover (portal-based, doesn't render in jsdom) ──────────── jest.mock('~/components/Popover', () => ({ - Popover: ({ children }: { children: React.ReactNode }) =>
{children}
, - PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, - PopoverContent: ({ children }: { children: React.ReactNode }) =>
{children}
+ Popover: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + PopoverTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + PopoverContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ) })); // ── Mock RTK Query hooks (actual import paths from FlagTable.tsx) ───────── @@ -75,7 +82,10 @@ function renderTable() { return render( - + ); @@ -95,7 +105,9 @@ describe('FlagTable — metadata filter', () => { it('renders Filter button in toolbar', () => { renderTable(); - expect(screen.getByRole('button', { name: /^filter$/i })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /^filter$/i }) + ).toBeInTheDocument(); }); it('adds a chip after applying a metadata filter', () => { diff --git a/ui/src/components/flags/FlagTable.tsx b/ui/src/components/flags/FlagTable.tsx index 8f2196f131..36320d3053 100644 --- a/ui/src/components/flags/FlagTable.tsx +++ b/ui/src/components/flags/FlagTable.tsx @@ -37,6 +37,7 @@ import { DataTablePagination } from '~/components/TablePagination'; import { TableSkeleton } from '~/components/TableSkeleton'; import { DataTableViewOptions } from '~/components/TableViewOptions'; import Well from '~/components/Well'; +import MetadataFilterPopover from '~/components/flags/MetadataFilterPopover'; import { IBatchFlagEvaluationCount } from '~/types/Analytics'; import { IEnvironment } from '~/types/Environment'; @@ -47,8 +48,6 @@ import { useError } from '~/data/hooks/error'; import { applyMetadataFilters } from '~/utils/flagMetadataFilter'; import { cls } from '~/utils/helpers'; -import MetadataFilterPopover from '~/components/flags/MetadataFilterPopover'; - function VariantFlagBadge({ enabled }: { enabled: boolean }) { return (
- setMetadataFilters((prev) => [...prev, f]) - } + onAdd={(f) => setMetadataFilters((prev) => [...prev, f])} />
{hasFlags && } @@ -361,7 +358,9 @@ export default function FlagTable(props: FlagTableProps) {