@sincpro/web-ui 0.0.0-stage → 0.2.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 +184 -2
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/Messages.d.ts +66 -0
- package/dist/components/activity/Messages.js +46 -0
- package/dist/components/activity/Timeline.d.ts +21 -0
- package/dist/components/activity/Timeline.js +30 -0
- package/dist/components/activity/index.d.ts +3 -0
- package/dist/components/activity/index.js +4 -0
- 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/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +47 -0
- package/dist/components/form/Sheet.js +90 -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 +111 -0
- package/dist/components/lines/LineItems.js +317 -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 +112 -0
- package/dist/components/listing/ListingBar.js +144 -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 +110 -0
- package/dist/components/listing/Row.js +123 -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 +75 -0
- package/dist/components/record/RecordHeader.js +104 -0
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -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 +703 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +430 -0
- package/dist/foundations/copy.js +520 -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 +19 -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 +33 -0
- package/dist/primitives/choice/SearchField.js +46 -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 +134 -0
- package/dist/primitives/date/DatePicker.d.ts +42 -0
- package/dist/primitives/date/DatePicker.js +103 -0
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +3 -0
- package/dist/primitives/date/index.js +6 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +2 -0
- package/dist/primitives/dialog/index.js +4 -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 +178 -0
- package/dist/primitives/field/Field.js +251 -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/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +9 -0
- package/dist/primitives/field/index.js +15 -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/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -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 +20 -0
- package/dist/primitives/index.js +21 -0
- package/dist/primitives/menu/Menu.d.ts +128 -0
- package/dist/primitives/menu/Menu.js +89 -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/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -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 +4 -0
- package/dist/primitives/notice/index.js +6 -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/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -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 +4 -0
- package/dist/primitives/overlay/index.js +7 -0
- package/dist/primitives/picker/Picker.d.ts +89 -0
- package/dist/primitives/picker/Picker.js +204 -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/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -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 +189 -0
- package/dist/primitives/table/DataTable.js +330 -0
- package/dist/primitives/table/EditableTable.d.ts +191 -0
- package/dist/primitives/table/EditableTable.js +617 -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 +73 -0
- package/dist/primitives/table/SheetTable.js +230 -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 +35 -0
- package/dist/primitives/tag/TagsField.js +186 -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/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Copyable.d.ts +48 -0
- package/dist/primitives/value/Copyable.js +71 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +89 -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/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +74 -0
- package/dist/primitives/value/Relation.js +74 -0
- package/dist/primitives/value/Totals.d.ts +121 -0
- package/dist/primitives/value/Totals.js +54 -0
- package/dist/primitives/value/Value.d.ts +40 -0
- package/dist/primitives/value/Value.js +80 -0
- package/dist/primitives/value/index.d.ts +9 -0
- package/dist/primitives/value/index.js +10 -0
- package/dist/tokens.css +1062 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +119 -0
- package/dist/views/list/List.js +101 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +295 -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 +87 -0
- package/dist/views/list/derive.js +207 -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 +105 -0
- package/dist/views/list/types.d.ts +284 -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 +145 -0
- package/dist/views/record/Record.js +211 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +113 -4
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { JoinWord } from "./JoinWord.js";
|
|
4
|
+
import { JsonEditor } from "./JsonEditor.js";
|
|
5
|
+
import { fieldLabel, filterableNames, } from "./model.js";
|
|
6
|
+
import { opLabel, sayCondition } from "./query.js";
|
|
7
|
+
import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
8
|
+
import { useSurface } from "../../foundations/surface.js";
|
|
9
|
+
import { Button } from "../../primitives/button/index.js";
|
|
10
|
+
import { FolderTree, Plus, Trash2 } from "lucide-react";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
/* ====================================================================== *
|
|
13
|
+
* UN FILTRO QUE ALGUIEN ARMA, DE LO QUE EL MODELO DICE QUE ACEPTA.
|
|
14
|
+
*
|
|
15
|
+
* Los recortes con nombre de al lado —«vencidas», «con entregas pendientes»— son las
|
|
16
|
+
* preguntas que alguien ya pensó. Esto es para las demás: elegir un campo, un
|
|
17
|
+
* operador, escribir un valor — agruparlos con Y o con O, anidar un grupo
|
|
18
|
+
* adentro de otro tantas veces como haga falta, o escribir directamente el
|
|
19
|
+
* JSON para copiar o pegar una criteria entera.
|
|
20
|
+
*
|
|
21
|
+
* **Nada acá está escrito a mano.** Los campos salen de la definición del
|
|
22
|
+
* modelo que manda el servidor, y también los operadores de cada uno: un
|
|
23
|
+
* campo de texto ofrece «contiene», uno numérico ofrece «>», uno que admite
|
|
24
|
+
* vacío ofrece «está vacío». Agregar una columna la hace aparecer acá, con
|
|
25
|
+
* sus operadores correctos, sin tocar este archivo.
|
|
26
|
+
*
|
|
27
|
+
* **Un árbol, no una lista.** Cada grupo —la raíz incluida— tiene su propio
|
|
28
|
+
* «todas / cualquiera» y sus propios hijos: una condición, o otro grupo
|
|
29
|
+
* anidado adentro. El ícono del árbol agrega un grupo nuevo al lado de la
|
|
30
|
+
* fila que se tocó; el `+` agrega otra condición.
|
|
31
|
+
*
|
|
32
|
+
* **Un solo componente renderiza y construye — y siempre hay un JSON al
|
|
33
|
+
* lado.** El árbol y el editor de código son dos vistas de la MISMA
|
|
34
|
+
* criteria, sincronizadas en las dos direcciones: tocar el árbol reescribe
|
|
35
|
+
* el JSON, y escribir un JSON reconocible rehace el árbol. Lo único que el
|
|
36
|
+
* árbol no sabe construir ni mostrar es un NOT: si el filtro trae uno en
|
|
37
|
+
* cualquier nivel, se dibuja de sólo lectura con `ExpressionView` en su
|
|
38
|
+
* lugar, y el JSON sigue siendo la manera de tocarlo.
|
|
39
|
+
* ====================================================================== */
|
|
40
|
+
/* Cómo se llama un operador para quien lo lee: `copy.ops`, que es la MISMA tabla que usa la
|
|
41
|
+
barra al armar la pregunta. Dos tablas es que la misma comparación se llame de dos maneras
|
|
42
|
+
en la misma pantalla. */
|
|
43
|
+
/** Qué operadores no llevan valor: preguntar si algo está vacío no necesita escribir nada. */
|
|
44
|
+
const WITHOUT_VALUE = new Set(["is null"]);
|
|
45
|
+
/** Qué operadores comparan contra DOS valores. «Entre el 1 y el 31» es la pregunta que todo
|
|
46
|
+
* el mundo hace sobre fechas, y escribirla como dos reglas sueltas es la forma de
|
|
47
|
+
* equivocarse con uno de los dos extremos. */
|
|
48
|
+
const WITH_RANGE = new Set(["between"]);
|
|
49
|
+
/** Qué clase de input pide un valor, leído del tipo del campo. */
|
|
50
|
+
function inputType(type) {
|
|
51
|
+
if (type === "integer" || type === "number")
|
|
52
|
+
return "number";
|
|
53
|
+
if (type === "date")
|
|
54
|
+
return "date";
|
|
55
|
+
if (type === "datetime")
|
|
56
|
+
return "datetime-local";
|
|
57
|
+
return "text";
|
|
58
|
+
}
|
|
59
|
+
function isCondition(value) {
|
|
60
|
+
return (typeof value === "object" &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
!Array.isArray(value) &&
|
|
63
|
+
typeof value.field === "string" &&
|
|
64
|
+
typeof value.operator === "string" &&
|
|
65
|
+
"value" in value);
|
|
66
|
+
}
|
|
67
|
+
/** Si esto es una de las formas que el filtro reconoce — condición, o grupo `all`/`any`/
|
|
68
|
+
* `negate`. Un JSON válido que no es ninguna de éstas —`{}`, `{"foo": 1}`, un array suelto—
|
|
69
|
+
* no es un filtro que este componente sepa dibujar, y se trata como error de forma en vez de
|
|
70
|
+
* intentar mostrar cualquier cosa. */
|
|
71
|
+
export function isRecognizedFilter(value) {
|
|
72
|
+
if (isCondition(value))
|
|
73
|
+
return true;
|
|
74
|
+
if (typeof value !== "object" || value === null)
|
|
75
|
+
return false;
|
|
76
|
+
const record = value;
|
|
77
|
+
if (Array.isArray(record.all))
|
|
78
|
+
return true;
|
|
79
|
+
if (Array.isArray(record.any))
|
|
80
|
+
return true;
|
|
81
|
+
if ("negate" in record)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
export function blankCondition(model, first) {
|
|
86
|
+
const field = first ?? "";
|
|
87
|
+
return {
|
|
88
|
+
kind: "condition",
|
|
89
|
+
field,
|
|
90
|
+
operator: model.fields[field]?.ops[0] ?? "=",
|
|
91
|
+
value: "",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function blankGroup(model, first) {
|
|
95
|
+
return { kind: "group", join: "all", children: [blankCondition(model, first)] };
|
|
96
|
+
}
|
|
97
|
+
/** El árbol como el filtro que viaja.
|
|
98
|
+
*
|
|
99
|
+
* in condición sola
|
|
100
|
+
* out esa condición, sin envolver
|
|
101
|
+
*
|
|
102
|
+
* in grupo con dos hijos, join «any»
|
|
103
|
+
* out {any: [ … , … ]}
|
|
104
|
+
*
|
|
105
|
+
* Un grupo de un solo hijo no se envuelve —es ese hijo—, y un grupo sin hijos desaparece: ni
|
|
106
|
+
* «todas» ni «cualquiera» vacío es un filtro que alguien pidió. La regla vale en cualquier
|
|
107
|
+
* nivel, no sólo en la raíz, así que anidar un grupo de una condición sola es lo mismo que no
|
|
108
|
+
* haberlo anidado. */
|
|
109
|
+
export function treeToFilter(node) {
|
|
110
|
+
if (node.kind === "condition") {
|
|
111
|
+
if (node.field === "")
|
|
112
|
+
return undefined;
|
|
113
|
+
if (WITHOUT_VALUE.has(node.operator)) {
|
|
114
|
+
return { field: node.field, operator: node.operator, value: true };
|
|
115
|
+
}
|
|
116
|
+
if (WITH_RANGE.has(node.operator)) {
|
|
117
|
+
/* Un rango a medio escribir no es medio rango: mientras falte un extremo, la regla no
|
|
118
|
+
viaja. */
|
|
119
|
+
if (node.value === "" || (node.upper ?? "") === "")
|
|
120
|
+
return undefined;
|
|
121
|
+
return { field: node.field, operator: node.operator, value: [node.value, node.upper] };
|
|
122
|
+
}
|
|
123
|
+
/* **Una regla sin valor no filtra.** Viajaba con `value: ""`, y eso no es «todavía no
|
|
124
|
+
escribí nada»: contra un `=` es «el campo es la cadena vacía», que vacía la lista
|
|
125
|
+
mientras alguien sigue tipeando, y contra un `like` es «contiene nada», que la deja
|
|
126
|
+
igual. Para preguntar por lo que está vacío está el operador «está vacío», que el
|
|
127
|
+
modelo publica en los campos que lo admiten — explícito, y no por omisión. */
|
|
128
|
+
if (node.value === "")
|
|
129
|
+
return undefined;
|
|
130
|
+
return { field: node.field, operator: node.operator, value: node.value };
|
|
131
|
+
}
|
|
132
|
+
const kept = node.children
|
|
133
|
+
.map(treeToFilter)
|
|
134
|
+
.filter((one) => one !== undefined);
|
|
135
|
+
if (kept.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
/* «Ninguna» niega el grupo ENTERO y no cada hijo por su cuenta: «ni vencida ni
|
|
138
|
+
grande» es `no (vencida o grande)`. Y un grupo de uno tampoco se desarma
|
|
139
|
+
acá —desarmarlo perdería la negación—, que es lo mismo que hace la barra. */
|
|
140
|
+
if (node.join === "not") {
|
|
141
|
+
return { negate: kept.length === 1 ? kept[0] : { any: kept } };
|
|
142
|
+
}
|
|
143
|
+
if (kept.length === 1)
|
|
144
|
+
return kept[0];
|
|
145
|
+
return node.join === "any" ? { any: kept } : { all: kept };
|
|
146
|
+
}
|
|
147
|
+
/** Una condición de vuelta a una fila editable: el valor vuelve a texto, que es lo único que
|
|
148
|
+
* un `<input>` sabe mostrar. Una lista —el valor de un `in`— se escribe separada por comas. */
|
|
149
|
+
function conditionToTree(condition) {
|
|
150
|
+
if (WITH_RANGE.has(condition.operator) && Array.isArray(condition.value)) {
|
|
151
|
+
const [from, to] = condition.value;
|
|
152
|
+
return {
|
|
153
|
+
kind: "condition",
|
|
154
|
+
field: condition.field,
|
|
155
|
+
operator: condition.operator,
|
|
156
|
+
value: String(from ?? ""),
|
|
157
|
+
upper: String(to ?? ""),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
kind: "condition",
|
|
162
|
+
field: condition.field,
|
|
163
|
+
operator: condition.operator,
|
|
164
|
+
value: WITHOUT_VALUE.has(condition.operator)
|
|
165
|
+
? ""
|
|
166
|
+
: Array.isArray(condition.value)
|
|
167
|
+
? condition.value.join(",")
|
|
168
|
+
: String(condition.value ?? ""),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** El filtro leído de vuelta a un árbol.
|
|
172
|
+
*
|
|
173
|
+
* in {field: "name", operator: "=", value: "labs"}
|
|
174
|
+
* out TreeCondition
|
|
175
|
+
*
|
|
176
|
+
* in {any: [{all: [cond1, cond2]}, cond3]} grupo adentro de grupo
|
|
177
|
+
* out TreeGroup(any) con un TreeGroup(all) y una condición como hijos
|
|
178
|
+
*
|
|
179
|
+
* in {negate: {any: [cond1, cond2]}}
|
|
180
|
+
* out TreeGroup(not) con las dos condiciones como hijos
|
|
181
|
+
*
|
|
182
|
+
* El `negate` de un `any` se abre en el grupo «ninguna» y no queda como un grupo
|
|
183
|
+
* adentro de otro: es lo que `treeToFilter` vuelve a escribir, así que ida y
|
|
184
|
+
* vuelta dan lo mismo. Lo que NO se puede leer —un objeto que no es ni una
|
|
185
|
+
* condición ni ninguna de las tres formas— devuelve `null`, y ahí el componente
|
|
186
|
+
* lo muestra de sólo lectura sin dejar de poder tocarlo como JSON.
|
|
187
|
+
*/
|
|
188
|
+
export function treeFromFilter(filter) {
|
|
189
|
+
if (isCondition(filter))
|
|
190
|
+
return conditionToTree(filter);
|
|
191
|
+
if (typeof filter !== "object" || filter === null)
|
|
192
|
+
return null;
|
|
193
|
+
const record = filter;
|
|
194
|
+
if ("negate" in record) {
|
|
195
|
+
const inside = record.negate;
|
|
196
|
+
/* Un `negate` de un `any` son los hijos del grupo negado; cualquier otra cosa
|
|
197
|
+
es un solo hijo. */
|
|
198
|
+
const raws = typeof inside === "object" &&
|
|
199
|
+
inside !== null &&
|
|
200
|
+
Array.isArray(inside.any)
|
|
201
|
+
? inside.any
|
|
202
|
+
: [inside];
|
|
203
|
+
const children = readChildren(raws);
|
|
204
|
+
return children === null ? null : { kind: "group", join: "not", children };
|
|
205
|
+
}
|
|
206
|
+
const join = Array.isArray(record.all)
|
|
207
|
+
? "all"
|
|
208
|
+
: Array.isArray(record.any)
|
|
209
|
+
? "any"
|
|
210
|
+
: null;
|
|
211
|
+
if (join === null)
|
|
212
|
+
return null;
|
|
213
|
+
const children = readChildren(record[join]);
|
|
214
|
+
return children === null ? null : { kind: "group", join, children };
|
|
215
|
+
}
|
|
216
|
+
/** Los hijos de un grupo, o `null` si alguno no se puede leer: medio árbol es
|
|
217
|
+
* peor que ninguno, porque dibuja algo que no es lo que filtra. */
|
|
218
|
+
function readChildren(raws) {
|
|
219
|
+
const children = [];
|
|
220
|
+
for (const raw of raws) {
|
|
221
|
+
const child = treeFromFilter(raw);
|
|
222
|
+
if (child === null)
|
|
223
|
+
return null;
|
|
224
|
+
children.push(child);
|
|
225
|
+
}
|
|
226
|
+
return children;
|
|
227
|
+
}
|
|
228
|
+
function stringifyFilter(filter) {
|
|
229
|
+
return filter === undefined ? "" : JSON.stringify(filter, null, 2);
|
|
230
|
+
}
|
|
231
|
+
/** Un valor de condición, legible: una lista se junta con comas, y lo que no tiene nada se
|
|
232
|
+
* dice en palabras en vez de mostrar `true`/`undefined` sueltos. */
|
|
233
|
+
function describeValue(value, operator) {
|
|
234
|
+
const copy = currentCopy();
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return operator !== undefined && WITH_RANGE.has(operator)
|
|
237
|
+
? value.join(` ${copy.search.and} `)
|
|
238
|
+
: value.join(", ");
|
|
239
|
+
}
|
|
240
|
+
/* Las mismas dos palabras que la tabla pone en una celda booleana: un `sí` que
|
|
241
|
+
no es el `sí` de al lado es una segunda manera de decir lo mismo. */
|
|
242
|
+
if (typeof value === "boolean")
|
|
243
|
+
return value ? copy.table.yes : copy.table.no;
|
|
244
|
+
return String(value ?? "");
|
|
245
|
+
}
|
|
246
|
+
/** El filtro dicho en una línea, para una ficha o un rótulo.
|
|
247
|
+
*
|
|
248
|
+
* in {field: "row_count", operator: ">", value: 1000}, con el modelo
|
|
249
|
+
* out 'filas mayor que 1000'
|
|
250
|
+
*
|
|
251
|
+
* in {all: [cond1, cond2]}
|
|
252
|
+
* out 'el nombre contiene labs y filas mayor que 1000'
|
|
253
|
+
*
|
|
254
|
+
* in {any: [cond1, cond2]}
|
|
255
|
+
* out 'el nombre es a o el nombre es b'
|
|
256
|
+
*
|
|
257
|
+
* Cada condición la dice `sayCondition`, que es la misma que usa la barra: sin
|
|
258
|
+
* eso, lo escrito en la barra y lo armado en el diálogo se leen distinto aunque
|
|
259
|
+
* filtren igual, y nadie sabe cuál de las dos frases es la que manda.
|
|
260
|
+
*
|
|
261
|
+
* **El modelo es el que sabe cómo se llama un campo.** Sin él queda el nombre
|
|
262
|
+
* crudo —`row_count`—, que es lo mejor que se puede decir sin preguntarle a
|
|
263
|
+
* nadie.
|
|
264
|
+
*
|
|
265
|
+
* Un grupo anidado se encierra entre paréntesis, que es lo que evita que «a y (b o c)» se
|
|
266
|
+
* lea como «(a y b) o c». Para más de tres condiciones se corta: una ficha que no entra en
|
|
267
|
+
* la barra no dice nada — para eso está abrir el constructor y verlo entero. */
|
|
268
|
+
export function describeFilter(node, model) {
|
|
269
|
+
return say(node, model, 0);
|
|
270
|
+
}
|
|
271
|
+
function say(node, model, depth) {
|
|
272
|
+
const copy = currentCopy();
|
|
273
|
+
if ("all" in node || "any" in node) {
|
|
274
|
+
const items = "all" in node ? node.all : node.any;
|
|
275
|
+
const glue = "all" in node ? copy.search.and : copy.search.or;
|
|
276
|
+
const said = items.slice(0, 3).map((child) => say(child, model, depth + 1));
|
|
277
|
+
if (items.length > 3) {
|
|
278
|
+
said.push(fillCopy(copy.search.andMore, { count: items.length - 3 }));
|
|
279
|
+
}
|
|
280
|
+
const joined = said.join(` ${glue} `);
|
|
281
|
+
return depth === 0 || said.length === 1 ? joined : `(${joined})`;
|
|
282
|
+
}
|
|
283
|
+
if ("negate" in node)
|
|
284
|
+
return `${copy.search.notWord} ${say(node.negate, model, depth + 1)}`;
|
|
285
|
+
const label = model === undefined ? node.field : fieldLabel(model, node.field, copy.locale);
|
|
286
|
+
return sayCondition(label, node.operator, describeValue(node.value, node.operator));
|
|
287
|
+
}
|
|
288
|
+
/** El filtro dibujado tal como es, recursivo: una condición es una línea, `all`/`any` son un
|
|
289
|
+
* grupo con su rótulo y sus hijos adentro, `negate` es «no» envolviendo al que niega.
|
|
290
|
+
*
|
|
291
|
+
* Es la mitad de sólo lectura de este archivo, y también sirve sola: mostrar qué dice un
|
|
292
|
+
* filtro que ya está puesto no necesita abrir el constructor. */
|
|
293
|
+
export function ExpressionView({ node, model, depth = 0, }) {
|
|
294
|
+
const surface = useSurface();
|
|
295
|
+
const copy = useUiCopy();
|
|
296
|
+
const box = surface === "chrome" ? "border-chrome-ink/20" : "border-ink/15";
|
|
297
|
+
const indent = depth === 0 ? "" : "ml-4";
|
|
298
|
+
if ("all" in node || "any" in node) {
|
|
299
|
+
/* LAS MISMAS TRES PALABRAS QUE LA BARRA. Un grupo que acá se llama «coincide
|
|
300
|
+
con alguna» y en la ficha «cualquiera» son dos vocabularios para una sola
|
|
301
|
+
cosa, y el que los lee tiene que aprender los dos. */
|
|
302
|
+
const label = "all" in node ? copy.search.joins.all : copy.search.joins.any;
|
|
303
|
+
const items = "all" in node ? node.all : node.any;
|
|
304
|
+
return (_jsxs("div", { className: `flex flex-col gap-1 border-l-2 pl-2 ${box} ${indent}`, children: [_jsx("p", { className: "text-micro text-muted", children: label }), items.map((child, index) => (_jsx(ExpressionView, { node: child, ...(model === undefined ? {} : { model }), depth: depth + 1 }, index)))] }));
|
|
305
|
+
}
|
|
306
|
+
if ("negate" in node) {
|
|
307
|
+
return (_jsxs("div", { className: `flex flex-col gap-1 border-l-2 pl-2 ${box} ${indent}`, children: [_jsx("p", { className: "text-micro text-muted", children: copy.search.joins.none }), _jsx(ExpressionView, { node: node.negate, ...(model === undefined ? {} : { model }), depth: depth + 1 })] }));
|
|
308
|
+
}
|
|
309
|
+
/* Y la misma frase que la ficha: una condición se lee igual esté donde esté. */
|
|
310
|
+
return _jsx("p", { className: `text-meta ${indent}`, children: describeFilter(node, model) });
|
|
311
|
+
}
|
|
312
|
+
/* ---------------------------------------------------------------------- *
|
|
313
|
+
* UN GRUPO, DIBUJADO — la pieza que se llama a sí misma para anidar.
|
|
314
|
+
* ---------------------------------------------------------------------- */
|
|
315
|
+
export function WhereTree({ model, group, onChange, depth = 0, focus, }) {
|
|
316
|
+
const names = filterableNames(model);
|
|
317
|
+
const copy = useUiCopy();
|
|
318
|
+
const surface = useSurface();
|
|
319
|
+
const box = surface === "chrome"
|
|
320
|
+
? "border-chrome-ink/20 bg-chrome text-chrome-ink"
|
|
321
|
+
: "border-ink/15 bg-surface text-ink";
|
|
322
|
+
function setChild(index, next) {
|
|
323
|
+
onChange({
|
|
324
|
+
...group,
|
|
325
|
+
children: group.children.map((c, at) => (at === index ? next : c)),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function addConditionAfter(index) {
|
|
329
|
+
onChange({
|
|
330
|
+
...group,
|
|
331
|
+
children: [
|
|
332
|
+
...group.children.slice(0, index + 1),
|
|
333
|
+
blankCondition(model, names[0]),
|
|
334
|
+
...group.children.slice(index + 1),
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function addGroupAfter(index) {
|
|
339
|
+
onChange({
|
|
340
|
+
...group,
|
|
341
|
+
children: [
|
|
342
|
+
...group.children.slice(0, index + 1),
|
|
343
|
+
blankGroup(model, names[0]),
|
|
344
|
+
...group.children.slice(index + 1),
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function removeAt(index) {
|
|
349
|
+
onChange({ ...group, children: group.children.filter((_, at) => at !== index) });
|
|
350
|
+
}
|
|
351
|
+
return (_jsxs("div", { className: `flex min-w-0 flex-col gap-2 rounded-control border p-2 ${box}`, children: [_jsxs("p", { className: "flex flex-wrap items-baseline gap-1 text-meta", children: [copy.search.askJoin, _jsx(JoinWord, { join: group.join, onChange: (join) => onChange({ ...group, join }) })] }), group.children.map((child, index) => child.kind === "group" ? (_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(WhereTree, { model: model, group: child, onChange: (next) => setChild(index, next), depth: depth + 1, ...(focus === undefined ? {} : { focus }) }) }), _jsx("button", { type: "button", "aria-label": copy.builder.dropGroup, disabled: group.children.length === 1, onClick: () => removeAt(index), className: "mt-2 text-muted disabled:opacity-30", children: _jsx(Trash2, { className: "size-4" }) })] }, index)) : (_jsxs("div", {
|
|
352
|
+
/* La que se vino a ver se marca y se trae a la vista. El anillo y no
|
|
353
|
+
un fondo: el fondo compite con el `select` que hay que tocar. */
|
|
354
|
+
ref: focus !== undefined && child.field === focus
|
|
355
|
+
? (node) => node?.scrollIntoView({ block: "nearest" })
|
|
356
|
+
: undefined, className: `flex min-w-0 flex-wrap items-center gap-2 rounded-control ${focus !== undefined && child.field === focus
|
|
357
|
+
? "-m-1 p-1 ring-1 ring-primary/60"
|
|
358
|
+
: ""}`, children: [_jsx("select", { value: child.field, onChange: (event) => setChild(index, {
|
|
359
|
+
...child,
|
|
360
|
+
field: event.target.value,
|
|
361
|
+
operator: model.fields[event.target.value]?.ops.includes(child.operator)
|
|
362
|
+
? child.operator
|
|
363
|
+
: (model.fields[event.target.value]?.ops[0] ?? "="),
|
|
364
|
+
}), className: `min-w-0 flex-1 basis-40 rounded-control border px-2 py-1 text-meta ${box}`, children: names.map((name) => (_jsx("option", { value: name, children: name }, name))) }), _jsx("select", { value: child.operator, onChange: (event) => setChild(index, { ...child, operator: event.target.value }), className: `min-w-0 flex-1 basis-36 rounded-control border px-2 py-1 text-meta ${box}`, children: (model.fields[child.field]?.ops ?? []).map((operator) => (_jsx("option", { value: operator, children: opLabel(operator) }, operator))) }), WITHOUT_VALUE.has(child.operator) ? (_jsx("span", { className: "flex-1 text-meta text-muted", children: copy.builder.noValue })) : WITH_RANGE.has(child.operator) ? (_jsxs("span", { className: "flex min-w-0 flex-1 basis-40 items-center gap-1", children: [_jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.value, onChange: (event) => setChild(index, { ...child, value: event.target.value }), placeholder: copy.builder.from, className: `min-w-0 flex-1 rounded-control border px-2 py-1 text-meta ${box}` }), _jsx("span", { className: "shrink-0 text-micro text-muted", children: copy.search.and }), _jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.upper ?? "", onChange: (event) => setChild(index, { ...child, upper: event.target.value }), placeholder: copy.builder.to, className: `min-w-0 flex-1 rounded-control border px-2 py-1 text-meta ${box}` })] })) : (_jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.value, onChange: (event) => setChild(index, { ...child, value: event.target.value }), placeholder: copy.builder.value, className: `min-w-0 flex-1 basis-40 rounded-control border px-2 py-1 text-meta ${box}` })), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": copy.builder.addRule, onClick: () => addConditionAfter(index), className: "text-muted hover:text-ink", children: _jsx(Plus, { className: "size-4" }) }), _jsx("button", { type: "button", "aria-label": copy.builder.addNested, onClick: () => addGroupAfter(index), className: "text-muted hover:text-ink", children: _jsx(FolderTree, { className: "size-4" }) }), _jsx("button", { type: "button", "aria-label": copy.builder.dropRule, disabled: group.children.length === 1, onClick: () => removeAt(index), className: "text-muted disabled:opacity-30", children: _jsx(Trash2, { className: "size-4" }) })] })] }, index)))] }));
|
|
365
|
+
}
|
|
366
|
+
export function FilterBuilder({ model, initial, onApply, onCancel, }) {
|
|
367
|
+
const names = filterableNames(model);
|
|
368
|
+
const copy = useUiCopy();
|
|
369
|
+
const decomposed = initial === undefined ? null : treeFromFilter(initial);
|
|
370
|
+
const [tree, setTree] = useState(() => decomposed !== null && decomposed.kind === "group"
|
|
371
|
+
? decomposed
|
|
372
|
+
: decomposed !== null
|
|
373
|
+
? { kind: "group", join: "all", children: [decomposed] }
|
|
374
|
+
: blankGroup(model, names[0]));
|
|
375
|
+
const [lockedView, setLockedView] = useState(initial !== undefined && decomposed === null ? initial : null);
|
|
376
|
+
const [rawText, setRawText] = useState(() => stringifyFilter(initial ?? treeToFilter(tree)));
|
|
377
|
+
const [rawError, setRawError] = useState(null);
|
|
378
|
+
/* Tocar el árbol reescribe el JSON para que muestre exactamente lo que el árbol ahora dice,
|
|
379
|
+
y suelta cualquier vista de sólo lectura que hubiera quedado. */
|
|
380
|
+
function updateTree(next) {
|
|
381
|
+
setTree(next);
|
|
382
|
+
setLockedView(null);
|
|
383
|
+
setRawText(stringifyFilter(treeToFilter(next)));
|
|
384
|
+
setRawError(null);
|
|
385
|
+
}
|
|
386
|
+
/* Escribir en el JSON intenta lo mismo al revés: si lo tipeado no trae un `negate` en
|
|
387
|
+
ningún nivel, el árbol se rehace para mostrarlo — Y y O, a cualquier profundidad. Si trae
|
|
388
|
+
uno, se dibuja de sólo lectura, pero el JSON sigue siendo lo que se va a aplicar. Si no
|
|
389
|
+
es ninguna de las dos —a medio escribir, o no es un filtro— no se toca lo que ya había. */
|
|
390
|
+
function updateRaw(text) {
|
|
391
|
+
setRawText(text);
|
|
392
|
+
if (text.trim() === "") {
|
|
393
|
+
setRawError(null);
|
|
394
|
+
setLockedView(null);
|
|
395
|
+
setTree(blankGroup(model, names[0]));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
let parsed;
|
|
399
|
+
try {
|
|
400
|
+
parsed = JSON.parse(text);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
setRawError(currentCopy().builder.invalidJson);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (!isRecognizedFilter(parsed)) {
|
|
407
|
+
setRawError(currentCopy().builder.notFilter);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
setRawError(null);
|
|
411
|
+
const node = treeFromFilter(parsed);
|
|
412
|
+
if (node !== null) {
|
|
413
|
+
setTree(node.kind === "group" ? node : { kind: "group", join: "all", children: [node] });
|
|
414
|
+
setLockedView(null);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
setLockedView(parsed);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 p-3", children: [_jsx("div", { className: "max-h-80 min-w-0 overflow-auto", children: lockedView !== null ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-meta text-muted", children: copy.search.unreadable }), _jsx(ExpressionView, { node: lockedView, model: model })] })) : (_jsx(WhereTree, { model: model, group: tree, onChange: updateTree })) }), _jsx(JsonEditor, { value: rawText, onChange: updateRaw, error: rawError, label: copy.builder.jsonFilter }), _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.add }), onCancel === undefined ? null : (_jsx(Button, { role: "ghost", onClick: onCancel, children: copy.builder.cancel }))] })] }));
|
|
421
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CriteriaJoin } from "./criteria.js";
|
|
2
|
+
import type { Surface } from "../../foundations/types.js";
|
|
3
|
+
export declare function JoinWord({ join, onChange, surface, }: {
|
|
4
|
+
join: CriteriaJoin;
|
|
5
|
+
/** Sin esto la palabra es un valor: dice cómo junta, y no se toca. */
|
|
6
|
+
onChange?: (next: CriteriaJoin) => void;
|
|
7
|
+
surface?: Surface;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { bySurface, useSurface } from "../../foundations/surface.js";
|
|
5
|
+
import { Tags } from "../../primitives/tag/Tags.js";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
/* ==================================================================== *
|
|
8
|
+
* CÓMO SE JUNTA UN NODO, en una palabra que se toca.
|
|
9
|
+
*
|
|
10
|
+
* Todas, cualquiera, ninguna. Son las tres y no dos: sin «ninguna», negar algo
|
|
11
|
+
* sólo se podía escribir en el JSON, y una pregunta que el editor no sabe
|
|
12
|
+
* construir es una pregunta que la mayoría no va a hacer.
|
|
13
|
+
*
|
|
14
|
+
* ── Una palabra, no un símbolo ──────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* «Y» y «O» son de quien escribe consultas; «todas» y «cualquiera» son de quien
|
|
17
|
+
* busca. Y son las de la copia, así que cambiarlas las cambia en los dos lados
|
|
18
|
+
* a la vez.
|
|
19
|
+
*
|
|
20
|
+
* ── Se abre en sus opciones, no rota ────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* Con tres, un botón que alterna no dice cuáles hay ni cuál está puesta: hay
|
|
23
|
+
* que apretarlo para averiguarlo, y averiguarlo cambia lo que filtra.
|
|
24
|
+
*
|
|
25
|
+
* Vive acá y no adentro de la barra porque el constructor pregunta lo mismo. Un
|
|
26
|
+
* `<select>` allá y una lista acá son dos controles para una decisión, y el que
|
|
27
|
+
* aprendió uno no reconoce el otro.
|
|
28
|
+
* ==================================================================== */
|
|
29
|
+
const OPTIONS = ["all", "any", "not"];
|
|
30
|
+
export function JoinWord({ join, onChange, surface, }) {
|
|
31
|
+
const sup = useSurface(surface);
|
|
32
|
+
const copy = useUiCopy();
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const word = copy.search.joins[join === "not" ? "none" : join];
|
|
35
|
+
if (onChange === undefined) {
|
|
36
|
+
return _jsx("span", { className: "shrink-0 text-micro text-muted", children: word });
|
|
37
|
+
}
|
|
38
|
+
return (_jsxs("span", { className: "relative inline-flex shrink-0", children: [_jsx("button", { type: "button", onClick: () => setOpen((current) => !current), "aria-expanded": open, title: copy.search.switchJoin, className: `shrink-0 rounded-control px-1 text-micro font-medium underline decoration-dotted underline-offset-4 decoration-current/40 transition-colors hover:decoration-current focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${bySurface(sup, "text-muted hover:text-ink", "text-chrome-muted hover:text-chrome-ink")}`, children: word }), open ? (_jsx("span", { className: "absolute top-full left-0 z-40 mt-1 rounded-panel border border-line-strong bg-surface p-1.5 shadow-float", children: _jsx(Tags, { stack: true, surface: "canvas", label: copy.search.switchJoin, items: OPTIONS.map((one) => ({
|
|
39
|
+
id: one,
|
|
40
|
+
label: copy.search.joins[one === "not" ? "none" : one],
|
|
41
|
+
selected: one === join,
|
|
42
|
+
onSelect: () => {
|
|
43
|
+
onChange(one);
|
|
44
|
+
setOpen(false);
|
|
45
|
+
},
|
|
46
|
+
})) }) })) : null] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Un JSON acomodado, o el mismo texto si todavía no se puede leer.
|
|
2
|
+
*
|
|
3
|
+
* in '{"all":[{"field":"a","operator":"=","value":1}]}'
|
|
4
|
+
* out el mismo, con sangría de dos espacios y un campo por línea
|
|
5
|
+
*
|
|
6
|
+
* Separado del componente para que se pueda probar sin dibujar nada, que es lo único que
|
|
7
|
+
* tiene lógica acá adentro. */
|
|
8
|
+
export declare function formatJson(text: string): string;
|
|
9
|
+
export declare function JsonEditor({ value, onChange, error, height, label, }: {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (next: string) => void;
|
|
12
|
+
/** Qué está mal con lo escrito más allá de la sintaxis — que sea JSON válido pero no un
|
|
13
|
+
* filtro, por ejemplo. Los errores de sintaxis los marca el editor, en su lugar exacto. */
|
|
14
|
+
error?: string | null;
|
|
15
|
+
height?: string;
|
|
16
|
+
/** Cómo se llama este editor. Sin esto dice sólo que es un editor de código,
|
|
17
|
+
* que es lo único que se puede decir sin saber qué se escribe adentro. */
|
|
18
|
+
label?: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
|
4
|
+
import { linter, lintGutter } from "@codemirror/lint";
|
|
5
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { useSurface } from "../../foundations/surface.js";
|
|
7
|
+
import { useTheme } from "../../foundations/theme.js";
|
|
8
|
+
import { Button } from "../../primitives/button/index.js";
|
|
9
|
+
import CodeMirror, { EditorView, Prec } from "@uiw/react-codemirror";
|
|
10
|
+
import { Wand2 } from "lucide-react";
|
|
11
|
+
/* ====================================================================== *
|
|
12
|
+
* UN JSON QUE SE ESCRIBE A MANO, EN UN EDITOR DE VERDAD.
|
|
13
|
+
*
|
|
14
|
+
* CodeMirror y no un `<textarea>` con colores encima: eso último llegó hasta
|
|
15
|
+
* donde llegan cincuenta líneas —pintar las comillas— y se quedaba corto en
|
|
16
|
+
* todo lo demás, que es lo que hace usable escribir un filtro a mano:
|
|
17
|
+
* números de línea para saber dónde está el error, plegado para no perderse
|
|
18
|
+
* en un `all` de seis condiciones, y el subrayado en el carácter exacto
|
|
19
|
+
* donde el JSON se rompe en vez de un cartel que dice que algo está mal.
|
|
20
|
+
*
|
|
21
|
+
* El `linter` de abajo es el del propio paquete de JSON: marca el error
|
|
22
|
+
* mientras se escribe. El aviso de afuera —`error`— sigue existiendo porque
|
|
23
|
+
* dice otra cosa: que el JSON está bien escrito pero no es un filtro.
|
|
24
|
+
* ====================================================================== */
|
|
25
|
+
/** Un JSON acomodado, o el mismo texto si todavía no se puede leer.
|
|
26
|
+
*
|
|
27
|
+
* in '{"all":[{"field":"a","operator":"=","value":1}]}'
|
|
28
|
+
* out el mismo, con sangría de dos espacios y un campo por línea
|
|
29
|
+
*
|
|
30
|
+
* Separado del componente para que se pueda probar sin dibujar nada, que es lo único que
|
|
31
|
+
* tiene lógica acá adentro. */
|
|
32
|
+
export function formatJson(text) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/* The built-in `light`/`dark` themes pick the SYNTAX colours; this layer paints the box from
|
|
41
|
+
the system's tokens, so the editor sits on the same surface as the fields around it in
|
|
42
|
+
either theme instead of on a hard-coded one. */
|
|
43
|
+
const SURFACE_THEME = EditorView.theme({
|
|
44
|
+
"&": { backgroundColor: "var(--color-surface)", color: "var(--color-ink)" },
|
|
45
|
+
".cm-gutters": {
|
|
46
|
+
backgroundColor: "var(--color-ground)",
|
|
47
|
+
color: "var(--color-muted)",
|
|
48
|
+
borderRightStyle: "solid",
|
|
49
|
+
borderRightWidth: "thin",
|
|
50
|
+
borderRightColor: "var(--color-line)",
|
|
51
|
+
},
|
|
52
|
+
".cm-activeLine, .cm-activeLineGutter": {
|
|
53
|
+
backgroundColor: "color-mix(in srgb, var(--color-ink) 5%, transparent)",
|
|
54
|
+
},
|
|
55
|
+
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--color-ink)" },
|
|
56
|
+
});
|
|
57
|
+
export function JsonEditor({ value, onChange, error, height = "16rem", label, }) {
|
|
58
|
+
const copy = useUiCopy();
|
|
59
|
+
const surface = useSurface();
|
|
60
|
+
const { resolved } = useTheme();
|
|
61
|
+
/* The chrome is dark in both themes; the canvas follows the theme. */
|
|
62
|
+
const dark = surface === "chrome" || resolved === "dark";
|
|
63
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-micro text-muted", children: label ?? copy.builder.editor }), _jsxs(Button, { role: "ghost", size: "sm", disabled: value.trim() === "", onClick: () => onChange(formatJson(value)), children: [_jsx(Wand2, { className: "size-3" }), " ", copy.builder.format] })] }), _jsx("div", { className: `overflow-hidden rounded-control border ${error == null ? "border-ink/15" : "border-danger"}`, children: _jsx(CodeMirror, { value: value, onChange: onChange, height: height, theme: dark ? "dark" : "light", extensions: [
|
|
64
|
+
json(),
|
|
65
|
+
linter(jsonParseLinter()),
|
|
66
|
+
lintGutter(),
|
|
67
|
+
...(surface === "chrome" ? [] : [Prec.highest(SURFACE_THEME)]),
|
|
68
|
+
], basicSetup: {
|
|
69
|
+
lineNumbers: true,
|
|
70
|
+
foldGutter: true,
|
|
71
|
+
highlightActiveLine: true,
|
|
72
|
+
autocompletion: false,
|
|
73
|
+
/* El resaltado del par de llaves es lo que hace legible un filtro anidado: sin él,
|
|
74
|
+
en un `any` adentro de un `all` hay que contar corchetes con el dedo. */
|
|
75
|
+
bracketMatching: true,
|
|
76
|
+
closeBrackets: true,
|
|
77
|
+
} }) }), error == null ? null : _jsx("p", { className: "text-micro text-danger", children: error })] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type CriteriaJoin, type CriteriaPath, type SearchCriteria } from "./criteria.js";
|
|
2
|
+
import type { ModelDefinition } from "./model.js";
|
|
3
|
+
import type { Surface } from "../../foundations/types.js";
|
|
4
|
+
import type { MenuGroup } from "../../primitives/menu/Menu.js";
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
/** Dónde se puede buscar. Lo declara el dominio, no esta pieza. */
|
|
7
|
+
export interface SearchFacet {
|
|
8
|
+
id: string;
|
|
9
|
+
/** Cómo se lee en la sugerencia y en la ficha: «nombre», «columnas». */
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* La CONJUNCIÓN entre dos recortes: «y» o «o».
|
|
14
|
+
*
|
|
15
|
+
* Con `onToggle` se puede tocar, y eso no es un adorno: «vencidas Y de este
|
|
16
|
+
* mes» y «vencidas O de este mes» son dos listas completamente distintas, y
|
|
17
|
+
* hasta acá cuál de las dos era estaba decidido por dentro y no se podía cambiar
|
|
18
|
+
* sin abrir el constructor. Sin `onToggle` es una palabra y nada más — que es lo
|
|
19
|
+
* correcto entre dos ejes, donde la Y no es una opción.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Lo que la barra necesita. Está aparte de la función porque es el contrato: lo
|
|
23
|
+
* que una lista tiene que darle para que la barra pregunte, y nada más.
|
|
24
|
+
*/
|
|
25
|
+
export type SearchBarProps = {
|
|
26
|
+
facets: readonly SearchFacet[];
|
|
27
|
+
/**
|
|
28
|
+
* El modelo, cuando la lista lo tiene.
|
|
29
|
+
*
|
|
30
|
+
* Es lo que convierte la barra de «buscar un texto en un campo» a «hacer una
|
|
31
|
+
* pregunta»: de acá salen el tipo de cada campo —y con él qué se le puede
|
|
32
|
+
* escribir—, los operadores que acepta, sus valores posibles si es un enum, y
|
|
33
|
+
* por qué columna se busca un relacional. Sin esto, `facets` sigue funcionando
|
|
34
|
+
* como siempre: cada faceta es un campo de texto.
|
|
35
|
+
*/
|
|
36
|
+
model?: ModelDefinition;
|
|
37
|
+
/**
|
|
38
|
+
* TODO lo que la barra tiene puesto: los niveles de agrupado y los filtros, en
|
|
39
|
+
* una sola lista de instancias.
|
|
40
|
+
*
|
|
41
|
+
* Una sola y no tres porque son lo mismo con clases distintas —ver
|
|
42
|
+
* `criteria.ts`—: lo que entra por el numeral, lo que se escribe a mano, lo
|
|
43
|
+
* que un filtro declarado trae con nombre y lo que sale del constructor son
|
|
44
|
+
* todas instancias, y por eso todas se leen, se tocan y se sacan igual.
|
|
45
|
+
*/
|
|
46
|
+
criteria: readonly SearchCriteria[];
|
|
47
|
+
onCriteriaChange: (next: SearchCriteria[]) => void;
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Lo que el panel OFRECE. Lo puesto no sale de acá: sale de `criteria`. */
|
|
50
|
+
filters?: readonly MenuGroup[];
|
|
51
|
+
/**
|
|
52
|
+
* El editor de niveles de agrupado: `<GroupingLevels …/>`.
|
|
53
|
+
*
|
|
54
|
+
* Va en la MISMA columna del panel donde están los filtros, porque es la misma
|
|
55
|
+
* pregunta —«qué parte de esto estoy mirando»— y separarlos obliga a abrir dos
|
|
56
|
+
* cosas para armar un recorte sin poder ver los dos estados a la vez.
|
|
57
|
+
*/
|
|
58
|
+
grouping?: ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* Alguien escribió `#and`, `#or` o `#not` y la barra abrió un nodo: esto avisa
|
|
61
|
+
* cuál, por si quien compone quiere hacer algo más con eso.
|
|
62
|
+
*/
|
|
63
|
+
onOpenGroup?: (join: CriteriaJoin) => void;
|
|
64
|
+
favorites?: readonly MenuGroup[];
|
|
65
|
+
onFilterSelect?: (id: string) => void;
|
|
66
|
+
onFavoriteSelect?: (id: string) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Abre el constructor. Recibe la RUTA de la instancia que se tocó, cuando se
|
|
69
|
+
* tocó una: el constructor abre ahí, en vez de abrir en la primera regla y
|
|
70
|
+
* obligar a buscar la que uno venía mirando entre doce.
|
|
71
|
+
*/
|
|
72
|
+
onOpenBuilder?: (path?: CriteriaPath) => void;
|
|
73
|
+
/** Lo que va pegado al campo, a su derecha. */
|
|
74
|
+
trailing?: ReactNode;
|
|
75
|
+
/** Por default la hereda de la superficie que la envuelve. */
|
|
76
|
+
surface?: Surface;
|
|
77
|
+
};
|
|
78
|
+
export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, surface, }: SearchBarProps): import("react").JSX.Element;
|