@open-mercato/ui 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/backend/DataTable.js +149 -53
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/filters/ActiveFilterChips.js +142 -0
- package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
- package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
- package/dist/backend/filters/FilterEmptyState.js +41 -0
- package/dist/backend/filters/FilterEmptyState.js.map +7 -0
- package/dist/backend/filters/FilterFieldPicker.js +148 -0
- package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
- package/dist/backend/filters/FilteredEmptyResults.js +26 -0
- package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
- package/dist/backend/filters/QuickFilters.js +41 -0
- package/dist/backend/filters/QuickFilters.js.map +7 -0
- package/dist/backend/filters/filterValidation.js +47 -0
- package/dist/backend/filters/filterValidation.js.map +7 -0
- package/dist/backend/filters/treeReducer.js +159 -0
- package/dist/backend/filters/treeReducer.js.map +7 -0
- package/dist/backend/hooks/useAdvancedFilter.js +82 -1
- package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
- package/dist/backend/utils/api.js +18 -1
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/backend/utils/useCurrentUserId.js +31 -0
- package/dist/backend/utils/useCurrentUserId.js.map +7 -0
- package/dist/primitives/dialog.js +7 -5
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/tag.js +4 -2
- package/dist/primitives/tag.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +307 -63
- package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
- package/src/backend/filters/ActiveFilterChips.tsx +179 -0
- package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
- package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
- package/src/backend/filters/FilterEmptyState.tsx +51 -0
- package/src/backend/filters/FilterFieldPicker.tsx +139 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
- package/src/backend/filters/QuickFilters.tsx +62 -0
- package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
- package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
- package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
- package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
- package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
- package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
- package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
- package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
- package/src/backend/filters/filterValidation.ts +61 -0
- package/src/backend/filters/treeReducer.ts +201 -0
- package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
- package/src/backend/hooks/useAdvancedFilter.ts +130 -0
- package/src/backend/utils/api.ts +27 -2
- package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
- package/src/backend/utils/useCurrentUserId.ts +40 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +22 -0
- package/src/primitives/dialog.tsx +18 -7
- package/src/primitives/tag.tsx +3 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { createEmptyCondition, getDefaultOperator, normalizeAdvancedFilterState } from "@open-mercato/shared/lib/query/advanced-filter";
|
|
4
|
+
import { treeReducer } from "../filters/treeReducer.js";
|
|
5
|
+
import { validateTreeForApply } from "../filters/filterValidation.js";
|
|
4
6
|
function useAdvancedFilter({ fields, onChange }) {
|
|
5
7
|
const [state, setState] = React.useState({
|
|
6
8
|
logic: "and",
|
|
@@ -58,7 +60,86 @@ function useAdvancedFilter({ fields, onChange }) {
|
|
|
58
60
|
hasActiveConditions
|
|
59
61
|
};
|
|
60
62
|
}
|
|
63
|
+
function useAdvancedFilterTree({
|
|
64
|
+
initial,
|
|
65
|
+
fields,
|
|
66
|
+
onApply,
|
|
67
|
+
debounceMs = 400
|
|
68
|
+
}) {
|
|
69
|
+
const [tree, setTree] = React.useState(initial);
|
|
70
|
+
const [appliedTree, setAppliedTree] = React.useState(initial);
|
|
71
|
+
const [pendingErrors, setPendingErrors] = React.useState([]);
|
|
72
|
+
const timerRef = React.useRef(null);
|
|
73
|
+
const didMountRef = React.useRef(false);
|
|
74
|
+
const onApplyRef = React.useRef(onApply);
|
|
75
|
+
const fieldsRef = React.useRef(fields);
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
onApplyRef.current = onApply;
|
|
78
|
+
}, [onApply]);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
fieldsRef.current = fields;
|
|
81
|
+
}, [fields]);
|
|
82
|
+
const tryApply = React.useCallback((candidate) => {
|
|
83
|
+
const result = validateTreeForApply(candidate, fieldsRef.current);
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
setPendingErrors([]);
|
|
86
|
+
setAppliedTree(candidate);
|
|
87
|
+
onApplyRef.current?.(candidate);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
setPendingErrors(result.errors);
|
|
91
|
+
return false;
|
|
92
|
+
}, []);
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
if (!didMountRef.current) {
|
|
95
|
+
didMountRef.current = true;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
tryApply(tree);
|
|
101
|
+
}, debounceMs);
|
|
102
|
+
timerRef.current = timer;
|
|
103
|
+
return () => clearTimeout(timer);
|
|
104
|
+
}, [tree, debounceMs, tryApply]);
|
|
105
|
+
const dispatch = React.useCallback((action) => {
|
|
106
|
+
setTree((prev) => treeReducer(prev, action));
|
|
107
|
+
}, []);
|
|
108
|
+
const flush = React.useCallback(() => {
|
|
109
|
+
if (timerRef.current) {
|
|
110
|
+
clearTimeout(timerRef.current);
|
|
111
|
+
timerRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
tryApply(tree);
|
|
114
|
+
}, [tree, tryApply]);
|
|
115
|
+
const clear = React.useCallback(() => {
|
|
116
|
+
if (timerRef.current) {
|
|
117
|
+
clearTimeout(timerRef.current);
|
|
118
|
+
timerRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
setTree((prev) => {
|
|
121
|
+
const emptied = { root: { ...prev.root, children: [] } };
|
|
122
|
+
setAppliedTree(emptied);
|
|
123
|
+
return emptied;
|
|
124
|
+
});
|
|
125
|
+
setPendingErrors([]);
|
|
126
|
+
onApplyRef.current?.({ root: { id: "", type: "group", combinator: "and", children: [] } });
|
|
127
|
+
}, []);
|
|
128
|
+
const replaceTree = React.useCallback((next) => {
|
|
129
|
+
if (timerRef.current) {
|
|
130
|
+
clearTimeout(timerRef.current);
|
|
131
|
+
timerRef.current = null;
|
|
132
|
+
}
|
|
133
|
+
setTree(next);
|
|
134
|
+
setAppliedTree(next);
|
|
135
|
+
setPendingErrors([]);
|
|
136
|
+
onApplyRef.current?.(next);
|
|
137
|
+
}, []);
|
|
138
|
+
const hasActiveRules = tree.root.children.length > 0;
|
|
139
|
+
return { tree, appliedTree, setTree, dispatch, pendingErrors, flush, clear, replaceTree, hasActiveRules };
|
|
140
|
+
}
|
|
61
141
|
export {
|
|
62
|
-
useAdvancedFilter
|
|
142
|
+
useAdvancedFilter,
|
|
143
|
+
useAdvancedFilterTree
|
|
63
144
|
};
|
|
64
145
|
//# sourceMappingURL=useAdvancedFilter.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/hooks/useAdvancedFilter.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { AdvancedFilterState, FilterCondition, FilterFieldDef } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { createEmptyCondition, getDefaultOperator, normalizeAdvancedFilterState } from '@open-mercato/shared/lib/query/advanced-filter'\n\nexport type UseAdvancedFilterOptions = {\n fields: FilterFieldDef[]\n onChange?: (state: AdvancedFilterState) => void\n}\n\nexport function useAdvancedFilter({ fields, onChange }: UseAdvancedFilterOptions) {\n const [state, setState] = React.useState<AdvancedFilterState>({\n logic: 'and',\n conditions: [],\n })\n\n const updateState = React.useCallback((next: AdvancedFilterState) => {\n const normalized = normalizeAdvancedFilterState(next)\n setState(normalized)\n onChange?.(normalized)\n }, [onChange])\n\n const addCondition = React.useCallback(() => {\n const newCondition = createEmptyCondition()\n if (fields.length > 0) {\n newCondition.field = fields[0].key\n newCondition.operator = getDefaultOperator(fields[0].type)\n }\n updateState({\n ...state,\n conditions: [...state.conditions, newCondition],\n })\n }, [fields, state, updateState])\n\n const removeCondition = React.useCallback((conditionId: string) => {\n updateState({\n ...state,\n conditions: state.conditions.filter((c) => c.id !== conditionId),\n })\n }, [state, updateState])\n\n const updateCondition = React.useCallback((conditionId: string, updates: Partial<FilterCondition>) => {\n updateState({\n ...state,\n conditions: state.conditions.map((c) =>\n c.id === conditionId ? { ...c, ...updates } : c,\n ),\n })\n }, [state, updateState])\n\n const toggleLogic = React.useCallback(() => {\n const nextLogic = state.logic === 'and' ? 'or' : 'and'\n updateState({\n logic: nextLogic,\n conditions: state.conditions.map((condition, index) => (\n index === 0 ? condition : { ...condition, join: nextLogic }\n )),\n })\n }, [state, updateState])\n\n const clearAll = React.useCallback(() => {\n updateState({ logic: 'and', conditions: [] })\n }, [updateState])\n\n const hasActiveConditions = state.conditions.some((c) => c.field && c.operator)\n\n return {\n state,\n setState: updateState,\n addCondition,\n removeCondition,\n updateCondition,\n toggleLogic,\n clearAll,\n hasActiveConditions,\n }\n}\n"],
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AAEvB,SAAS,sBAAsB,oBAAoB,oCAAoC;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { AdvancedFilterState, FilterCondition, FilterFieldDef } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { createEmptyCondition, getDefaultOperator, normalizeAdvancedFilterState } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { AdvancedFilterTree } from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport { treeReducer, type TreeAction } from '../filters/treeReducer'\nimport { validateTreeForApply, type ValidationError } from '../filters/filterValidation'\n\nexport type UseAdvancedFilterOptions = {\n fields: FilterFieldDef[]\n onChange?: (state: AdvancedFilterState) => void\n}\n\nexport function useAdvancedFilter({ fields, onChange }: UseAdvancedFilterOptions) {\n const [state, setState] = React.useState<AdvancedFilterState>({\n logic: 'and',\n conditions: [],\n })\n\n const updateState = React.useCallback((next: AdvancedFilterState) => {\n const normalized = normalizeAdvancedFilterState(next)\n setState(normalized)\n onChange?.(normalized)\n }, [onChange])\n\n const addCondition = React.useCallback(() => {\n const newCondition = createEmptyCondition()\n if (fields.length > 0) {\n newCondition.field = fields[0].key\n newCondition.operator = getDefaultOperator(fields[0].type)\n }\n updateState({\n ...state,\n conditions: [...state.conditions, newCondition],\n })\n }, [fields, state, updateState])\n\n const removeCondition = React.useCallback((conditionId: string) => {\n updateState({\n ...state,\n conditions: state.conditions.filter((c) => c.id !== conditionId),\n })\n }, [state, updateState])\n\n const updateCondition = React.useCallback((conditionId: string, updates: Partial<FilterCondition>) => {\n updateState({\n ...state,\n conditions: state.conditions.map((c) =>\n c.id === conditionId ? { ...c, ...updates } : c,\n ),\n })\n }, [state, updateState])\n\n const toggleLogic = React.useCallback(() => {\n const nextLogic = state.logic === 'and' ? 'or' : 'and'\n updateState({\n logic: nextLogic,\n conditions: state.conditions.map((condition, index) => (\n index === 0 ? condition : { ...condition, join: nextLogic }\n )),\n })\n }, [state, updateState])\n\n const clearAll = React.useCallback(() => {\n updateState({ logic: 'and', conditions: [] })\n }, [updateState])\n\n const hasActiveConditions = state.conditions.some((c) => c.field && c.operator)\n\n return {\n state,\n setState: updateState,\n addCondition,\n removeCondition,\n updateCondition,\n toggleLogic,\n clearAll,\n hasActiveConditions,\n }\n}\n\nexport type UseAdvancedFilterTreeArgs = {\n initial: AdvancedFilterTree\n fields: FilterFieldDef[]\n /**\n * Optional side-effect that fires whenever the tree is validated and applied\n * (debounced auto-apply, explicit `flush()`, `clear()`, or `replaceTree()`).\n * Use this for non-state effects such as resetting the page index when the\n * filter changes. State propagation should read `appliedTree` directly \u2014 the\n * hook is the single source of truth and the page MUST NOT keep a parallel\n * copy.\n */\n onApply?: (tree: AdvancedFilterTree) => void\n debounceMs?: number\n}\n\nexport type UseAdvancedFilterTreeResult = {\n /** In-progress editor tree. Bound to the filter panel's `value` / `onChange`. */\n tree: AdvancedFilterTree\n /**\n * Last validated, applied tree. Bound to URL serialization, query requests,\n * active chips, and the filter-aware empty-state. Lags `tree` by `debounceMs`\n * while the user is typing in an editor; advances immediately on `flush()`,\n * `clear()`, and `replaceTree()`.\n */\n appliedTree: AdvancedFilterTree\n setTree: (t: AdvancedFilterTree) => void\n dispatch: (a: TreeAction) => void\n pendingErrors: ValidationError[]\n flush: () => void\n clear: () => void\n /**\n * Replace BOTH the editor and applied trees immediately, bypassing the\n * debounce. Use this for perspective restoration and saved-filter apply \u2014\n * actions that semantically jump the filter to a new known-good state rather\n * than building it up via user edits.\n */\n replaceTree: (next: AdvancedFilterTree) => void\n hasActiveRules: boolean\n}\n\n/**\n * Tree-shaped advanced filter state with debounced auto-apply, validation gating,\n * and helpers for the CRM filter builder UI.\n *\n * **State ownership**: this hook is the single source of truth for the filter\n * tree. The host page MUST NOT keep a parallel `useState<AdvancedFilterTree>` \u2014\n * read `appliedTree` for URL/query consumption and `tree` for the editor panel.\n *\n * - Debounced apply (default 400 ms) \u2014 fires only when the tree validates.\n * `appliedTree` advances and the optional `onApply(tree)` callback fires.\n * - `pendingErrors` exposes the latest validation errors for inline UI rendering.\n * - `flush()` applies immediately if valid; `clear()` empties root children.\n * - `replaceTree(t)` jumps BOTH editor and applied trees (perspective restore).\n * - `hasActiveRules` mirrors `tree.root.children.length > 0`.\n */\nexport function useAdvancedFilterTree({\n initial,\n fields,\n onApply,\n debounceMs = 400,\n}: UseAdvancedFilterTreeArgs): UseAdvancedFilterTreeResult {\n const [tree, setTree] = React.useState<AdvancedFilterTree>(initial)\n const [appliedTree, setAppliedTree] = React.useState<AdvancedFilterTree>(initial)\n const [pendingErrors, setPendingErrors] = React.useState<ValidationError[]>([])\n const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n const didMountRef = React.useRef(false)\n const onApplyRef = React.useRef(onApply)\n const fieldsRef = React.useRef(fields)\n React.useEffect(() => { onApplyRef.current = onApply }, [onApply])\n React.useEffect(() => { fieldsRef.current = fields }, [fields])\n\n const tryApply = React.useCallback((candidate: AdvancedFilterTree): boolean => {\n const result = validateTreeForApply(candidate, fieldsRef.current)\n if (result.ok) {\n setPendingErrors([])\n setAppliedTree(candidate)\n onApplyRef.current?.(candidate)\n return true\n }\n setPendingErrors(result.errors)\n return false\n }, [])\n\n React.useEffect(() => {\n if (!didMountRef.current) {\n didMountRef.current = true\n return\n }\n if (timerRef.current) clearTimeout(timerRef.current)\n const timer = setTimeout(() => { tryApply(tree) }, debounceMs)\n timerRef.current = timer\n return () => clearTimeout(timer)\n }, [tree, debounceMs, tryApply])\n\n const dispatch = React.useCallback((action: TreeAction) => {\n setTree((prev) => treeReducer(prev, action))\n }, [])\n\n const flush = React.useCallback(() => {\n if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }\n tryApply(tree)\n }, [tree, tryApply])\n\n const clear = React.useCallback(() => {\n if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }\n setTree((prev) => {\n const emptied: AdvancedFilterTree = { root: { ...prev.root, children: [] } }\n setAppliedTree(emptied)\n return emptied\n })\n setPendingErrors([])\n onApplyRef.current?.({ root: { id: '', type: 'group', combinator: 'and', children: [] } })\n }, [])\n\n const replaceTree = React.useCallback((next: AdvancedFilterTree) => {\n if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }\n setTree(next)\n setAppliedTree(next)\n setPendingErrors([])\n onApplyRef.current?.(next)\n }, [])\n\n const hasActiveRules = tree.root.children.length > 0\n\n return { tree, appliedTree, setTree, dispatch, pendingErrors, flush, clear, replaceTree, hasActiveRules }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,6 +17,22 @@ function mergeHeaders(base, scoped) {
|
|
|
17
17
|
function readRedirectOverride(headers, headerName) {
|
|
18
18
|
return headers.get(headerName) === "0";
|
|
19
19
|
}
|
|
20
|
+
function isSameOriginRequest(input) {
|
|
21
|
+
if (typeof window === "undefined") return false;
|
|
22
|
+
const host = window.location?.host;
|
|
23
|
+
if (!host) return false;
|
|
24
|
+
let urlString;
|
|
25
|
+
if (typeof input === "string") urlString = input;
|
|
26
|
+
else if (input instanceof URL) urlString = input.toString();
|
|
27
|
+
else if (typeof Request !== "undefined" && input instanceof Request) urlString = input.url;
|
|
28
|
+
else return false;
|
|
29
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true;
|
|
30
|
+
try {
|
|
31
|
+
return new URL(urlString).host === host;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
async function withScopedApiHeaders(headers, run) {
|
|
21
37
|
return scopedHeaders.withScopedHeaders(headers, run);
|
|
22
38
|
}
|
|
@@ -83,7 +99,8 @@ async function apiFetch(input, init) {
|
|
|
83
99
|
);
|
|
84
100
|
}
|
|
85
101
|
const scoped = scopedHeaders.resolveScopedHeaders();
|
|
86
|
-
const
|
|
102
|
+
const baseInit = Object.keys(scoped).length ? { ...init ?? {}, headers: mergeHeaders(init?.headers, scoped) } : init ?? {};
|
|
103
|
+
const mergedInit = baseInit.credentials ? baseInit : isSameOriginRequest(input) ? { ...baseInit, credentials: "include" } : baseInit;
|
|
87
104
|
const requestHeaders = new Headers(mergedInit?.headers);
|
|
88
105
|
const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, "x-om-unauthorized-redirect");
|
|
89
106
|
const disableForbiddenRedirect = readRedirectOverride(requestHeaders, "x-om-forbidden-redirect");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/api.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n if (window.location.pathname.startsWith('/login')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n const effectiveRoles = roles.length ? roles : fallbackRoles\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : effectiveRoles.length\n ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(','))}`\n : ''\n const url = query\n ? `/login?${query}&redirect=${encodeURIComponent(current)}`\n : `/login?redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const mergedInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n if (window.location.pathname.startsWith('/login')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n const effectiveRoles = roles.length ? roles : fallbackRoles\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : effectiveRoles.length\n ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(','))}`\n : ''\n const url = query\n ? `/login?${query}&redirect=${encodeURIComponent(current)}`\n : `/login?redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -45,6 +45,7 @@ function useAutoDiscoveredFields({
|
|
|
45
45
|
type,
|
|
46
46
|
group: meta?.filterGroup ?? meta?.columnChooserGroup
|
|
47
47
|
};
|
|
48
|
+
if (meta?.filterIconName) field.iconName = meta.filterIconName;
|
|
48
49
|
if (meta?.filterOptions) field.options = meta.filterOptions;
|
|
49
50
|
if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions;
|
|
50
51
|
filterFields.push(field);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/useAutoDiscoveredFields.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterFieldDef as AdvancedFilterFieldDef, FilterFieldType, FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ColumnChooserField } from '../columns/ColumnChooserPanel'\nimport type { CustomFieldDefDto } from './customFieldDefs'\nimport {\n mapCustomFieldKindToFilterType,\n normalizeCustomFieldFilterOptions,\n supportsCustomFieldColumn,\n} from './customFieldColumns'\n\ntype ColumnMeta = {\n filterKey?: string\n filterType?: FilterFieldType\n filterOptions?: FilterOption[]\n filterLoadOptions?: (query?: string) => Promise<FilterOption[]>\n filterable?: boolean\n filterGroup?: string\n columnChooserGroup?: string\n alwaysVisible?: boolean\n hidden?: boolean\n truncate?: boolean\n maxWidth?: string\n priority?: number\n tooltipContent?: (row: unknown) => string | undefined\n}\n\nfunction resolveHeaderLabel(column: ColumnDef<any, any>): string {\n const header = (column as any).header\n if (typeof header === 'string') return header\n const accessorKey = (column as any).accessorKey as string | undefined\n if (!accessorKey) return ''\n return accessorKey\n .replace(/^cf_/, '')\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n}\n\nfunction inferFilterType(accessorKey: string, meta?: ColumnMeta): FilterFieldType {\n if (meta?.filterType) return meta.filterType\n if (accessorKey.endsWith('_at') || accessorKey === 'createdAt' || accessorKey === 'updatedAt' || accessorKey === 'expectedCloseAt' || accessorKey.endsWith('At')) return 'date'\n if (accessorKey === 'probability' || accessorKey === 'valueAmount' || accessorKey === 'value_amount') return 'number'\n if (meta?.filterOptions) return 'select'\n return 'text'\n}\n\nexport type UseAutoDiscoveredFieldsInput = {\n columns: ColumnDef<any, any>[]\n customFieldDefs: CustomFieldDefDto[]\n}\n\nexport type UseAutoDiscoveredFieldsResult = {\n advancedFilterFields: AdvancedFilterFieldDef[]\n columnChooserFields: ColumnChooserField[]\n}\n\nexport function useAutoDiscoveredFields({\n columns,\n customFieldDefs,\n}: UseAutoDiscoveredFieldsInput): UseAutoDiscoveredFieldsResult {\n return React.useMemo(() => {\n const filterFields: AdvancedFilterFieldDef[] = []\n const chooserFields: ColumnChooserField[] = []\n const seenFilterKeys = new Set<string>()\n const seenChooserKeys = new Set<string>()\n\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n const accessorKey = (col as any).accessorKey as string | undefined\n if (!accessorKey) continue\n\n const meta = (col as any).meta as ColumnMeta | undefined\n const label = resolveHeaderLabel(col)\n if (!label) continue\n const filterKey = meta?.filterKey ?? accessorKey\n\n // Advanced filter field\n if (meta?.filterable !== false && filterKey && !seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = inferFilterType(filterKey, meta)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label,\n type,\n group: meta?.filterGroup ?? meta?.columnChooserGroup,\n }\n if (meta?.filterOptions) field.options = meta.filterOptions\n if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions\n filterFields.push(field)\n }\n\n // Column chooser field\n if (!seenChooserKeys.has(accessorKey)) {\n seenChooserKeys.add(accessorKey)\n chooserFields.push({\n key: accessorKey,\n label,\n group: meta?.columnChooserGroup ?? 'Columns',\n alwaysVisible: meta?.alwaysVisible ?? i === 0,\n defaultVisible: true,\n })\n }\n }\n\n // Append ALL custom field definitions (not just filterable/listVisible)\n // so that every entity field appears in both the column chooser and advanced filter\n for (const def of customFieldDefs) {\n if (!supportsCustomFieldColumn(def)) continue\n const filterKey = `cf_${def.key}`\n if (!seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = mapCustomFieldKindToFilterType(def.kind)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label: def.label || def.key,\n type,\n group: 'Custom Fields',\n }\n if (type === 'select' && Array.isArray(def.options) && def.options.length) {\n field.options = normalizeCustomFieldFilterOptions(def.options)\n }\n filterFields.push(field)\n }\n if (!seenChooserKeys.has(filterKey)) {\n seenChooserKeys.add(filterKey)\n chooserFields.push({\n key: filterKey,\n label: def.label || def.key,\n group: def.group?.title ?? 'Custom Fields',\n defaultVisible: false,\n })\n }\n }\n\n return { advancedFilterFields: filterFields, columnChooserFields: chooserFields }\n }, [columns, customFieldDefs])\n}\n"],
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AAKvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterFieldDef as AdvancedFilterFieldDef, FilterFieldType, FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ColumnChooserField } from '../columns/ColumnChooserPanel'\nimport type { CustomFieldDefDto } from './customFieldDefs'\nimport {\n mapCustomFieldKindToFilterType,\n normalizeCustomFieldFilterOptions,\n supportsCustomFieldColumn,\n} from './customFieldColumns'\n\ntype ColumnMeta = {\n filterKey?: string\n filterType?: FilterFieldType\n filterOptions?: FilterOption[]\n filterLoadOptions?: (query?: string) => Promise<FilterOption[]>\n filterable?: boolean\n filterGroup?: string\n filterIconName?: string\n columnChooserGroup?: string\n alwaysVisible?: boolean\n hidden?: boolean\n truncate?: boolean\n maxWidth?: string\n priority?: number\n tooltipContent?: (row: unknown) => string | undefined\n}\n\nfunction resolveHeaderLabel(column: ColumnDef<any, any>): string {\n const header = (column as any).header\n if (typeof header === 'string') return header\n const accessorKey = (column as any).accessorKey as string | undefined\n if (!accessorKey) return ''\n return accessorKey\n .replace(/^cf_/, '')\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n}\n\nfunction inferFilterType(accessorKey: string, meta?: ColumnMeta): FilterFieldType {\n if (meta?.filterType) return meta.filterType\n if (accessorKey.endsWith('_at') || accessorKey === 'createdAt' || accessorKey === 'updatedAt' || accessorKey === 'expectedCloseAt' || accessorKey.endsWith('At')) return 'date'\n if (accessorKey === 'probability' || accessorKey === 'valueAmount' || accessorKey === 'value_amount') return 'number'\n if (meta?.filterOptions) return 'select'\n return 'text'\n}\n\nexport type UseAutoDiscoveredFieldsInput = {\n columns: ColumnDef<any, any>[]\n customFieldDefs: CustomFieldDefDto[]\n}\n\nexport type UseAutoDiscoveredFieldsResult = {\n advancedFilterFields: AdvancedFilterFieldDef[]\n columnChooserFields: ColumnChooserField[]\n}\n\nexport function useAutoDiscoveredFields({\n columns,\n customFieldDefs,\n}: UseAutoDiscoveredFieldsInput): UseAutoDiscoveredFieldsResult {\n return React.useMemo(() => {\n const filterFields: AdvancedFilterFieldDef[] = []\n const chooserFields: ColumnChooserField[] = []\n const seenFilterKeys = new Set<string>()\n const seenChooserKeys = new Set<string>()\n\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n const accessorKey = (col as any).accessorKey as string | undefined\n if (!accessorKey) continue\n\n const meta = (col as any).meta as ColumnMeta | undefined\n const label = resolveHeaderLabel(col)\n if (!label) continue\n const filterKey = meta?.filterKey ?? accessorKey\n\n // Advanced filter field\n if (meta?.filterable !== false && filterKey && !seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = inferFilterType(filterKey, meta)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label,\n type,\n group: meta?.filterGroup ?? meta?.columnChooserGroup,\n }\n if (meta?.filterIconName) field.iconName = meta.filterIconName\n if (meta?.filterOptions) field.options = meta.filterOptions\n if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions\n filterFields.push(field)\n }\n\n // Column chooser field\n if (!seenChooserKeys.has(accessorKey)) {\n seenChooserKeys.add(accessorKey)\n chooserFields.push({\n key: accessorKey,\n label,\n group: meta?.columnChooserGroup ?? 'Columns',\n alwaysVisible: meta?.alwaysVisible ?? i === 0,\n defaultVisible: true,\n })\n }\n }\n\n // Append ALL custom field definitions (not just filterable/listVisible)\n // so that every entity field appears in both the column chooser and advanced filter\n for (const def of customFieldDefs) {\n if (!supportsCustomFieldColumn(def)) continue\n const filterKey = `cf_${def.key}`\n if (!seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = mapCustomFieldKindToFilterType(def.kind)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label: def.label || def.key,\n type,\n group: 'Custom Fields',\n }\n if (type === 'select' && Array.isArray(def.options) && def.options.length) {\n field.options = normalizeCustomFieldFilterOptions(def.options)\n }\n filterFields.push(field)\n }\n if (!seenChooserKeys.has(filterKey)) {\n seenChooserKeys.add(filterKey)\n chooserFields.push({\n key: filterKey,\n label: def.label || def.key,\n group: def.group?.title ?? 'Custom Fields',\n defaultVisible: false,\n })\n }\n }\n\n return { advancedFilterFields: filterFields, columnChooserFields: chooserFields }\n }, [columns, customFieldDefs])\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { apiCall } from "./apiCall.js";
|
|
4
|
+
function useCurrentUserId() {
|
|
5
|
+
const [userId, setUserId] = React.useState("");
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
let cancelled = false;
|
|
8
|
+
void (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const res = await apiCall("/api/auth/feature-check", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "content-type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ features: [] })
|
|
14
|
+
});
|
|
15
|
+
if (!cancelled && res.ok && typeof res.result?.userId === "string") {
|
|
16
|
+
setUserId(res.result.userId);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
if (!cancelled) setUserId("");
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
return () => {
|
|
23
|
+
cancelled = true;
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
return userId;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
useCurrentUserId
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=useCurrentUserId.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/utils/useCurrentUserId.ts"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { apiCall } from './apiCall'\n\ntype FeatureCheckResponse = {\n ok: boolean\n granted: string[]\n userId: string\n}\n\n/**\n * Read the current user's id on the client. Returns `''` until the request resolves.\n *\n * Implementation note: there is no first-class `/api/auth/me` endpoint yet, so this\n * piggy-backs on `/api/auth/feature-check` with an empty `features` array \u2014 the response\n * always carries the JWT `userId`. Replace with a dedicated `/api/auth/me` or an\n * SSR-bootstrapped user context once available, and remove this hop.\n */\nexport function useCurrentUserId(): string {\n const [userId, setUserId] = React.useState<string>('')\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n try {\n const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: [] }),\n })\n if (!cancelled && res.ok && typeof res.result?.userId === 'string') {\n setUserId(res.result.userId)\n }\n } catch {\n if (!cancelled) setUserId('')\n }\n })()\n return () => { cancelled = true }\n }, [])\n return userId\n}\n"],
|
|
5
|
+
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,eAAe;AAgBjB,SAAS,mBAA2B;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiB,EAAE;AACrD,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,MAAM,MAAM,QAA8B,2BAA2B;AAAA,UACzE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC,EAAE,CAAC;AAAA,QACvC,CAAC;AACD,YAAI,CAAC,aAAa,IAAI,MAAM,OAAO,IAAI,QAAQ,WAAW,UAAU;AAClE,oBAAU,IAAI,OAAO,MAAM;AAAA,QAC7B;AAAA,MACF,QAAQ;AACN,YAAI,CAAC,UAAW,WAAU,EAAE;AAAA,MAC9B;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,CAAC;AACL,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -9,19 +9,20 @@ const Dialog = DialogPrimitive.Root;
|
|
|
9
9
|
const DialogTrigger = DialogPrimitive.Trigger;
|
|
10
10
|
const DialogPortal = DialogPrimitive.Portal;
|
|
11
11
|
const DialogClose = DialogPrimitive.Close;
|
|
12
|
-
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
12
|
+
const DialogOverlay = React.forwardRef(({ className, elevated, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
13
13
|
DialogPrimitive.Overlay,
|
|
14
14
|
{
|
|
15
15
|
ref,
|
|
16
16
|
className: cn(
|
|
17
|
-
"fixed inset-0
|
|
17
|
+
"fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
18
|
+
elevated ? "z-modal-elevated" : "z-modal",
|
|
18
19
|
className
|
|
19
20
|
),
|
|
20
21
|
...props
|
|
21
22
|
}
|
|
22
23
|
));
|
|
23
24
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
24
|
-
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
25
|
+
const DialogContent = React.forwardRef(({ className, children, elevated, ...props }, ref) => {
|
|
25
26
|
const t = useT();
|
|
26
27
|
React.useEffect(() => {
|
|
27
28
|
return () => {
|
|
@@ -34,14 +35,15 @@ const DialogContent = React.forwardRef(({ className, children, ...props }, ref)
|
|
|
34
35
|
};
|
|
35
36
|
}, []);
|
|
36
37
|
return /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
37
|
-
/* @__PURE__ */ jsx(DialogOverlay, {}),
|
|
38
|
+
/* @__PURE__ */ jsx(DialogOverlay, { elevated }),
|
|
38
39
|
/* @__PURE__ */ jsxs(
|
|
39
40
|
DialogPrimitive.Content,
|
|
40
41
|
{
|
|
41
42
|
ref,
|
|
42
43
|
"data-dialog-content": "",
|
|
43
44
|
className: cn(
|
|
44
|
-
"fixed inset-x-0 bottom-0
|
|
45
|
+
"fixed inset-x-0 bottom-0 flex min-h-[50vh] max-h-[70vh] w-full translate-x-0 translate-y-0 flex-col gap-4 overflow-y-auto rounded-t-2xl border-t bg-card p-6 shadow-lg",
|
|
46
|
+
elevated ? "z-modal-elevated" : "z-modal",
|
|
45
47
|
"sm:inset-auto sm:left-1/2 sm:top-1/2 sm:min-h-0 sm:h-auto sm:w-full sm:max-w-lg sm:max-h-[90vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:border",
|
|
46
48
|
"focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
47
49
|
className
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/dialog.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {\n /** Render above popovers (z-modal-elevated, 55) instead of the default z-modal (40).\n * Use when this dialog is opened from inside a popover so it isn't occluded. */\n elevated?: boolean\n }\n>(({ className, elevated, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n 'fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity data-[state=open]:animate-in data-[state=closed]:animate-out',\n elevated ? 'z-modal-elevated' : 'z-modal',\n className\n )}\n {...props}\n />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {\n /** Render above popovers (z-modal-elevated, 55) instead of the default z-modal (40).\n * Set on dialogs that open from inside another popover (e.g. the SaveFilterDialog\n * inside the AdvancedFilterPanel popover) so they aren't hidden behind the popover. */\n elevated?: boolean\n }\n>(({ className, children, elevated, ...props }, ref) => {\n const t = useT()\n\n React.useEffect(() => {\n return () => {\n if (typeof window === 'undefined') return\n window.setTimeout(() => {\n if (document.querySelector('[data-dialog-content][data-state=\"open\"]')) return\n document.body.style.removeProperty('overflow')\n document.body.style.removeProperty('pointer-events')\n }, 0)\n }\n }, [])\n\n return (\n <DialogPortal>\n <DialogOverlay elevated={elevated} />\n <DialogPrimitive.Content\n ref={ref}\n data-dialog-content=\"\"\n className={cn(\n 'fixed inset-x-0 bottom-0 flex min-h-[50vh] max-h-[70vh] w-full translate-x-0 translate-y-0 flex-col gap-4 overflow-y-auto rounded-t-2xl border-t bg-card p-6 shadow-lg',\n elevated ? 'z-modal-elevated' : 'z-modal',\n 'sm:inset-auto sm:left-1/2 sm:top-1/2 sm:min-h-0 sm:h-auto sm:w-full sm:max-w-lg sm:max-h-[90vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:border',\n 'focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out',\n className,\n )}\n {...props}\n >\n <DialogClose\n data-dialog-close=\"\"\n className=\"absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n aria-label={t('ui.dialog.close.ariaLabel', 'Close')}\n >\n <X className=\"h-4 w-4\" />\n </DialogClose>\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n})\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />\n)\nDialogHeader.displayName = 'DialogHeader'\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n <div className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />\n)\nDialogFooter.displayName = 'DialogFooter'\n\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-lg font-semibold leading-none tracking-tight', className)}\n {...props}\n />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogTrigger,\n DialogClose,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"],
|
|
5
|
+
"mappings": ";AAwBE,cAqCI,YArCJ;AAtBF,YAAY,WAAW;AACvB,YAAY,qBAAqB;AACjC,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,YAAY;AAErB,MAAM,SAAS,gBAAgB;AAE/B,MAAM,gBAAgB,gBAAgB;AAEtC,MAAM,eAAe,gBAAgB;AAErC,MAAM,cAAc,gBAAgB;AAEpC,MAAM,gBAAgB,MAAM,WAO1B,CAAC,EAAE,WAAW,UAAU,GAAG,MAAM,GAAG,QACpC;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW;AAAA,MACT;AAAA,MACA,WAAW,qBAAqB;AAAA,MAChC;AAAA,IACF;AAAA,IACC,GAAG;AAAA;AACN,CACD;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAEpD,MAAM,gBAAgB,MAAM,WAQ1B,CAAC,EAAE,WAAW,UAAU,UAAU,GAAG,MAAM,GAAG,QAAQ;AACtD,QAAM,IAAI,KAAK;AAEf,QAAM,UAAU,MAAM;AACpB,WAAO,MAAM;AACX,UAAI,OAAO,WAAW,YAAa;AACnC,aAAO,WAAW,MAAM;AACtB,YAAI,SAAS,cAAc,0CAA0C,EAAG;AACxE,iBAAS,KAAK,MAAM,eAAe,UAAU;AAC7C,iBAAS,KAAK,MAAM,eAAe,gBAAgB;AAAA,MACrD,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE,qBAAC,gBACC;AAAA,wBAAC,iBAAc,UAAoB;AAAA,IACnC;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC;AAAA,QACA,uBAAoB;AAAA,QACpB,WAAW;AAAA,UACT;AAAA,UACA,WAAW,qBAAqB;AAAA,UAChC;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEJ;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,qBAAkB;AAAA,cAClB,WAAU;AAAA,cACV,cAAY,EAAE,6BAA6B,OAAO;AAAA,cAElD,8BAAC,KAAE,WAAU,WAAU;AAAA;AAAA,UACzB;AAAA,UACC;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ,CAAC;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAEpD,MAAM,eAAe,CAAC,EAAE,WAAW,GAAG,MAAM,MAC1C,oBAAC,SAAI,WAAW,GAAG,sDAAsD,SAAS,GAAI,GAAG,OAAO;AAElG,aAAa,cAAc;AAE3B,MAAM,eAAe,CAAC,EAAE,WAAW,GAAG,MAAM,MAC1C,oBAAC,SAAI,WAAW,GAAG,0DAA0D,SAAS,GAAI,GAAG,OAAO;AAEtG,aAAa,cAAc;AAE3B,MAAM,cAAc,MAAM,WAGxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,qDAAqD,SAAS;AAAA,IAC3E,GAAG;AAAA;AACN,CACD;AACD,YAAY,cAAc,gBAAgB,MAAM;AAEhD,MAAM,oBAAoB,MAAM,WAG9B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,iCAAiC,SAAS;AAAA,IACvD,GAAG;AAAA;AACN,CACD;AACD,kBAAkB,cAAc,gBAAgB,YAAY;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/primitives/tag.js
CHANGED
|
@@ -13,7 +13,8 @@ const tagVariants = cva(
|
|
|
13
13
|
error: "border-status-error-border bg-status-error-bg text-status-error-text",
|
|
14
14
|
info: "border-status-info-border bg-status-info-bg text-status-info-text",
|
|
15
15
|
neutral: "border-status-neutral-border bg-status-neutral-bg text-status-neutral-text",
|
|
16
|
-
brand: "border-brand-violet/30 bg-brand-violet/10 text-brand-violet"
|
|
16
|
+
brand: "border-brand-violet/30 bg-brand-violet/10 text-brand-violet",
|
|
17
|
+
pink: "border-status-pink-border bg-status-pink-bg text-status-pink-text"
|
|
17
18
|
},
|
|
18
19
|
shape: {
|
|
19
20
|
pill: "rounded-full px-2.5 py-0.5",
|
|
@@ -30,7 +31,8 @@ const dotColorMap = {
|
|
|
30
31
|
error: "bg-status-error-icon",
|
|
31
32
|
info: "bg-status-info-icon",
|
|
32
33
|
neutral: "bg-status-neutral-icon",
|
|
33
|
-
brand: "bg-brand-violet"
|
|
34
|
+
brand: "bg-brand-violet",
|
|
35
|
+
pink: "bg-status-pink-icon"
|
|
34
36
|
};
|
|
35
37
|
function Tag({
|
|
36
38
|
className,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/tag.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport { X } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst tagVariants = cva(\n 'inline-flex items-center gap-1.5 border text-xs font-medium',\n {\n variants: {\n variant: {\n default: 'border-border bg-background text-muted-foreground',\n success: 'border-status-success-border bg-status-success-bg text-status-success-text',\n warning: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n error: 'border-status-error-border bg-status-error-bg text-status-error-text',\n info: 'border-status-info-border bg-status-info-bg text-status-info-text',\n neutral: 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text',\n brand: 'border-brand-violet/30 bg-brand-violet/10 text-brand-violet',\n },\n shape: {\n pill: 'rounded-full px-2.5 py-0.5',\n square: 'rounded-md px-2 py-1',\n },\n },\n defaultVariants: { variant: 'default', shape: 'pill' },\n }\n)\n\nconst dotColorMap: Record<TagVariant, string> = {\n default: 'bg-muted-foreground',\n success: 'bg-status-success-icon',\n warning: 'bg-status-warning-icon',\n error: 'bg-status-error-icon',\n info: 'bg-status-info-icon',\n neutral: 'bg-status-neutral-icon',\n brand: 'bg-brand-violet',\n}\n\nexport type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'brand'\n\nexport type TagShape = 'pill' | 'square'\n\nexport type TagProps = React.HTMLAttributes<HTMLSpanElement> &\n Pick<VariantProps<typeof tagVariants>, 'shape'> & {\n variant?: TagVariant\n dot?: boolean\n /**\n * Render an inline close (\"\u00D7\") button after the label. When provided, the button\n * fires `onRemove` on click and stops propagation so a click on the chip body is\n * not triggered. Matches Figma \"Dismiss Icon=On\" Tag variants.\n */\n onRemove?: () => void\n /** Accessible label for the close button. Defaults to `Remove`. */\n removeAriaLabel?: string\n /** Visually mute the chip and disable its close button. */\n disabled?: boolean\n }\n\nexport function Tag({\n className,\n variant = 'default',\n shape = 'pill',\n dot = false,\n onRemove,\n removeAriaLabel = 'Remove',\n disabled = false,\n children,\n ...props\n}: TagProps) {\n return (\n <span\n className={cn(\n tagVariants({ variant, shape }),\n disabled && 'opacity-60',\n className,\n )}\n aria-disabled={disabled || undefined}\n data-slot=\"tag\"\n data-shape={shape}\n {...props}\n >\n {dot && (\n <span\n className={cn('inline-block size-1.5 rounded-full shrink-0', dotColorMap[variant])}\n aria-hidden=\"true\"\n />\n )}\n <span className=\"min-w-0\">{children}</span>\n {onRemove ? (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={(event) => {\n event.stopPropagation()\n onRemove()\n }}\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60',\n shape === 'pill' ? 'size-3' : 'size-4',\n )}\n aria-label={removeAriaLabel}\n data-slot=\"tag-remove\"\n >\n <X\n className={shape === 'pill' ? 'size-2.5' : 'size-3.5'}\n aria-hidden=\"true\"\n />\n </button>\n ) : null}\n </span>\n )\n}\n\n/**\n * Helper type: modules define their own tag label \u2192 variant mapping.\n *\n * @example\n * const leadTagMap: TagMap<'customer' | 'hot' | 'inactive'> = {\n * customer: 'success',\n * hot: 'error',\n * inactive: 'neutral',\n * }\n *\n * <Tag variant={leadTagMap[tag.type]} dot>{tag.label}</Tag>\n */\nexport type TagMap<T extends string = string> = Record<T, TagVariant>\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { X } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst tagVariants = cva(\n 'inline-flex items-center gap-1.5 border text-xs font-medium',\n {\n variants: {\n variant: {\n default: 'border-border bg-background text-muted-foreground',\n success: 'border-status-success-border bg-status-success-bg text-status-success-text',\n warning: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n error: 'border-status-error-border bg-status-error-bg text-status-error-text',\n info: 'border-status-info-border bg-status-info-bg text-status-info-text',\n neutral: 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text',\n brand: 'border-brand-violet/30 bg-brand-violet/10 text-brand-violet',\n pink: 'border-status-pink-border bg-status-pink-bg text-status-pink-text',\n },\n shape: {\n pill: 'rounded-full px-2.5 py-0.5',\n square: 'rounded-md px-2 py-1',\n },\n },\n defaultVariants: { variant: 'default', shape: 'pill' },\n }\n)\n\nconst dotColorMap: Record<TagVariant, string> = {\n default: 'bg-muted-foreground',\n success: 'bg-status-success-icon',\n warning: 'bg-status-warning-icon',\n error: 'bg-status-error-icon',\n info: 'bg-status-info-icon',\n neutral: 'bg-status-neutral-icon',\n brand: 'bg-brand-violet',\n pink: 'bg-status-pink-icon',\n}\n\nexport type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'brand' | 'pink'\n\nexport type TagShape = 'pill' | 'square'\n\nexport type TagProps = React.HTMLAttributes<HTMLSpanElement> &\n Pick<VariantProps<typeof tagVariants>, 'shape'> & {\n variant?: TagVariant\n dot?: boolean\n /**\n * Render an inline close (\"\u00D7\") button after the label. When provided, the button\n * fires `onRemove` on click and stops propagation so a click on the chip body is\n * not triggered. Matches Figma \"Dismiss Icon=On\" Tag variants.\n */\n onRemove?: () => void\n /** Accessible label for the close button. Defaults to `Remove`. */\n removeAriaLabel?: string\n /** Visually mute the chip and disable its close button. */\n disabled?: boolean\n }\n\nexport function Tag({\n className,\n variant = 'default',\n shape = 'pill',\n dot = false,\n onRemove,\n removeAriaLabel = 'Remove',\n disabled = false,\n children,\n ...props\n}: TagProps) {\n return (\n <span\n className={cn(\n tagVariants({ variant, shape }),\n disabled && 'opacity-60',\n className,\n )}\n aria-disabled={disabled || undefined}\n data-slot=\"tag\"\n data-shape={shape}\n {...props}\n >\n {dot && (\n <span\n className={cn('inline-block size-1.5 rounded-full shrink-0', dotColorMap[variant])}\n aria-hidden=\"true\"\n />\n )}\n <span className=\"min-w-0\">{children}</span>\n {onRemove ? (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={(event) => {\n event.stopPropagation()\n onRemove()\n }}\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60',\n shape === 'pill' ? 'size-3' : 'size-4',\n )}\n aria-label={removeAriaLabel}\n data-slot=\"tag-remove\"\n >\n <X\n className={shape === 'pill' ? 'size-2.5' : 'size-3.5'}\n aria-hidden=\"true\"\n />\n </button>\n ) : null}\n </span>\n )\n}\n\n/**\n * Helper type: modules define their own tag label \u2192 variant mapping.\n *\n * @example\n * const leadTagMap: TagMap<'customer' | 'hot' | 'inactive'> = {\n * customer: 'success',\n * hot: 'error',\n * inactive: 'neutral',\n * }\n *\n * <Tag variant={leadTagMap[tag.type]} dot>{tag.label}</Tag>\n */\nexport type TagMap<T extends string = string> = Record<T, TagVariant>\n"],
|
|
5
|
+
"mappings": "AAuEI,SAYI,KAZJ;AAtEJ,SAAS,SAAS;AAClB,SAAS,WAA8B;AACvC,SAAS,UAAU;AAEnB,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAS;AAAA,QACT,MAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAS;AAAA,QACT,MAAS;AAAA,MACX;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,SAAS,WAAW,OAAO,OAAO;AAAA,EACvD;AACF;AAEA,MAAM,cAA0C;AAAA,EAC9C,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAS;AAAA,EACT,MAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAS;AAAA,EACT,MAAS;AACX;AAsBO,SAAS,IAAI;AAAA,EAClB;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AAAA,EACN;AAAA,EACA,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAa;AACX,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT,YAAY,EAAE,SAAS,MAAM,CAAC;AAAA,QAC9B,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MACA,iBAAe,YAAY;AAAA,MAC3B,aAAU;AAAA,MACV,cAAY;AAAA,MACX,GAAG;AAAA,MAEH;AAAA,eACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,GAAG,+CAA+C,YAAY,OAAO,CAAC;AAAA,YACjF,eAAY;AAAA;AAAA,QACd;AAAA,QAEF,oBAAC,UAAK,WAAU,WAAW,UAAS;AAAA,QACnC,WACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL;AAAA,YACA,SAAS,CAAC,UAAU;AAClB,oBAAM,gBAAgB;AACtB,uBAAS;AAAA,YACX;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA,UAAU,SAAS,WAAW;AAAA,YAChC;AAAA,YACA,cAAY;AAAA,YACZ,aAAU;AAAA,YAEV;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW,UAAU,SAAS,aAAa;AAAA,gBAC3C,eAAY;AAAA;AAAA,YACd;AAAA;AAAA,QACF,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3278.1.a4d26475e4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -141,12 +141,12 @@
|
|
|
141
141
|
"remark-gfm": "^4.0.1"
|
|
142
142
|
},
|
|
143
143
|
"peerDependencies": {
|
|
144
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
144
|
+
"@open-mercato/shared": "0.6.1-develop.3278.1.a4d26475e4",
|
|
145
145
|
"react": ">=18.0.0",
|
|
146
146
|
"react-dom": ">=18.0.0"
|
|
147
147
|
},
|
|
148
148
|
"devDependencies": {
|
|
149
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
149
|
+
"@open-mercato/shared": "0.6.1-develop.3278.1.a4d26475e4",
|
|
150
150
|
"@testing-library/dom": "^10.4.1",
|
|
151
151
|
"@testing-library/jest-dom": "^6.9.1",
|
|
152
152
|
"@testing-library/react": "^16.3.1",
|