@sincpro/web-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/chatter/Chatter.d.ts +161 -0
- package/dist/components/chatter/Chatter.js +232 -0
- package/dist/components/chatter/Messages.d.ts +66 -0
- package/dist/components/chatter/Messages.js +43 -0
- package/dist/components/chatter/Timeline.d.ts +21 -0
- package/dist/components/chatter/Timeline.js +30 -0
- package/dist/components/chatter/index.d.ts +3 -0
- package/dist/components/chatter/index.js +4 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +43 -0
- package/dist/components/form/Sheet.js +83 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +74 -0
- package/dist/components/lines/LineItems.js +229 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +73 -0
- package/dist/components/listing/ListingBar.js +105 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +106 -0
- package/dist/components/listing/Row.js +122 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +71 -0
- package/dist/components/record/RecordHeader.js +72 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +619 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +377 -0
- package/dist/foundations/copy.js +472 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +16 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +24 -0
- package/dist/primitives/choice/SearchField.js +27 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +110 -0
- package/dist/primitives/date/DatePicker.d.ts +34 -0
- package/dist/primitives/date/DatePicker.js +99 -0
- package/dist/primitives/date/index.d.ts +2 -0
- package/dist/primitives/date/index.js +4 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +166 -0
- package/dist/primitives/field/Field.js +240 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/index.d.ts +7 -0
- package/dist/primitives/field/index.js +11 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +19 -0
- package/dist/primitives/index.js +20 -0
- package/dist/primitives/menu/Menu.d.ts +100 -0
- package/dist/primitives/menu/Menu.js +59 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +3 -0
- package/dist/primitives/notice/index.js +5 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +3 -0
- package/dist/primitives/overlay/index.js +5 -0
- package/dist/primitives/picker/Picker.d.ts +48 -0
- package/dist/primitives/picker/Picker.js +150 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +135 -0
- package/dist/primitives/table/DataTable.js +244 -0
- package/dist/primitives/table/EditableTable.d.ts +96 -0
- package/dist/primitives/table/EditableTable.js +423 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +70 -0
- package/dist/primitives/table/SheetTable.js +197 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +32 -0
- package/dist/primitives/tag/TagsField.js +182 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/value/Copyable.d.ts +37 -0
- package/dist/primitives/value/Copyable.js +50 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +80 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/Relation.d.ts +69 -0
- package/dist/primitives/value/Relation.js +57 -0
- package/dist/primitives/value/Totals.d.ts +26 -0
- package/dist/primitives/value/Totals.js +4 -0
- package/dist/primitives/value/Value.d.ts +77 -0
- package/dist/primitives/value/Value.js +119 -0
- package/dist/primitives/value/index.d.ts +8 -0
- package/dist/primitives/value/index.js +9 -0
- package/dist/tokens.css +1025 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +113 -0
- package/dist/views/list/List.js +97 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +269 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +76 -0
- package/dist/views/list/derive.js +181 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +103 -0
- package/dist/views/list/types.d.ts +272 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +126 -0
- package/dist/views/record/Record.js +141 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +109 -4
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { blankGroup, ExpressionView, treeFromFilter, treeToFilter, WhereTree, } from "./FilterBuilder.js";
|
|
4
|
+
import { JsonEditor } from "./JsonEditor.js";
|
|
5
|
+
import { filterableNames, } from "./model.js";
|
|
6
|
+
import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
7
|
+
import { GRAIN_ORDER } from "../../foundations/grouping.js";
|
|
8
|
+
import { useSurface } from "../../foundations/surface.js";
|
|
9
|
+
import { Rule } from "../../primitives/block/Block.js";
|
|
10
|
+
import { Button } from "../../primitives/button/index.js";
|
|
11
|
+
import { Tags } from "../../primitives/tag/Tags.js";
|
|
12
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
13
|
+
import { Fragment, useMemo, useState } from "react";
|
|
14
|
+
/**
|
|
15
|
+
* Los cortes de una fecha, de GRUESO a fino, con `null` adelante: sin corte, un
|
|
16
|
+
* campo de fecha parte por instante y cada fila termina en su propio montón.
|
|
17
|
+
*
|
|
18
|
+
* El orden no es una preferencia — un día entero vive adentro de un solo mes— y
|
|
19
|
+
* es el mismo que `GRAIN_ORDER` publica para la barra: una sola dirección para
|
|
20
|
+
* las dos pantallas. La ficha de un nivel rota por esta lista.
|
|
21
|
+
*/
|
|
22
|
+
const GRAINS = [null, ...GRAIN_ORDER];
|
|
23
|
+
/** Qué tipos admiten un grano: recortar un texto por mes no significa nada. */
|
|
24
|
+
const DATE_TYPES = new Set(["date", "datetime"]);
|
|
25
|
+
/** Los operadores que un plegado —siempre un número— o `count` admiten en un `having`: los
|
|
26
|
+
* mismos que la definición del modelo publica para un campo numérico, `is null` aparte, que
|
|
27
|
+
* no significa nada sobre un número que el propio motor calculó. */
|
|
28
|
+
const COMPARABLE = ["=", "!=", ">", ">=", "<", "<=", "between"];
|
|
29
|
+
/** Las funciones que un plegado puede pedir, cerradas del lado del servidor porque el nombre
|
|
30
|
+
* llega a SQL. */
|
|
31
|
+
const FOLD_FUNCTIONS = ["sum", "avg", "min", "max", "count"];
|
|
32
|
+
function isEmpty(criteria) {
|
|
33
|
+
return (criteria.where === undefined &&
|
|
34
|
+
(criteria.order ?? []).length === 0 &&
|
|
35
|
+
(criteria.grouping?.by ?? []).length === 0 &&
|
|
36
|
+
Object.keys(criteria.grouping?.totals ?? {}).length === 0 &&
|
|
37
|
+
Object.keys(criteria.specification ?? {}).length === 0);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* LA FICHA QUE AGREGA: un «+» que abre la lista de lo que se puede elegir.
|
|
41
|
+
*
|
|
42
|
+
* Es lo mismo que hace el numeral en la barra, y por la misma razón: elegir
|
|
43
|
+
* entre veinte campos es elegir de una LISTA, no desplegar un `<select>` que ya
|
|
44
|
+
* viene con uno puesto y que hay que cambiar. Con el `<select>`, agregar un
|
|
45
|
+
* nivel ponía el primer campo del modelo aunque nadie lo hubiera pedido.
|
|
46
|
+
*/
|
|
47
|
+
function AddTag({ label, options, onPick, }) {
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
if (options.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return (_jsxs("span", { className: "relative inline-flex shrink-0", children: [_jsxs("button", { type: "button", onClick: () => setOpen((current) => !current), "aria-expanded": open, className: "inline-flex shrink-0 items-center gap-1 rounded-pill border border-dashed border-line-strong px-2 py-0.5 text-micro text-muted transition-colors duration-(--duration-instant) hover:border-line-strong hover:bg-ink/5 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-3", strokeWidth: 2, "aria-hidden": true }), label] }), open ? (_jsx("span", { className: "absolute top-full left-0 z-40 mt-1 max-h-64 overflow-y-auto rounded-panel border border-line-strong bg-surface p-1 shadow-float", children: _jsx(Tags, { stack: true, label: label, items: options.map((name) => ({
|
|
52
|
+
id: name,
|
|
53
|
+
label: name,
|
|
54
|
+
onSelect: () => {
|
|
55
|
+
onPick(name);
|
|
56
|
+
setOpen(false);
|
|
57
|
+
},
|
|
58
|
+
})) }) })) : null] }));
|
|
59
|
+
}
|
|
60
|
+
/** El corte siguiente, y vuelta a empezar. */
|
|
61
|
+
function nextGrain(grain) {
|
|
62
|
+
const at = GRAINS.indexOf(grain ?? null);
|
|
63
|
+
return GRAINS[(at + 1) % GRAINS.length] ?? null;
|
|
64
|
+
}
|
|
65
|
+
export function CriteriaBuilder({ model, initial, focus, onApply, onCancel, }) {
|
|
66
|
+
const surface = useSurface();
|
|
67
|
+
const names = Object.keys(model.fields).sort();
|
|
68
|
+
/* Nunca un relacional ni un valor-objeto: ninguno de los dos tiene una comparación propia
|
|
69
|
+
(`ops` vacío), así que ninguno sirve de blanco para un filtro, un orden o un nivel. */
|
|
70
|
+
const scalarNames = filterableNames(model);
|
|
71
|
+
const sortable = names.filter((name) => model.fields[name]?.sortable === true);
|
|
72
|
+
const decomposed = initial?.where === undefined ? null : treeFromFilter(initial.where);
|
|
73
|
+
const [tree, setTree] = useState(() => decomposed !== null && decomposed.kind === "group"
|
|
74
|
+
? decomposed
|
|
75
|
+
: decomposed !== null
|
|
76
|
+
? { kind: "group", join: "all", children: [decomposed] }
|
|
77
|
+
: blankGroup(model, scalarNames[0]));
|
|
78
|
+
const copy = useUiCopy();
|
|
79
|
+
const [lockedWhere, setLockedWhere] = useState(initial?.where !== undefined && decomposed === null ? initial.where : null);
|
|
80
|
+
const [order, setOrder] = useState(initial?.order ?? []);
|
|
81
|
+
const [levels, setLevels] = useState(initial?.grouping?.by ?? []);
|
|
82
|
+
const [totals, setTotals] = useState(initial?.grouping?.totals ?? {});
|
|
83
|
+
const [groupOrder, setGroupOrder] = useState(initial?.grouping?.order ?? []);
|
|
84
|
+
const [groupLimit, setGroupLimit] = useState(initial?.grouping?.pagination?.limit);
|
|
85
|
+
/* `having` es un filtro como cualquier otro, sobre un modelo sintético cuyos únicos campos
|
|
86
|
+
son los nombres que admite: los de `totals`, más `count`. Reutilizar `WhereTree` sobre ese
|
|
87
|
+
modelo es más simple y más correcto que armar un editor de condiciones aparte. */
|
|
88
|
+
const havingModel = useMemo(() => ({
|
|
89
|
+
aggregate: "having",
|
|
90
|
+
identity: "count",
|
|
91
|
+
fields: Object.fromEntries([
|
|
92
|
+
["count", { type: "integer", nullable: false, sortable: false, ops: COMPARABLE }],
|
|
93
|
+
...Object.keys(totals).map((name) => [
|
|
94
|
+
name,
|
|
95
|
+
{ type: "number", nullable: false, sortable: false, ops: COMPARABLE },
|
|
96
|
+
]),
|
|
97
|
+
]),
|
|
98
|
+
}), [totals]);
|
|
99
|
+
const havingDecomposed = initial?.grouping?.having === undefined ? null : treeFromFilter(initial.grouping.having);
|
|
100
|
+
const [havingTree, setHavingTree] = useState(() => havingDecomposed !== null && havingDecomposed.kind === "group"
|
|
101
|
+
? havingDecomposed
|
|
102
|
+
: havingDecomposed !== null
|
|
103
|
+
? { kind: "group", join: "all", children: [havingDecomposed] }
|
|
104
|
+
: blankGroup(havingModel, "count"));
|
|
105
|
+
const [fields, setFields] = useState(Object.keys(initial?.specification ?? {}));
|
|
106
|
+
const [rawText, setRawText] = useState(() => initial === undefined ? "" : JSON.stringify(initial, null, 2));
|
|
107
|
+
const [rawError, setRawError] = useState(null);
|
|
108
|
+
/** Lo que los controles dicen, junto: es lo que se aplica y lo que el JSON muestra. */
|
|
109
|
+
function assemble(next) {
|
|
110
|
+
const where = next.locked ?? lockedWhere ?? treeToFilter(next.tree ?? tree);
|
|
111
|
+
const sorts = next.order ?? order;
|
|
112
|
+
const by = next.levels ?? levels;
|
|
113
|
+
const folds = next.totals ?? totals;
|
|
114
|
+
const having = treeToFilter(next.havingTree ?? havingTree);
|
|
115
|
+
const groupSorts = next.groupOrder ?? groupOrder;
|
|
116
|
+
const limit = "groupLimit" in next ? next.groupLimit : groupLimit;
|
|
117
|
+
const chosen = next.fields ?? fields;
|
|
118
|
+
/* Sin niveles no hay qué plegar, qué filtrar entre montones ni en qué orden o página
|
|
119
|
+
volverían — un total, un `having` o una página puestos y luego vaciados de niveles no
|
|
120
|
+
tienen que sobrevivir sueltos en la criteria que se manda. */
|
|
121
|
+
const grouping = by.length === 0
|
|
122
|
+
? {}
|
|
123
|
+
: {
|
|
124
|
+
by,
|
|
125
|
+
...(Object.keys(folds).length === 0 ? {} : { totals: folds }),
|
|
126
|
+
...(having === undefined ? {} : { having }),
|
|
127
|
+
...(groupSorts.length === 0 ? {} : { order: groupSorts }),
|
|
128
|
+
...(limit === undefined ? {} : { pagination: { limit } }),
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
...(where === undefined ? {} : { where }),
|
|
132
|
+
...(sorts.length === 0 ? {} : { order: sorts }),
|
|
133
|
+
...(Object.keys(grouping).length === 0 ? {} : { grouping }),
|
|
134
|
+
...(chosen.length === 0
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
specification: Object.fromEntries(chosen.map((name) => [name, {}])),
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/* Tocar cualquier control reescribe el JSON entero: las dos vistas son la misma criteria. */
|
|
142
|
+
function push(next) {
|
|
143
|
+
if (next.tree !== undefined)
|
|
144
|
+
setTree(next.tree);
|
|
145
|
+
if (next.locked !== undefined)
|
|
146
|
+
setLockedWhere(next.locked);
|
|
147
|
+
if (next.order !== undefined)
|
|
148
|
+
setOrder(next.order);
|
|
149
|
+
if (next.levels !== undefined)
|
|
150
|
+
setLevels(next.levels);
|
|
151
|
+
if (next.totals !== undefined)
|
|
152
|
+
setTotals(next.totals);
|
|
153
|
+
if (next.havingTree !== undefined)
|
|
154
|
+
setHavingTree(next.havingTree);
|
|
155
|
+
if (next.groupOrder !== undefined)
|
|
156
|
+
setGroupOrder(next.groupOrder);
|
|
157
|
+
if ("groupLimit" in next)
|
|
158
|
+
setGroupLimit(next.groupLimit);
|
|
159
|
+
if (next.fields !== undefined)
|
|
160
|
+
setFields(next.fields);
|
|
161
|
+
const built = assemble(next);
|
|
162
|
+
setRawText(isEmpty(built) ? "" : JSON.stringify(built, null, 2));
|
|
163
|
+
setRawError(null);
|
|
164
|
+
}
|
|
165
|
+
/* Escribir el JSON rehace los controles, mientras lo escrito sea una criteria. */
|
|
166
|
+
function updateRaw(text) {
|
|
167
|
+
setRawText(text);
|
|
168
|
+
if (text.trim() === "") {
|
|
169
|
+
setRawError(null);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(text);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
setRawError(currentCopy().builder.invalidJson);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
181
|
+
setRawError(currentCopy().builder.notCriteria);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
setRawError(null);
|
|
185
|
+
const node = parsed.where === undefined ? null : treeFromFilter(parsed.where);
|
|
186
|
+
if (node !== null) {
|
|
187
|
+
setTree(node.kind === "group" ? node : { kind: "group", join: "all", children: [node] });
|
|
188
|
+
setLockedWhere(null);
|
|
189
|
+
}
|
|
190
|
+
else if (parsed.where !== undefined) {
|
|
191
|
+
setLockedWhere(parsed.where);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
setTree(blankGroup(model, scalarNames[0]));
|
|
195
|
+
setLockedWhere(null);
|
|
196
|
+
}
|
|
197
|
+
setOrder(parsed.order ?? []);
|
|
198
|
+
setLevels(parsed.grouping?.by ?? []);
|
|
199
|
+
setTotals(parsed.grouping?.totals ?? {});
|
|
200
|
+
setGroupOrder(parsed.grouping?.order ?? []);
|
|
201
|
+
setGroupLimit(parsed.grouping?.pagination?.limit);
|
|
202
|
+
const havingNode = parsed.grouping?.having === undefined ? null : treeFromFilter(parsed.grouping.having);
|
|
203
|
+
setHavingTree(havingNode !== null && havingNode.kind === "group"
|
|
204
|
+
? havingNode
|
|
205
|
+
: havingNode !== null
|
|
206
|
+
? { kind: "group", join: "all", children: [havingNode] }
|
|
207
|
+
: blankGroup(havingModel, "count"));
|
|
208
|
+
setFields(Object.keys(parsed.specification ?? {}));
|
|
209
|
+
}
|
|
210
|
+
const box = surface === "chrome"
|
|
211
|
+
? "border-chrome-ink/20 bg-chrome text-chrome-ink"
|
|
212
|
+
: "border-ink/15 bg-surface text-ink";
|
|
213
|
+
const select = `min-w-0 flex-1 basis-40 rounded-control border px-2 py-1 text-meta ${box}`;
|
|
214
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 p-3", children: [_jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.filter }), _jsx("div", { className: "max-h-72 min-w-0 overflow-auto", children: lockedWhere !== null ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "mb-2 text-meta text-muted", children: copy.search.unreadable }), _jsx(ExpressionView, { node: lockedWhere, model: model })] })) : (_jsx(WhereTree, { model: model, group: tree, onChange: (next) => push({ tree: next }), ...(focus === undefined ? {} : { focus }) })) })] }), _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.order }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1", children: [_jsx(Tags, { items: order.map((sort, index) => ({
|
|
215
|
+
id: `${sort.field}-${index}`,
|
|
216
|
+
label: sort.field,
|
|
217
|
+
note: sort.descending === true ? "↓" : "↑",
|
|
218
|
+
title: sort.descending === true
|
|
219
|
+
? `${copy.builder.descending} · ${copy.builder.flip}`
|
|
220
|
+
: `${copy.builder.ascending} · ${copy.builder.flip}`,
|
|
221
|
+
onSelect: () => push({
|
|
222
|
+
order: order.map((one, at) => at === index ? { ...one, descending: one.descending !== true } : one),
|
|
223
|
+
}),
|
|
224
|
+
onRemove: () => push({ order: order.filter((_, at) => at !== index) }),
|
|
225
|
+
removeLabel: fillCopy(copy.builder.dropOrderBy, { field: sort.field }),
|
|
226
|
+
})) }), _jsx(AddTag, { label: copy.builder.addOrder, options: sortable.filter((name) => !order.some((one) => one.field === name)), onPick: (name) => push({ order: [...order, { field: name, descending: false }] }) })] }), order.length === 0 ? (_jsx("p", { className: "text-meta text-muted", children: copy.builder.noOrder })) : null] }), _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.groupBy }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1", children: [levels.map((level, index) => (_jsxs(Fragment, { children: [index === 0 ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted", "aria-hidden": true, children: "\u203A" })), _jsx(Tags, { items: [
|
|
227
|
+
{
|
|
228
|
+
id: `${level.field}-${index}`,
|
|
229
|
+
label: level.field,
|
|
230
|
+
...(level.grain == null ? {} : { note: level.grain }),
|
|
231
|
+
...(DATE_TYPES.has(model.fields[level.field]?.type ?? "")
|
|
232
|
+
? {
|
|
233
|
+
title: copy.builder.changeGrain,
|
|
234
|
+
onSelect: () => push({
|
|
235
|
+
levels: levels.map((one, at) => at === index ? { ...one, grain: nextGrain(one.grain) } : one),
|
|
236
|
+
}),
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
onRemove: () => push({ levels: levels.filter((_, at) => at !== index) }),
|
|
240
|
+
removeLabel: fillCopy(copy.builder.dropLevel, { field: level.field }),
|
|
241
|
+
},
|
|
242
|
+
] })] }, `${level.field}-${index}`))), _jsx(AddTag, { label: copy.builder.addGroup, options: scalarNames.filter((name) => !levels.some((one) => one.field === name)), onPick: (name) => push({ levels: [...levels, { field: name }] }) })] }), levels.length === 0 ? (_jsx("p", { className: "text-meta text-muted", children: copy.builder.noGrouping })) : null] }), levels.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.totals }), Object.keys(totals).length === 0 ? (_jsx("p", { className: "text-meta text-muted", children: copy.builder.noTotals })) : null, Object.entries(totals).map(([name, [fn, field]]) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("select", { value: fn, onChange: (event) => push({
|
|
243
|
+
totals: { ...totals, [name]: [event.target.value, field] },
|
|
244
|
+
}), className: `min-w-0 flex-1 basis-28 rounded-control border px-2 py-1 text-meta ${box}`, children: FOLD_FUNCTIONS.map((one) => (_jsx("option", { value: one, children: one }, one))) }), _jsx("select", { value: field, onChange: (event) => push({ totals: { ...totals, [name]: [fn, event.target.value] } }), className: select, children: scalarNames.map((one) => (_jsx("option", { value: one, children: one }, one))) }), _jsx("span", { className: "shrink-0 text-micro text-muted", children: fillCopy(copy.builder.totalAs, { name }) }), _jsx("button", { type: "button", "aria-label": copy.builder.dropTotal, onClick: () => {
|
|
245
|
+
const { [name]: _dropped, ...rest } = totals;
|
|
246
|
+
push({ totals: rest });
|
|
247
|
+
}, className: "ml-auto shrink-0 text-muted", children: _jsx(Trash2, { className: "size-4" }) })] }, name))), _jsx("div", { children: _jsxs(Button, { role: "ghost", disabled: scalarNames.length === 0, onClick: () => {
|
|
248
|
+
let name = "total";
|
|
249
|
+
let index = 2;
|
|
250
|
+
while (name in totals)
|
|
251
|
+
name = `total_${index++}`;
|
|
252
|
+
push({
|
|
253
|
+
totals: { ...totals, [name]: ["sum", scalarNames[0] ?? ""] },
|
|
254
|
+
});
|
|
255
|
+
}, children: [_jsx(Plus, { className: "size-4" }), " ", copy.builder.addTotal] }) })] }), _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.filterPiles }), _jsx("p", { className: "text-meta text-muted", children: copy.builder.pilesNote }), _jsx(WhereTree, { model: havingModel, group: havingTree, onChange: (next) => push({ havingTree: next }) })] }), _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.pileOrder }), groupOrder.length === 0 ? (_jsx("p", { className: "text-meta text-muted", children: copy.builder.noPileOrder })) : null, groupOrder.map((sort, index) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("select", { value: sort.field, onChange: (event) => push({
|
|
256
|
+
groupOrder: groupOrder.map((one, at) => at === index ? { ...one, field: event.target.value } : one),
|
|
257
|
+
}), className: select, children: [
|
|
258
|
+
...levels.map((level) => level.field),
|
|
259
|
+
...Object.keys(totals),
|
|
260
|
+
"count",
|
|
261
|
+
].map((name) => (_jsx("option", { value: name, children: name }, name))) }), _jsxs("select", { value: sort.descending === true ? "desc" : "asc", onChange: (event) => push({
|
|
262
|
+
groupOrder: groupOrder.map((one, at) => at === index
|
|
263
|
+
? { ...one, descending: event.target.value === "desc" }
|
|
264
|
+
: one),
|
|
265
|
+
}), className: `min-w-0 flex-1 basis-32 rounded-control border px-2 py-1 text-meta ${box}`, children: [_jsx("option", { value: "asc", children: copy.builder.ascending }), _jsx("option", { value: "desc", children: copy.builder.descending })] }), _jsx("button", { type: "button", "aria-label": copy.builder.dropOrder, onClick: () => push({ groupOrder: groupOrder.filter((_, at) => at !== index) }), className: "ml-auto shrink-0 text-muted", children: _jsx(Trash2, { className: "size-4" }) })] }, index))), _jsx("div", { children: _jsxs(Button, { role: "ghost", onClick: () => push({
|
|
266
|
+
groupOrder: [...groupOrder, { field: "count", descending: true }],
|
|
267
|
+
}), children: [_jsx(Plus, { className: "size-4" }), " ", copy.builder.addPileOrder] }) })] }), _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.pilePage }), _jsx("p", { className: "text-meta text-muted", children: copy.builder.pilePageNote }), _jsxs("label", { className: "flex items-center gap-2 text-meta", children: [_jsx("input", { type: "checkbox", checked: groupLimit !== undefined, onChange: (event) => push({ groupLimit: event.target.checked ? 20 : undefined }) }), copy.builder.limitPiles] }), groupLimit !== undefined ? (_jsx("input", { type: "number", min: 0, value: groupLimit, onChange: (event) => push({ groupLimit: Number(event.target.value) }), className: `w-32 rounded-control border px-2 py-1 text-meta ${box}` })) : null] })] })) : null, _jsxs("section", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(Rule, { label: copy.builder.fields }), _jsx("p", { className: "text-meta text-muted", children: fields.length === 0
|
|
268
|
+
? copy.builder.allFields
|
|
269
|
+
: fillCopy(copy.builder.someFields, {
|
|
270
|
+
shown: fields.length,
|
|
271
|
+
total: names.length,
|
|
272
|
+
}) }), _jsx("div", { className: "flex flex-wrap gap-2", children: names.map((name) => {
|
|
273
|
+
const chosen = fields.includes(name);
|
|
274
|
+
return (_jsx("button", { type: "button", onClick: () => push({
|
|
275
|
+
fields: chosen ? fields.filter((one) => one !== name) : [...fields, name],
|
|
276
|
+
}), className: `rounded-pill border px-2 py-0.5 text-micro ${chosen ? "border-accent/40 bg-accent/18 text-ink" : `text-muted ${box}`}`, children: name }, name));
|
|
277
|
+
}) })] }), _jsx(JsonEditor, { value: rawText, onChange: updateRaw, error: rawError, height: "14rem", label: copy.builder.json }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { role: "primary", disabled: rawError !== null, onClick: () => onApply(rawText.trim() === "" ? undefined : JSON.parse(rawText)), children: copy.builder.apply }), onCancel === undefined ? null : (_jsx(Button, { role: "ghost", onClick: onCancel, children: copy.builder.cancel }))] })] }));
|
|
278
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CriteriaJoin, type CriteriaPath, type SearchCriteria } from "./criteria.js";
|
|
2
|
+
import type { Surface } from "../../foundations/types.js";
|
|
3
|
+
export type CriteriaChipsProps = {
|
|
4
|
+
/** Lo que la barra tiene puesto. Grupos y filtros mezclados: acá se separan. */
|
|
5
|
+
criteria: readonly SearchCriteria[];
|
|
6
|
+
/** Abrir el constructor sobre una instancia. */
|
|
7
|
+
onOpen?: (path: CriteriaPath) => void;
|
|
8
|
+
/** Cambiar cómo se juntan los de un nodo. */
|
|
9
|
+
onJoin?: (path: CriteriaPath, join: CriteriaJoin) => void;
|
|
10
|
+
/** Sacar una instancia, o una rama entera. */
|
|
11
|
+
onRemove?: (path: CriteriaPath) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Cuál nodo está ABIERTO: dónde va a caer lo próximo que se escriba.
|
|
14
|
+
*
|
|
15
|
+
* Se marca porque si no, escribir `#and` y después una condición la mete en un
|
|
16
|
+
* lugar que nadie eligió mirando: el grupo existe, la condición entra, y el
|
|
17
|
+
* único que sabía dónde era el que la escribió.
|
|
18
|
+
*/
|
|
19
|
+
open?: CriteriaPath;
|
|
20
|
+
surface?: Surface;
|
|
21
|
+
};
|
|
22
|
+
export declare function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surface, }: CriteriaChipsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { filtersOf, groupsOf, } from "./criteria.js";
|
|
4
|
+
import { JoinWord } from "./JoinWord.js";
|
|
5
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { bySurface, useSurface } from "../../foundations/surface.js";
|
|
7
|
+
import { ChevronRight, Layers, ListFilter, X } from "lucide-react";
|
|
8
|
+
export function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surface, }) {
|
|
9
|
+
const sup = useSurface(surface);
|
|
10
|
+
const copy = useUiCopy();
|
|
11
|
+
const groups = groupsOf(criteria);
|
|
12
|
+
const filters = filtersOf(criteria);
|
|
13
|
+
/* La ruta se cuenta sobre la lista ENTERA y no sobre la mitad dibujada: es la
|
|
14
|
+
identidad de la instancia, y una identidad que cambia según de qué lado se
|
|
15
|
+
la mire no identifica nada. */
|
|
16
|
+
const pathOf = (one) => [
|
|
17
|
+
criteria.findIndex((each) => each.id === one.id),
|
|
18
|
+
];
|
|
19
|
+
return (_jsxs(_Fragment, { children: [groups.length === 0 ? null : (_jsx(Zone, { lead: copy.search.groupedBy, icon: Layers, surface: sup, children: groups.map((one, at) => (_jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [at === 0 ? null : (_jsx(ChevronRight, { className: "size-3 shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true })), _jsx(Chip, { label: one.label, path: pathOf(one), ...(onOpen === undefined ? {} : { onOpen }), ...(onRemove === undefined ? {} : { onRemove }), surface: sup })] }, one.id))) })), filters.length === 0 ? null : (_jsx(Zone, { lead: copy.search.narrowedBy, icon: ListFilter, surface: sup, children: filters.map((one) => (_jsx(Node, { node: one, path: pathOf(one), ...(onOpen === undefined ? {} : { onOpen }), ...(onJoin === undefined ? {} : { onJoin }), ...(onRemove === undefined ? {} : { onRemove }), ...(open === undefined ? {} : { open }), surface: sup }, one.id))) }))] }));
|
|
20
|
+
}
|
|
21
|
+
function Node({ node, path, onOpen, onJoin, onRemove, open, surface, }) {
|
|
22
|
+
const copy = useUiCopy();
|
|
23
|
+
if (node.kind !== "join") {
|
|
24
|
+
/* Una instancia que no lleva expresión no tiene qué editar: el constructor
|
|
25
|
+
edita lo que VIAJA, y ésta la contesta quien la declaró. Se ve y se saca
|
|
26
|
+
como cualquiera; lo que no hace es prometer una puerta que da a nada. */
|
|
27
|
+
const editable = node.kind === "group" || node.where !== undefined;
|
|
28
|
+
return (_jsx(Chip, { label: node.label, path: path, ...(onOpen === undefined || !editable ? {} : { onOpen }), ...(onRemove === undefined ? {} : { onRemove }), surface: surface }));
|
|
29
|
+
}
|
|
30
|
+
/* El nodo abierto se marca: es donde va a caer lo próximo. Un anillo y no un
|
|
31
|
+
fondo, porque adentro hay fichas que ya tienen el suyo. */
|
|
32
|
+
const isOpen = open !== undefined && open.join(".") === path.join(".");
|
|
33
|
+
return (_jsxs("span", { className: `inline-flex min-w-0 flex-wrap items-center gap-1 rounded-control border px-1.5 py-0.5 ${bySurface(surface, "border-line bg-ink/4", "border-chrome-ink/25 bg-chrome-ink/8")} ${isOpen ? "ring-1 ring-primary/60" : ""}`, children: [_jsx(JoinWord, { join: node.join, ...(onJoin === undefined ? {} : { onChange: (next) => onJoin(path, next) }), surface: surface }), node.of.length === 0 ? (
|
|
34
|
+
/* Un nodo recién abierto no tiene nada adentro, y un corchete vacío se
|
|
35
|
+
lee como un error. Dice que está esperando. */
|
|
36
|
+
_jsx("span", { className: `shrink-0 text-micro italic ${bySurface(surface, "text-muted", "text-chrome-muted")}`, children: copy.search.landsHere })) : null, node.of.map((child, at) => (_jsx(Node, { node: child, path: [...path, at], ...(onOpen === undefined ? {} : { onOpen }), ...(onJoin === undefined ? {} : { onJoin }), ...(onRemove === undefined ? {} : { onRemove }), ...(open === undefined ? {} : { open }), surface: surface }, child.id)))] }));
|
|
37
|
+
}
|
|
38
|
+
/** El contexto, con su palabra adelante. Sin la palabra, las fichas no dicen de
|
|
39
|
+
* qué son. No lleva caja: una caja adentro del campo las separa de él. */
|
|
40
|
+
function Zone({ lead, icon: Icon, surface, children, }) {
|
|
41
|
+
return (_jsxs("span", { className: "inline-flex min-w-0 shrink items-center gap-1", children: [_jsx(Icon, { className: `size-3 shrink-0 ${bySurface(surface, "text-muted", "text-chrome-muted")}`, strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: `shrink-0 text-micro ${bySurface(surface, "text-muted", "text-chrome-muted")}`, children: lead }), _jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: children })] }));
|
|
42
|
+
}
|
|
43
|
+
function Chip({ label, path, onOpen, onRemove, surface, }) {
|
|
44
|
+
const copy = useUiCopy();
|
|
45
|
+
const shell = `inline-flex min-w-0 shrink-0 items-center gap-1 rounded-pill border py-0.5 text-micro ${bySurface(surface, "border-accent/40 bg-accent/18 text-ink", "border-chrome-link/35 bg-chrome-link/18 text-chrome-link")} ${onRemove === undefined ? "px-2" : "pr-0.5 pl-2"}`;
|
|
46
|
+
const remove = onRemove === undefined ? null : (_jsx("button", { type: "button", onClick: (event) => {
|
|
47
|
+
/* Sin esto, sacarla también la abre: el click llega acá y después
|
|
48
|
+
sube. */
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
onRemove(path);
|
|
51
|
+
}, "aria-label": copy.search.removeFilter.replace("{label}", label), className: `inline-flex size-4 shrink-0 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${bySurface(surface, "hover:bg-primary/20", "hover:bg-chrome-link/25")}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) }));
|
|
52
|
+
if (onOpen === undefined) {
|
|
53
|
+
return (_jsxs("span", { className: shell, children: [_jsx("span", { className: "min-w-0 truncate", children: label }), remove] }));
|
|
54
|
+
}
|
|
55
|
+
/* Un botón adentro de otro es HTML inválido —el navegador cierra el de afuera
|
|
56
|
+
antes de tiempo y el click deja de llegar—, así que la ficha que se abre Y se
|
|
57
|
+
saca es un `span` con `role`, y la × adentro es el único botón de verdad. */
|
|
58
|
+
return (_jsxs("span", { role: "button", tabIndex: 0, onClick: () => onOpen(path), onKeyDown: (event) => {
|
|
59
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
onOpen(path);
|
|
62
|
+
}
|
|
63
|
+
}, title: copy.search.openBuilderHere, className: `${shell} cursor-pointer`, children: [_jsx("span", { className: "min-w-0 truncate", children: label }), remove] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { CriteriaJoin } from "./criteria.js";
|
|
2
|
+
import { type ModelDefinition } from "./model.js";
|
|
3
|
+
/** Una condición, tal como la declara el motor. `operator` sale del `ops` del campo, así que
|
|
4
|
+
* lo que el constructor ofrece es exactamente lo que el modelo acepta. */
|
|
5
|
+
export interface BuiltCondition {
|
|
6
|
+
field: string;
|
|
7
|
+
operator: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** Lo que el filtro puede ser en su forma real: una condición, un grupo `all`/`any` de más
|
|
11
|
+
* filtros —lo que el árbol arma, a cualquier profundidad—, o un `negate` que llegó de otro
|
|
12
|
+
* lado y que el árbol no fabrica, pero que este componente sabe mostrar y editar como JSON. */
|
|
13
|
+
export type BuiltFilter = BuiltCondition | {
|
|
14
|
+
all: BuiltFilter[];
|
|
15
|
+
} | {
|
|
16
|
+
any: BuiltFilter[];
|
|
17
|
+
} | {
|
|
18
|
+
negate: BuiltFilter;
|
|
19
|
+
};
|
|
20
|
+
/** Si esto es una de las formas que el filtro reconoce — condición, o grupo `all`/`any`/
|
|
21
|
+
* `negate`. Un JSON válido que no es ninguna de éstas —`{}`, `{"foo": 1}`, un array suelto—
|
|
22
|
+
* no es un filtro que este componente sepa dibujar, y se trata como error de forma en vez de
|
|
23
|
+
* intentar mostrar cualquier cosa. */
|
|
24
|
+
export declare function isRecognizedFilter(value: unknown): value is BuiltFilter;
|
|
25
|
+
export interface TreeCondition {
|
|
26
|
+
kind: "condition";
|
|
27
|
+
field: string;
|
|
28
|
+
operator: string;
|
|
29
|
+
value: string;
|
|
30
|
+
/** El otro extremo, para los operadores que comparan contra dos —hoy `between`—. Vacío en
|
|
31
|
+
* todos los demás, donde no significa nada. */
|
|
32
|
+
upper?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface TreeGroup {
|
|
35
|
+
kind: "group";
|
|
36
|
+
/** Todas, cualquiera, o NINGUNA. Las tres, y las mismas que la barra: negar
|
|
37
|
+
* algo se podía escribir sólo en el JSON, y una pregunta que el editor no
|
|
38
|
+
* sabe construir es una pregunta que la mayoría no va a hacer. */
|
|
39
|
+
join: CriteriaJoin;
|
|
40
|
+
children: TreeNode[];
|
|
41
|
+
}
|
|
42
|
+
export type TreeNode = TreeCondition | TreeGroup;
|
|
43
|
+
export declare function blankCondition(model: ModelDefinition, first: string | undefined): TreeCondition;
|
|
44
|
+
export declare function blankGroup(model: ModelDefinition, first: string | undefined): TreeGroup;
|
|
45
|
+
/** El árbol como el filtro que viaja.
|
|
46
|
+
*
|
|
47
|
+
* in condición sola
|
|
48
|
+
* out esa condición, sin envolver
|
|
49
|
+
*
|
|
50
|
+
* in grupo con dos hijos, join «any»
|
|
51
|
+
* out {any: [ … , … ]}
|
|
52
|
+
*
|
|
53
|
+
* Un grupo de un solo hijo no se envuelve —es ese hijo—, y un grupo sin hijos desaparece: ni
|
|
54
|
+
* «todas» ni «cualquiera» vacío es un filtro que alguien pidió. La regla vale en cualquier
|
|
55
|
+
* nivel, no sólo en la raíz, así que anidar un grupo de una condición sola es lo mismo que no
|
|
56
|
+
* haberlo anidado. */
|
|
57
|
+
export declare function treeToFilter(node: TreeNode): BuiltFilter | undefined;
|
|
58
|
+
/** El filtro leído de vuelta a un árbol.
|
|
59
|
+
*
|
|
60
|
+
* in {field: "name", operator: "=", value: "labs"}
|
|
61
|
+
* out TreeCondition
|
|
62
|
+
*
|
|
63
|
+
* in {any: [{all: [cond1, cond2]}, cond3]} grupo adentro de grupo
|
|
64
|
+
* out TreeGroup(any) con un TreeGroup(all) y una condición como hijos
|
|
65
|
+
*
|
|
66
|
+
* in {negate: {any: [cond1, cond2]}}
|
|
67
|
+
* out TreeGroup(not) con las dos condiciones como hijos
|
|
68
|
+
*
|
|
69
|
+
* El `negate` de un `any` se abre en el grupo «ninguna» y no queda como un grupo
|
|
70
|
+
* adentro de otro: es lo que `treeToFilter` vuelve a escribir, así que ida y
|
|
71
|
+
* vuelta dan lo mismo. Lo que NO se puede leer —un objeto que no es ni una
|
|
72
|
+
* condición ni ninguna de las tres formas— devuelve `null`, y ahí el componente
|
|
73
|
+
* lo muestra de sólo lectura sin dejar de poder tocarlo como JSON.
|
|
74
|
+
*/
|
|
75
|
+
export declare function treeFromFilter(filter: unknown): TreeNode | null;
|
|
76
|
+
/** El filtro dicho en una línea, para una ficha o un rótulo.
|
|
77
|
+
*
|
|
78
|
+
* in {field: "row_count", operator: ">", value: 1000}, con el modelo
|
|
79
|
+
* out 'filas mayor que 1000'
|
|
80
|
+
*
|
|
81
|
+
* in {all: [cond1, cond2]}
|
|
82
|
+
* out 'el nombre contiene labs y filas mayor que 1000'
|
|
83
|
+
*
|
|
84
|
+
* in {any: [cond1, cond2]}
|
|
85
|
+
* out 'el nombre es a o el nombre es b'
|
|
86
|
+
*
|
|
87
|
+
* Cada condición la dice `sayCondition`, que es la misma que usa la barra: sin
|
|
88
|
+
* eso, lo escrito en la barra y lo armado en el diálogo se leen distinto aunque
|
|
89
|
+
* filtren igual, y nadie sabe cuál de las dos frases es la que manda.
|
|
90
|
+
*
|
|
91
|
+
* **El modelo es el que sabe cómo se llama un campo.** Sin él queda el nombre
|
|
92
|
+
* crudo —`row_count`—, que es lo mejor que se puede decir sin preguntarle a
|
|
93
|
+
* nadie.
|
|
94
|
+
*
|
|
95
|
+
* Un grupo anidado se encierra entre paréntesis, que es lo que evita que «a y (b o c)» se
|
|
96
|
+
* lea como «(a y b) o c». Para más de tres condiciones se corta: una ficha que no entra en
|
|
97
|
+
* la barra no dice nada — para eso está abrir el constructor y verlo entero. */
|
|
98
|
+
export declare function describeFilter(node: BuiltFilter, model?: ModelDefinition): string;
|
|
99
|
+
/** El filtro dibujado tal como es, recursivo: una condición es una línea, `all`/`any` son un
|
|
100
|
+
* grupo con su rótulo y sus hijos adentro, `negate` es «no» envolviendo al que niega.
|
|
101
|
+
*
|
|
102
|
+
* Es la mitad de sólo lectura de este archivo, y también sirve sola: mostrar qué dice un
|
|
103
|
+
* filtro que ya está puesto no necesita abrir el constructor. */
|
|
104
|
+
export declare function ExpressionView({ node, model, depth, }: {
|
|
105
|
+
node: BuiltFilter;
|
|
106
|
+
/** Quién sabe cómo se llama un campo. Sin él queda el nombre crudo. */
|
|
107
|
+
model?: ModelDefinition;
|
|
108
|
+
depth?: number;
|
|
109
|
+
}): import("react").JSX.Element;
|
|
110
|
+
export declare function WhereTree({ model, group, onChange, depth, focus, }: {
|
|
111
|
+
model: ModelDefinition;
|
|
112
|
+
group: TreeGroup;
|
|
113
|
+
onChange: (next: TreeGroup) => void;
|
|
114
|
+
depth?: number;
|
|
115
|
+
/**
|
|
116
|
+
* El campo de la condición que hay que mostrar PRIMERO: la que alguien tocó en
|
|
117
|
+
* la barra.
|
|
118
|
+
*
|
|
119
|
+
* Sin esto, tocar una ficha abría el constructor en la regla uno y había que
|
|
120
|
+
* buscar entre doce la que uno venía mirando — que es la manera más rápida de
|
|
121
|
+
* editar la equivocada. Se marca y se lleva a la vista; no se abre en un modo
|
|
122
|
+
* distinto, porque el constructor es el mismo.
|
|
123
|
+
*
|
|
124
|
+
* Marca a CUALQUIER profundidad: el árbol se lo pasa a los grupos que anida.
|
|
125
|
+
*
|
|
126
|
+
* **Es un nombre de campo, no una ruta**, así que dos condiciones sobre el
|
|
127
|
+
* mismo campo en ramas distintas se marcan las dos y la vista va a la primera.
|
|
128
|
+
* Distinguirlas pide que el árbol y las instancias de la barra compartan la
|
|
129
|
+
* identidad, y hoy no la comparten: `whereOf` arma el filtro salteándose los
|
|
130
|
+
* niveles y lo que no viaja, así que la posición en uno no es la posición en
|
|
131
|
+
* el otro.
|
|
132
|
+
*/
|
|
133
|
+
focus?: string;
|
|
134
|
+
}): import("react").JSX.Element;
|
|
135
|
+
export declare function FilterBuilder({ model, initial, onApply, onCancel, }: {
|
|
136
|
+
model: ModelDefinition;
|
|
137
|
+
/** El filtro que ya estaba puesto, si había uno. Reabrir el constructor con esto adentro
|
|
138
|
+
* tiene que seguir donde quedó, no empezar en blanco. */
|
|
139
|
+
initial?: BuiltFilter;
|
|
140
|
+
onApply: (where: BuiltFilter | undefined) => void;
|
|
141
|
+
onCancel?: () => void;
|
|
142
|
+
}): import("react").JSX.Element;
|