@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,37 @@
|
|
|
1
|
+
/* ==================================================================== *
|
|
2
|
+
* EL VOCABULARIO DEL MODELO — lo que el servidor publica sobre sus campos.
|
|
3
|
+
*
|
|
4
|
+
* `model_meta_data` dice, por campo, de qué tipo es, qué comparaciones acepta,
|
|
5
|
+
* si es relacional y por qué columna se lo identifica, y qué valores puede
|
|
6
|
+
* tomar si es un enum. Todo lo que una pantalla ofrece sobre un modelo sale de
|
|
7
|
+
* acá: los filtros del constructor, los niveles del agrupado y las preguntas de
|
|
8
|
+
* la barra de búsqueda.
|
|
9
|
+
*
|
|
10
|
+
* Vive aparte de quien lo dibuja porque lo hablan los tres. Mientras estuvo
|
|
11
|
+
* adentro de `FilterBuilder.tsx`, la barra tenía que importar del constructor
|
|
12
|
+
* de filtros para saber cómo se llama un campo — y el constructor, de la barra
|
|
13
|
+
* para saber cómo se llama un operador. Un vocabulario compartido no puede
|
|
14
|
+
* colgar de uno de los que lo hablan.
|
|
15
|
+
* ==================================================================== */
|
|
16
|
+
/** Si este campo se puede usar en un `where`, un `order` o un `grouping.by` directamente.
|
|
17
|
+
* `ops.length === 0` alcanza como señal: la publican tanto `embedded` como los tres
|
|
18
|
+
* relacionales, así que no hace falta mirar `kind` aparte. */
|
|
19
|
+
export function isFilterable(field) {
|
|
20
|
+
return field.ops.length > 0;
|
|
21
|
+
}
|
|
22
|
+
/** El rótulo de un campo en el idioma pedido: el que el modelo publicó para ese idioma, o el
|
|
23
|
+
* que publicó como `default`, o el nombre del campo tal cual si el modelo no dijo nada —un
|
|
24
|
+
* modelo sintético, o uno que todavía no llegó. Nunca inventa una palabra: lo que no está en
|
|
25
|
+
* `translations` se ve tal cual sale de la tabla, y eso ya avisa que falta declararlo. */
|
|
26
|
+
export function fieldLabel(model, field, locale) {
|
|
27
|
+
const words = model.translations?.labels[field];
|
|
28
|
+
return words?.[locale] ?? words?.default ?? field;
|
|
29
|
+
}
|
|
30
|
+
/** Los nombres del modelo que se pueden usar como blanco de un filtro, un orden o un nivel de
|
|
31
|
+
* agrupado — nunca un relacional ni un valor-objeto, filtrados por `isFilterable`. Ordenados,
|
|
32
|
+
* porque es la lista que arma cada `<select>` de este archivo. */
|
|
33
|
+
export function filterableNames(model) {
|
|
34
|
+
return Object.keys(model.fields)
|
|
35
|
+
.filter((name) => isFilterable(model.fields[name]))
|
|
36
|
+
.sort();
|
|
37
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { type ModelDefinition } from "./model.js";
|
|
2
|
+
/** Qué clase de valor tiene un campo. Sale del `type` que publica el modelo y es
|
|
3
|
+
* lo que decide qué se le puede preguntar. */
|
|
4
|
+
export type QueryKind = "text" | "number" | "date" | "boolean" | "choice" | "relation";
|
|
5
|
+
/** Un campo por el que se puede preguntar desde la barra. */
|
|
6
|
+
export type QueryField = {
|
|
7
|
+
/** Contra qué campo va la condición. Para un relacional es la columna que lo
|
|
8
|
+
* identifica y no el relacional en sí: el motor no compara un agregado. */
|
|
9
|
+
field: string;
|
|
10
|
+
label: string;
|
|
11
|
+
kind: QueryKind;
|
|
12
|
+
/** Los operadores que el modelo acepta para él, en el orden que los publicó. */
|
|
13
|
+
ops: readonly string[];
|
|
14
|
+
/** Los valores posibles, cuando es un enum: de ahí sale la tercera lista. */
|
|
15
|
+
choices?: readonly string[];
|
|
16
|
+
/** Cuando se llega por una relación: contra qué agregado se está preguntando
|
|
17
|
+
* de verdad. Es lo que deja escribir «el proyecto se llama X» sin que nadie
|
|
18
|
+
* sepa que por dentro se compara otra columna. */
|
|
19
|
+
through?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Qué forma tiene lo que alguien escribió. Es lo que decide qué campos pueden
|
|
22
|
+
* contestarlo. */
|
|
23
|
+
export type ValueShape = "empty" | "text" | "glob" | "number" | "date";
|
|
24
|
+
/** Lo tipeado, ya leído: el operador que trae puesto —si lo trae— y su forma. */
|
|
25
|
+
export type TypedValue = {
|
|
26
|
+
op?: string;
|
|
27
|
+
value: string;
|
|
28
|
+
shape: ValueShape;
|
|
29
|
+
/** Se escribió `#`: se está eligiendo el ATRIBUTO y no un valor. */
|
|
30
|
+
attribute?: true;
|
|
31
|
+
/** Se escribió `#and`, `#or` o `#not`: no se está eligiendo un atributo, se
|
|
32
|
+
* está ABRIENDO UN GRUPO para meter adentro lo que venga. */
|
|
33
|
+
opens?: "all" | "any" | "not";
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Lee lo que alguien escribió: el operador que trae adelante, el valor, y de qué
|
|
37
|
+
* forma es.
|
|
38
|
+
*
|
|
39
|
+
* Un molde —`*.csv`, `ventas_*`— es distinto de un texto: se busca con `like` y
|
|
40
|
+
* el `*` se traduce al comodín del motor. Un texto suelto NO es un molde
|
|
41
|
+
* implícito: quien escribe `test` está pidiendo lo que contenga «test», y quien
|
|
42
|
+
* escribe `*.csv` está pidiendo lo que TERMINE en «.csv», que es otra pregunta.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readTyped(text: string): TypedValue;
|
|
45
|
+
/** El molde, con el comodín que entiende el motor. `*` es lo que la gente
|
|
46
|
+
* escribe —y lo que escribe una terminal— y `%` es lo que entiende un `like`. */
|
|
47
|
+
export declare function globToLike(value: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Los campos por los que la barra puede preguntar, sacados del modelo.
|
|
50
|
+
*
|
|
51
|
+
* Un relacional entra por la columna que lo IDENTIFICA y con el rótulo de la
|
|
52
|
+
* relación: preguntar «el cliente se llama Ferretería» es una pregunta sobre el
|
|
53
|
+
* cliente aunque por dentro compare su nombre. Un relacional sin `identifiedBy`
|
|
54
|
+
* no entra: no hay nada contra qué comparar, y ofrecerlo sería ofrecer una
|
|
55
|
+
* pregunta que no se puede contestar.
|
|
56
|
+
*/
|
|
57
|
+
export declare function queryFields(model: ModelDefinition, locale: string): QueryField[];
|
|
58
|
+
/**
|
|
59
|
+
* Si un campo puede contestar lo que se escribió.
|
|
60
|
+
*
|
|
61
|
+
* Manda el OPERADOR cuando se escribió uno, porque es lo que dice qué clase de
|
|
62
|
+
* pregunta es:
|
|
63
|
+
*
|
|
64
|
+
* = / != todo. Cualquier cosa se compara contra sí misma, así que
|
|
65
|
+
* escribir `=` abre la lista entera en vez de adivinar de qué
|
|
66
|
+
* clase es un valor que todavía no se escribió.
|
|
67
|
+
* > >= < <= sólo lo ORDENADO —un número, una fecha—. «Mayor que» sobre
|
|
68
|
+
* un texto es una comparación alfabética que nadie pidió.
|
|
69
|
+
*
|
|
70
|
+
* Sin operador escrito manda la FORMA del valor: `*.csv` es un molde y sólo lo
|
|
71
|
+
* contesta algo con letras; `100` sólo algo con números. Ofrecer «filas» para
|
|
72
|
+
* `*.csv` es ofrecer una pregunta que da cero.
|
|
73
|
+
*/
|
|
74
|
+
export declare function answers(field: QueryField, typed: TypedValue): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Con qué operador se pregunta, cuando nadie lo eligió.
|
|
77
|
+
*
|
|
78
|
+
* El que se escribió gana. Si no, el que la forma pide: un molde se pregunta con
|
|
79
|
+
* `like`, un texto con `like` si el campo lo acepta —«contiene» es lo que quiere
|
|
80
|
+
* quien escribe media palabra— y todo lo demás con `=`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function defaultOp(field: QueryField, typed: TypedValue): string;
|
|
83
|
+
/** El valor tal como viaja: un molde se traduce al comodín del motor, y lo demás
|
|
84
|
+
* va como se escribió. */
|
|
85
|
+
export declare function valueFor(op: string, typed: TypedValue): string;
|
|
86
|
+
/** Una pregunta a medio escribir: lo que ya se eligió y lo que se está
|
|
87
|
+
* tipeando. */
|
|
88
|
+
export type QueryDraft = {
|
|
89
|
+
field?: QueryField;
|
|
90
|
+
op?: string;
|
|
91
|
+
text: string;
|
|
92
|
+
};
|
|
93
|
+
export type QuerySuggestion =
|
|
94
|
+
/** Seguir por este campo. Todavía falta el operador y el valor. */
|
|
95
|
+
{
|
|
96
|
+
kind: "field";
|
|
97
|
+
id: string;
|
|
98
|
+
label: string;
|
|
99
|
+
hint?: string;
|
|
100
|
+
field: QueryField;
|
|
101
|
+
}
|
|
102
|
+
/** Preguntar con este operador. */
|
|
103
|
+
| {
|
|
104
|
+
kind: "op";
|
|
105
|
+
id: string;
|
|
106
|
+
label: string;
|
|
107
|
+
op: string;
|
|
108
|
+
}
|
|
109
|
+
/** Abrir un GRUPO: lo que se escriba después entra adentro. */
|
|
110
|
+
| {
|
|
111
|
+
kind: "group";
|
|
112
|
+
id: string;
|
|
113
|
+
label: string;
|
|
114
|
+
join: "all" | "any" | "not";
|
|
115
|
+
}
|
|
116
|
+
/** La pregunta entera, lista para entrar como ficha. */
|
|
117
|
+
| {
|
|
118
|
+
kind: "condition";
|
|
119
|
+
id: string;
|
|
120
|
+
label: string;
|
|
121
|
+
hint: string;
|
|
122
|
+
field: QueryField;
|
|
123
|
+
op: string;
|
|
124
|
+
value: string;
|
|
125
|
+
};
|
|
126
|
+
/** Cómo se llama un operador para quien lo lee. Sale de la copia, así que un
|
|
127
|
+
* operador que el servidor agregue aparece con su símbolo hasta que alguien le
|
|
128
|
+
* ponga palabra — y no desaparece. */
|
|
129
|
+
export declare function opLabel(op: string): string;
|
|
130
|
+
/** Si el operador no lleva valor: preguntar si algo está vacío no necesita que
|
|
131
|
+
* se escriba nada. */
|
|
132
|
+
export declare function withoutValue(op: string): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Si el operador compara contra DOS valores.
|
|
135
|
+
*
|
|
136
|
+
* La barra escribe UNO —es una línea, no un formulario— así que éstos no se
|
|
137
|
+
* ofrecen acá: elegir «está entre» y escribir un número arma medio rango, que
|
|
138
|
+
* no es una pregunta más laxa sino una que el motor no puede contestar. Se
|
|
139
|
+
* preguntan en el constructor, que tiene los dos campos.
|
|
140
|
+
*/
|
|
141
|
+
export declare function withTwoValues(op: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Qué se ofrece ahora mismo.
|
|
144
|
+
*
|
|
145
|
+
* Es una sola lista y no tres pantallas: en el primer paso conviven los campos
|
|
146
|
+
* que se llaman como lo tipeado —«nombr» → «nombre»— con las preguntas enteras
|
|
147
|
+
* que lo tipeado ya permite armar —«test» en el nombre—. Quien escribe el nombre
|
|
148
|
+
* de un campo quiere seguir por ahí; quien escribe un valor quiere preguntarlo
|
|
149
|
+
* ya. Las dos intenciones se escriben igual, así que se ofrecen las dos, con los
|
|
150
|
+
* campos primero: es el paso que todavía no decidió nada.
|
|
151
|
+
*/
|
|
152
|
+
export declare function suggest(draft: QueryDraft, fields: readonly QueryField[]): QuerySuggestion[];
|
|
153
|
+
/**
|
|
154
|
+
* CÓMO SE LEE UNA CONDICIÓN, en un solo lugar.
|
|
155
|
+
*
|
|
156
|
+
* La usan la barra —para la ficha de lo que alguien acaba de preguntar— y el
|
|
157
|
+
* constructor —para la ficha y para el árbol de lo que ya está armado—. Una
|
|
158
|
+
* sola, porque la misma condición escrita de dos maneras se lee distinta en dos
|
|
159
|
+
* pantallas y nadie sabe cuál de las dos es la que filtra.
|
|
160
|
+
*
|
|
161
|
+
* Toma el RÓTULO ya resuelto y no el campo: quién sabe cómo se llama
|
|
162
|
+
* `row_count` es el modelo, y esto sólo sabe armar la frase.
|
|
163
|
+
*/
|
|
164
|
+
export declare function sayCondition(label: string, op: string, value: string): string;
|
|
165
|
+
/** Cómo se lee una pregunta ya hecha, para la ficha: «el nombre contiene test». */
|
|
166
|
+
export declare function describeCondition(field: QueryField, op: string, value: string): string;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { fieldLabel, isFilterable, } from "./model.js";
|
|
2
|
+
import { currentCopy, fillCopy } from "../../foundations/copy.js";
|
|
3
|
+
/**
|
|
4
|
+
* Las palabras que, después del numeral, abren un GRUPO en vez de nombrar un
|
|
5
|
+
* atributo.
|
|
6
|
+
*
|
|
7
|
+
* Se escriben como se piensan —`#and`, `#or`, `#not`— y también como se leen en
|
|
8
|
+
* la barra —`#todas`, `#cualquiera`, `#ninguna`—, porque son la misma cosa dicha
|
|
9
|
+
* de dos maneras y obligar a una sola es obligar a aprenderla.
|
|
10
|
+
*/
|
|
11
|
+
const OPENS = {
|
|
12
|
+
and: "all",
|
|
13
|
+
all: "all",
|
|
14
|
+
todas: "all",
|
|
15
|
+
or: "any",
|
|
16
|
+
any: "any",
|
|
17
|
+
cualquiera: "any",
|
|
18
|
+
not: "not",
|
|
19
|
+
none: "not",
|
|
20
|
+
ninguna: "not",
|
|
21
|
+
};
|
|
22
|
+
/* Los operadores que se pueden escribir adelante del valor, de más largo a más
|
|
23
|
+
corto: si `<` se probara antes que `<=`, «<=100» se leería como «< =100». */
|
|
24
|
+
const WRITTEN_OPS = ["<=", ">=", "!=", "<", ">", "="];
|
|
25
|
+
/**
|
|
26
|
+
* Los que ORDENAN. Sólo los puede contestar algo que tenga orden —un número, una
|
|
27
|
+
* fecha—, y no porque el campo no publique el operador: «mayor que» sobre un
|
|
28
|
+
* texto es una comparación alfabética que nadie pidió nunca y que da resultados
|
|
29
|
+
* que no se pueden explicar.
|
|
30
|
+
*/
|
|
31
|
+
const ORDERING = new Set([">", ">=", "<", "<="]);
|
|
32
|
+
/**
|
|
33
|
+
* Los que sólo comparan IGUALDAD. Estos no discriminan por tipo: todo se puede
|
|
34
|
+
* comparar contra sí mismo, así que escribir `=` abre la lista entera en vez de
|
|
35
|
+
* adivinar de qué clase es lo que todavía no se escribió.
|
|
36
|
+
*/
|
|
37
|
+
const EQUALITY = new Set(["=", "!="]);
|
|
38
|
+
/** Qué clases tienen orden. Un enum no: sus valores son palabras aunque se
|
|
39
|
+
* escriban con números. */
|
|
40
|
+
const ORDERED = new Set(["number", "date"]);
|
|
41
|
+
const NUMBER = /^-?\d+([.,]\d+)?$/;
|
|
42
|
+
const DATE = /^\d{4}-\d{2}(-\d{2})?$/;
|
|
43
|
+
/**
|
|
44
|
+
* Lee lo que alguien escribió: el operador que trae adelante, el valor, y de qué
|
|
45
|
+
* forma es.
|
|
46
|
+
*
|
|
47
|
+
* Un molde —`*.csv`, `ventas_*`— es distinto de un texto: se busca con `like` y
|
|
48
|
+
* el `*` se traduce al comodín del motor. Un texto suelto NO es un molde
|
|
49
|
+
* implícito: quien escribe `test` está pidiendo lo que contenga «test», y quien
|
|
50
|
+
* escribe `*.csv` está pidiendo lo que TERMINE en «.csv», que es otra pregunta.
|
|
51
|
+
*/
|
|
52
|
+
export function readTyped(text) {
|
|
53
|
+
const clean = text.trim();
|
|
54
|
+
if (clean === "")
|
|
55
|
+
return { value: "", shape: "empty" };
|
|
56
|
+
/* `#` es «elegí el atributo»: lo que sigue no es un valor, es el nombre del
|
|
57
|
+
campo por el que se va a preguntar. Existe porque las dos intenciones se
|
|
58
|
+
escriben con las mismas letras —«nombre» puede ser el campo o el texto que
|
|
59
|
+
se busca— y hasta acá había que confiar en que la barra adivinara cuál. Con
|
|
60
|
+
el numeral no hay nada que adivinar. */
|
|
61
|
+
if (clean.startsWith("#")) {
|
|
62
|
+
const rest = clean.slice(1).trim();
|
|
63
|
+
const opens = OPENS[rest.toLowerCase()];
|
|
64
|
+
/* `#and` no es un atributo que se llame «and»: es la orden de abrir un
|
|
65
|
+
grupo. Se mira primero porque un modelo podría tener un campo llamado
|
|
66
|
+
`and`, y entre las dos lecturas gana la que alguien escribió a propósito
|
|
67
|
+
—nadie tipea el numeral para buscar la palabra «or». */
|
|
68
|
+
/* `#` a secas es «elegí el atributo» sin haber empezado a nombrarlo: la forma
|
|
69
|
+
es «vacío», como en cualquier otro lado. Decir «texto» de una cadena vacía
|
|
70
|
+
es mentir sobre lo que hay escrito. */
|
|
71
|
+
const shape = rest === "" ? "empty" : "text";
|
|
72
|
+
if (opens !== undefined)
|
|
73
|
+
return { value: rest, shape, attribute: true, opens };
|
|
74
|
+
return { value: rest, shape, attribute: true };
|
|
75
|
+
}
|
|
76
|
+
const written = WRITTEN_OPS.find((op) => clean.startsWith(op));
|
|
77
|
+
const rest = written === undefined ? clean : clean.slice(written.length).trim();
|
|
78
|
+
const quoted = rest.length > 1 && rest.startsWith('"') && rest.endsWith('"')
|
|
79
|
+
? rest.slice(1, -1)
|
|
80
|
+
: undefined;
|
|
81
|
+
const value = quoted ?? rest;
|
|
82
|
+
const shape = value === ""
|
|
83
|
+
? "empty"
|
|
84
|
+
: quoted !== undefined
|
|
85
|
+
? "text"
|
|
86
|
+
: NUMBER.test(value)
|
|
87
|
+
? "number"
|
|
88
|
+
: DATE.test(value)
|
|
89
|
+
? "date"
|
|
90
|
+
: value.includes("*")
|
|
91
|
+
? "glob"
|
|
92
|
+
: "text";
|
|
93
|
+
return { ...(written === undefined ? {} : { op: written }), value, shape };
|
|
94
|
+
}
|
|
95
|
+
/** El molde, con el comodín que entiende el motor. `*` es lo que la gente
|
|
96
|
+
* escribe —y lo que escribe una terminal— y `%` es lo que entiende un `like`. */
|
|
97
|
+
export function globToLike(value) {
|
|
98
|
+
return value.replace(/\*/g, "%");
|
|
99
|
+
}
|
|
100
|
+
/** Qué clase de valor es un campo, leída del tipo que publica el modelo. */
|
|
101
|
+
function kindOf(definition) {
|
|
102
|
+
if (definition.kind === "relation")
|
|
103
|
+
return "relation";
|
|
104
|
+
if (definition.choices !== undefined && definition.choices.length > 0)
|
|
105
|
+
return "choice";
|
|
106
|
+
const type = definition.type.toLowerCase();
|
|
107
|
+
if (type === "integer" || type === "number" || type === "float")
|
|
108
|
+
return "number";
|
|
109
|
+
if (type === "date" || type === "datetime")
|
|
110
|
+
return "date";
|
|
111
|
+
if (type === "boolean" || type === "bool")
|
|
112
|
+
return "boolean";
|
|
113
|
+
return "text";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Los campos por los que la barra puede preguntar, sacados del modelo.
|
|
117
|
+
*
|
|
118
|
+
* Un relacional entra por la columna que lo IDENTIFICA y con el rótulo de la
|
|
119
|
+
* relación: preguntar «el cliente se llama Ferretería» es una pregunta sobre el
|
|
120
|
+
* cliente aunque por dentro compare su nombre. Un relacional sin `identifiedBy`
|
|
121
|
+
* no entra: no hay nada contra qué comparar, y ofrecerlo sería ofrecer una
|
|
122
|
+
* pregunta que no se puede contestar.
|
|
123
|
+
*/
|
|
124
|
+
export function queryFields(model, locale) {
|
|
125
|
+
/* Las columnas que ya nombran a una relación no se ofrecen por su cuenta: son
|
|
126
|
+
la misma pregunta con dos rótulos —«proyecto» y «nombre del proyecto»— y la
|
|
127
|
+
que se entiende es la de la relación. */
|
|
128
|
+
const naming = new Set(Object.values(model.fields)
|
|
129
|
+
.filter((one) => one.kind === "relation")
|
|
130
|
+
.map((one) => one.identifiedBy)
|
|
131
|
+
.filter((one) => one !== null && one !== undefined));
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const [name, definition] of Object.entries(model.fields)) {
|
|
134
|
+
const kind = kindOf(definition);
|
|
135
|
+
if (kind === "relation") {
|
|
136
|
+
const through = definition.identifiedBy;
|
|
137
|
+
if (through === null || through === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
const identifying = model.fields[through];
|
|
140
|
+
if (identifying === undefined || !isFilterable(identifying))
|
|
141
|
+
continue;
|
|
142
|
+
out.push({
|
|
143
|
+
field: through,
|
|
144
|
+
label: fieldLabel(model, name, locale),
|
|
145
|
+
kind: "relation",
|
|
146
|
+
ops: identifying.ops,
|
|
147
|
+
through: definition.relation ?? name,
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!isFilterable(definition) || naming.has(name))
|
|
152
|
+
continue;
|
|
153
|
+
out.push({
|
|
154
|
+
field: name,
|
|
155
|
+
label: fieldLabel(model, name, locale),
|
|
156
|
+
kind,
|
|
157
|
+
ops: definition.ops,
|
|
158
|
+
...(definition.choices === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: { choices: definition.choices.map((one) => String(one)) }),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Si un campo puede contestar lo que se escribió.
|
|
167
|
+
*
|
|
168
|
+
* Manda el OPERADOR cuando se escribió uno, porque es lo que dice qué clase de
|
|
169
|
+
* pregunta es:
|
|
170
|
+
*
|
|
171
|
+
* = / != todo. Cualquier cosa se compara contra sí misma, así que
|
|
172
|
+
* escribir `=` abre la lista entera en vez de adivinar de qué
|
|
173
|
+
* clase es un valor que todavía no se escribió.
|
|
174
|
+
* > >= < <= sólo lo ORDENADO —un número, una fecha—. «Mayor que» sobre
|
|
175
|
+
* un texto es una comparación alfabética que nadie pidió.
|
|
176
|
+
*
|
|
177
|
+
* Sin operador escrito manda la FORMA del valor: `*.csv` es un molde y sólo lo
|
|
178
|
+
* contesta algo con letras; `100` sólo algo con números. Ofrecer «filas» para
|
|
179
|
+
* `*.csv` es ofrecer una pregunta que da cero.
|
|
180
|
+
*/
|
|
181
|
+
export function answers(field, typed) {
|
|
182
|
+
/* El operador se mira ANTES que el valor, y no después: escribir `>` ya
|
|
183
|
+
descarta los textos aunque todavía no se haya escrito contra qué. Al revés,
|
|
184
|
+
la barra ofrecía las cinco columnas hasta que se tipeaba el primer dígito. */
|
|
185
|
+
if (typed.op !== undefined) {
|
|
186
|
+
if (!field.ops.includes(typed.op))
|
|
187
|
+
return false;
|
|
188
|
+
if (EQUALITY.has(typed.op))
|
|
189
|
+
return true;
|
|
190
|
+
if (ORDERING.has(typed.op) && !ORDERED.has(field.kind))
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (typed.shape === "empty")
|
|
194
|
+
return true;
|
|
195
|
+
switch (typed.shape) {
|
|
196
|
+
case "number":
|
|
197
|
+
return field.kind === "number" || field.kind === "choice";
|
|
198
|
+
case "date":
|
|
199
|
+
return field.kind === "date";
|
|
200
|
+
case "glob":
|
|
201
|
+
return field.kind === "text" || field.kind === "relation";
|
|
202
|
+
default:
|
|
203
|
+
/* Un texto lo contesta lo que tiene letras adentro, y también un enum: sus
|
|
204
|
+
valores son palabras. Un número no —«test» no es un número— y una fecha
|
|
205
|
+
tampoco. */
|
|
206
|
+
return field.kind === "text" || field.kind === "relation" || field.kind === "choice";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Con qué operador se pregunta, cuando nadie lo eligió.
|
|
211
|
+
*
|
|
212
|
+
* El que se escribió gana. Si no, el que la forma pide: un molde se pregunta con
|
|
213
|
+
* `like`, un texto con `like` si el campo lo acepta —«contiene» es lo que quiere
|
|
214
|
+
* quien escribe media palabra— y todo lo demás con `=`.
|
|
215
|
+
*/
|
|
216
|
+
export function defaultOp(field, typed) {
|
|
217
|
+
if (typed.op !== undefined && field.ops.includes(typed.op))
|
|
218
|
+
return typed.op;
|
|
219
|
+
if (typed.shape === "glob" && field.ops.includes("like"))
|
|
220
|
+
return "like";
|
|
221
|
+
if (typed.shape === "text" && field.ops.includes("like"))
|
|
222
|
+
return "like";
|
|
223
|
+
return field.ops.includes("=") ? "=" : (field.ops[0] ?? "=");
|
|
224
|
+
}
|
|
225
|
+
/** El valor tal como viaja: un molde se traduce al comodín del motor, y lo demás
|
|
226
|
+
* va como se escribió. */
|
|
227
|
+
export function valueFor(op, typed) {
|
|
228
|
+
return op === "like" && typed.shape === "glob" ? globToLike(typed.value) : typed.value;
|
|
229
|
+
}
|
|
230
|
+
/** Cómo se llama un operador para quien lo lee. Sale de la copia, así que un
|
|
231
|
+
* operador que el servidor agregue aparece con su símbolo hasta que alguien le
|
|
232
|
+
* ponga palabra — y no desaparece. */
|
|
233
|
+
export function opLabel(op) {
|
|
234
|
+
return currentCopy().ops[op] ?? op;
|
|
235
|
+
}
|
|
236
|
+
/** Si el operador no lleva valor: preguntar si algo está vacío no necesita que
|
|
237
|
+
* se escriba nada. */
|
|
238
|
+
export function withoutValue(op) {
|
|
239
|
+
return op === "is null" || op === "is not null";
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Si el operador compara contra DOS valores.
|
|
243
|
+
*
|
|
244
|
+
* La barra escribe UNO —es una línea, no un formulario— así que éstos no se
|
|
245
|
+
* ofrecen acá: elegir «está entre» y escribir un número arma medio rango, que
|
|
246
|
+
* no es una pregunta más laxa sino una que el motor no puede contestar. Se
|
|
247
|
+
* preguntan en el constructor, que tiene los dos campos.
|
|
248
|
+
*/
|
|
249
|
+
export function withTwoValues(op) {
|
|
250
|
+
return op === "between";
|
|
251
|
+
}
|
|
252
|
+
function matches(field, text) {
|
|
253
|
+
const needle = text.trim().toLowerCase();
|
|
254
|
+
if (needle === "")
|
|
255
|
+
return true;
|
|
256
|
+
return (field.label.toLowerCase().includes(needle) || field.field.toLowerCase().includes(needle));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Qué se ofrece ahora mismo.
|
|
260
|
+
*
|
|
261
|
+
* Es una sola lista y no tres pantallas: en el primer paso conviven los campos
|
|
262
|
+
* que se llaman como lo tipeado —«nombr» → «nombre»— con las preguntas enteras
|
|
263
|
+
* que lo tipeado ya permite armar —«test» en el nombre—. Quien escribe el nombre
|
|
264
|
+
* de un campo quiere seguir por ahí; quien escribe un valor quiere preguntarlo
|
|
265
|
+
* ya. Las dos intenciones se escriben igual, así que se ofrecen las dos, con los
|
|
266
|
+
* campos primero: es el paso que todavía no decidió nada.
|
|
267
|
+
*/
|
|
268
|
+
export function suggest(draft, fields) {
|
|
269
|
+
const typed = readTyped(draft.text);
|
|
270
|
+
/* Paso 3 — el campo y el operador ya están: falta el valor. */
|
|
271
|
+
if (draft.field !== undefined && draft.op !== undefined) {
|
|
272
|
+
const field = draft.field;
|
|
273
|
+
/* Escribir un molde CAMBIA la comparación: «igual a ventas_*» busca un
|
|
274
|
+
nombre que termine en asterisco, que no existe y que nadie quiso pedir.
|
|
275
|
+
Sólo pisa la igualdad —quien eligió «mayor que» y escribió un `*` está
|
|
276
|
+
escribiendo cualquier otra cosa— y sólo si el campo publica `like`. */
|
|
277
|
+
const op = typed.shape === "glob" && EQUALITY.has(draft.op) && field.ops.includes("like")
|
|
278
|
+
? "like"
|
|
279
|
+
: draft.op;
|
|
280
|
+
const values = field.choices === undefined
|
|
281
|
+
? []
|
|
282
|
+
: field.choices.filter((one) => one.toLowerCase().includes(typed.value.toLowerCase()));
|
|
283
|
+
const ready = values.map((one) => ({
|
|
284
|
+
kind: "condition",
|
|
285
|
+
id: `v:${one}`,
|
|
286
|
+
label: one,
|
|
287
|
+
hint: `${field.label} ${opLabel(op)}`,
|
|
288
|
+
field,
|
|
289
|
+
op,
|
|
290
|
+
value: one,
|
|
291
|
+
}));
|
|
292
|
+
if (typed.shape !== "empty" && !values.includes(typed.value)) {
|
|
293
|
+
ready.unshift({
|
|
294
|
+
kind: "condition",
|
|
295
|
+
id: "v:typed",
|
|
296
|
+
label: valueFor(op, typed),
|
|
297
|
+
hint: `${field.label} ${opLabel(op)}`,
|
|
298
|
+
field,
|
|
299
|
+
op,
|
|
300
|
+
value: valueFor(op, typed),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return ready;
|
|
304
|
+
}
|
|
305
|
+
/* Paso 2 — el campo ya está: se ofrecen SUS operadores. El que la forma de lo
|
|
306
|
+
tipeado pide va primero, porque escribir «100» después de elegir «filas» es
|
|
307
|
+
casi siempre «igual a 100». */
|
|
308
|
+
if (draft.field !== undefined) {
|
|
309
|
+
const field = draft.field;
|
|
310
|
+
const first = defaultOp(field, typed);
|
|
311
|
+
const ops = [first, ...field.ops.filter((one) => one !== first)].filter((op) => !withTwoValues(op));
|
|
312
|
+
return ops.map((op) =>
|
|
313
|
+
/* El que NO lleva valor es una pregunta entera apenas se elige: mandarlo
|
|
314
|
+
al paso del valor dejaba al que eligió «está vacío» tipeando algo que no
|
|
315
|
+
significa nada, con una lista vacía abajo y Enter sin hacer nada. */
|
|
316
|
+
withoutValue(op)
|
|
317
|
+
? {
|
|
318
|
+
kind: "condition",
|
|
319
|
+
id: `op:${op}`,
|
|
320
|
+
label: opLabel(op),
|
|
321
|
+
hint: field.label,
|
|
322
|
+
field,
|
|
323
|
+
op,
|
|
324
|
+
value: "",
|
|
325
|
+
}
|
|
326
|
+
: { kind: "op", id: `op:${op}`, label: opLabel(op), op });
|
|
327
|
+
}
|
|
328
|
+
/* `#and`, `#or`, `#not`: lo que se pidió es abrir un grupo, no elegir un
|
|
329
|
+
campo. Se ofrece ESO solo — quien lo escribió ya sabe lo que quiere. */
|
|
330
|
+
if (typed.opens !== undefined) {
|
|
331
|
+
const join = typed.opens;
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
kind: "group",
|
|
335
|
+
id: `g:${join}`,
|
|
336
|
+
label: currentCopy().search.joins[join === "not" ? "none" : join],
|
|
337
|
+
join,
|
|
338
|
+
},
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
/* Con `#` a secas se está eligiendo el ATRIBUTO, así que se ofrecen los campos
|
|
342
|
+
—y los tres abridores de grupo, porque también se piden con el numeral y
|
|
343
|
+
esconderlos sería tener una notación que nadie descubre. */
|
|
344
|
+
if (typed.attribute === true) {
|
|
345
|
+
const opening = typed.value === ""
|
|
346
|
+
? ["all", "any", "not"].map((join) => ({
|
|
347
|
+
kind: "group",
|
|
348
|
+
id: `g:${join}`,
|
|
349
|
+
label: currentCopy().search.joins[join === "not" ? "none" : join],
|
|
350
|
+
join,
|
|
351
|
+
}))
|
|
352
|
+
: [];
|
|
353
|
+
return [
|
|
354
|
+
...opening,
|
|
355
|
+
...fields
|
|
356
|
+
.filter((field) => matches(field, typed.value))
|
|
357
|
+
.map((field) => ({
|
|
358
|
+
kind: "field",
|
|
359
|
+
id: `f:${field.field}`,
|
|
360
|
+
label: field.label,
|
|
361
|
+
...(field.through === undefined ? {} : { hint: field.through }),
|
|
362
|
+
field,
|
|
363
|
+
})),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
/* Paso 1 — todavía no se eligió nada. */
|
|
367
|
+
const byName = typed.shape === "empty"
|
|
368
|
+
? []
|
|
369
|
+
: fields
|
|
370
|
+
.filter((field) => matches(field, draft.text))
|
|
371
|
+
.map((field) => ({
|
|
372
|
+
kind: "field",
|
|
373
|
+
id: `f:${field.field}`,
|
|
374
|
+
label: field.label,
|
|
375
|
+
...(field.through === undefined ? {} : { hint: field.through }),
|
|
376
|
+
field,
|
|
377
|
+
}));
|
|
378
|
+
const ready = fields
|
|
379
|
+
.filter((field) => answers(field, typed))
|
|
380
|
+
.map((field) => {
|
|
381
|
+
const op = defaultOp(field, typed);
|
|
382
|
+
return {
|
|
383
|
+
kind: "condition",
|
|
384
|
+
id: `c:${field.field}`,
|
|
385
|
+
label: field.label,
|
|
386
|
+
hint: `${opLabel(op)} ${valueFor(op, typed)}`,
|
|
387
|
+
field,
|
|
388
|
+
op,
|
|
389
|
+
value: valueFor(op, typed),
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
/* Sin VALOR todavía no hay ninguna pregunta que armar —una pregunta sin valor
|
|
393
|
+
no es una pregunta— así que se ofrecen campos. Pero si ya se escribió el
|
|
394
|
+
operador, eso solo recorta: `>` deja los que tienen orden y saca los textos
|
|
395
|
+
antes de que se escriba el primer dígito. */
|
|
396
|
+
if (typed.shape === "empty") {
|
|
397
|
+
return fields
|
|
398
|
+
.filter((field) => answers(field, typed))
|
|
399
|
+
.map((field) => ({
|
|
400
|
+
kind: "field",
|
|
401
|
+
id: `f:${field.field}`,
|
|
402
|
+
label: field.label,
|
|
403
|
+
...(field.through === undefined ? {} : { hint: field.through }),
|
|
404
|
+
field,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
/* Un campo que se llama como lo tipeado no se ofrece dos veces: si «nombre»
|
|
408
|
+
aparece arriba para seguir por él, abajo ya no hace falta como pregunta
|
|
409
|
+
hecha — salvo que lo tipeado sea también un valor razonable, que es
|
|
410
|
+
justamente el caso de «test». */
|
|
411
|
+
const named = new Set(byName.map((one) => (one.kind === "field" ? one.field.field : "")));
|
|
412
|
+
return [
|
|
413
|
+
...byName,
|
|
414
|
+
...ready.filter((one) => one.kind !== "condition" || !named.has(one.field.field)),
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* CÓMO SE LEE UNA CONDICIÓN, en un solo lugar.
|
|
419
|
+
*
|
|
420
|
+
* La usan la barra —para la ficha de lo que alguien acaba de preguntar— y el
|
|
421
|
+
* constructor —para la ficha y para el árbol de lo que ya está armado—. Una
|
|
422
|
+
* sola, porque la misma condición escrita de dos maneras se lee distinta en dos
|
|
423
|
+
* pantallas y nadie sabe cuál de las dos es la que filtra.
|
|
424
|
+
*
|
|
425
|
+
* Toma el RÓTULO ya resuelto y no el campo: quién sabe cómo se llama
|
|
426
|
+
* `row_count` es el modelo, y esto sólo sabe armar la frase.
|
|
427
|
+
*/
|
|
428
|
+
export function sayCondition(label, op, value) {
|
|
429
|
+
return withoutValue(op)
|
|
430
|
+
? `${label} ${opLabel(op)}`
|
|
431
|
+
: fillCopy(currentCopy().search.condition, { field: label, op: opLabel(op), value });
|
|
432
|
+
}
|
|
433
|
+
/** Cómo se lee una pregunta ya hecha, para la ficha: «el nombre contiene test». */
|
|
434
|
+
export function describeCondition(field, op, value) {
|
|
435
|
+
return sayCondition(field.label, op, value);
|
|
436
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function AppFrame({
|
|
3
|
+
/** The bar at the top. It stays in view while scrolling. */
|
|
4
|
+
menu,
|
|
5
|
+
/** The band at the bottom. It goes AFTER the content, not pinned to the
|
|
6
|
+
* floor of the window — pinning left a hole between it and a short view. */
|
|
7
|
+
status,
|
|
8
|
+
/** The module's navigation, on the left of the view: a `NavigationTree`. */
|
|
9
|
+
sidebar, children, }: {
|
|
10
|
+
menu?: ReactNode;
|
|
11
|
+
status?: ReactNode;
|
|
12
|
+
sidebar?: ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|