pgkiln 0.44.0 → 0.46.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/db/migrations/102_app_computations.sql +345 -0
- package/db/migrations/103_shortcuts.sql +341 -0
- package/db/migrations/104_report_queries.sql +413 -0
- package/db/migrations/105_component_settings_groups.sql +367 -0
- package/db/migrations/106_form_designer.sql +102 -0
- package/db/migrations/107_app_version.sql +9 -0
- package/docs/apex-feature-parity.md +15 -12
- package/docs/guide/02-concepts.md +24 -0
- package/docs/guide/03-builder.md +69 -13
- package/docs/guide/04-pages-and-regions.md +84 -3
- package/docs/guide/05-items.md +14 -2
- package/docs/guide/06-processing.md +22 -0
- package/docs/guide/09-reference.md +14 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +8 -2
- package/docs/guide/16-files.md +23 -0
- package/docs/guide/18-cli.md +4 -0
- package/examples/hr/hr_61_form_designer.sql +80 -0
- package/package.json +1 -1
- package/public/app.css +79 -2
- package/public/app.js +307 -35
- package/public/builder.css +21 -1
- package/public/builder.js +67 -0
- package/public/sw.js +2 -1
- package/src/app.ts +9 -1
- package/src/appfiles.ts +18 -2
- package/src/builder/advisor.ts +7 -2
- package/src/builder/appwizard.ts +22 -6
- package/src/builder/arrange.ts +33 -12
- package/src/builder/code-editor.ts +7 -2
- package/src/builder/components.ts +86 -4
- package/src/builder/designer.ts +44 -17
- package/src/builder/errors.ts +65 -0
- package/src/builder/form-designer.ts +229 -0
- package/src/builder/forms.ts +1 -1
- package/src/builder/locks.ts +6 -0
- package/src/builder/newapp.ts +2 -1
- package/src/builder/object-browser.ts +81 -18
- package/src/builder/region-help.ts +2 -0
- package/src/builder/region-settings.ts +2 -0
- package/src/builder/reporter.ts +1 -1
- package/src/builder/routes.ts +7 -5
- package/src/builder/search.ts +3 -3
- package/src/builder/shared.ts +41 -8
- package/src/builder/subscriptions.ts +30 -4
- package/src/builder/ui.ts +2 -0
- package/src/cli/replace.ts +16 -2
- package/src/i18n/ar.ts +62 -0
- package/src/i18n/cs.ts +62 -0
- package/src/i18n/da.ts +62 -0
- package/src/i18n/de.ts +62 -0
- package/src/i18n/el.ts +62 -0
- package/src/i18n/es.ts +62 -0
- package/src/i18n/fi.ts +62 -0
- package/src/i18n/fr.ts +62 -0
- package/src/i18n/he.ts +62 -0
- package/src/i18n/it.ts +62 -0
- package/src/i18n/ja.ts +62 -0
- package/src/i18n/ko.ts +62 -0
- package/src/i18n/nb.ts +62 -0
- package/src/i18n/pl.ts +62 -0
- package/src/i18n/pt.ts +62 -0
- package/src/i18n/ru.ts +62 -0
- package/src/i18n/sv.ts +62 -0
- package/src/i18n/tr.ts +62 -0
- package/src/i18n/uk.ts +62 -0
- package/src/i18n/zh.ts +62 -0
- package/src/i18n.ts +124 -0
- package/src/metadata.ts +18 -3
- package/src/runtime/component-settings.ts +42 -0
- package/src/runtime/context.ts +4 -0
- package/src/runtime/devbar.ts +1 -1
- package/src/runtime/documents.ts +29 -3
- package/src/runtime/engine.ts +2 -2
- package/src/runtime/facet-state.ts +10 -5
- package/src/runtime/facets.ts +29 -5
- package/src/runtime/form-designer.ts +676 -0
- package/src/runtime/grid.ts +36 -11
- package/src/runtime/items.ts +47 -32
- package/src/runtime/lists.ts +1 -1
- package/src/runtime/logic.ts +12 -7
- package/src/runtime/pdf.ts +2 -1
- package/src/runtime/regions.ts +8 -3
- package/src/runtime/render.ts +8 -3
- package/src/runtime/report.ts +4 -3
- package/src/runtime/routes.ts +7 -3
- package/src/runtime/shortcuts.ts +43 -0
- package/src/runtime/smart-filters.ts +2 -1
- package/src/runtime/tasks.ts +6 -3
- package/src/security.ts +2 -1
- package/src/subscriptions.ts +67 -0
package/src/i18n/uk.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const uk: Record<MessageKey, string> = {
|
|
|
63
63
|
'common.back': "Назад",
|
|
64
64
|
'common.home': "На головну сторінку",
|
|
65
65
|
'common.skip': "Перейти до вмісту",
|
|
66
|
+
'common.back_to_top': "Угору",
|
|
66
67
|
'common.toggle_nav': "Показати або сховати навігацію",
|
|
67
68
|
'common.dismiss': "Закрити",
|
|
68
69
|
// messages and errors
|
|
@@ -390,6 +391,9 @@ export const uk: Record<MessageKey, string> = {
|
|
|
390
391
|
'facets.stars_up': "{n} зірки й вище",
|
|
391
392
|
'facets.star_up': "1 зірка й вище",
|
|
392
393
|
'facets.clear_short': "Очистити",
|
|
394
|
+
'facets.menu': "Параметри: {facet}",
|
|
395
|
+
'facets.sort_count': "Сортувати за кількістю",
|
|
396
|
+
'facets.sort_name': "Сортувати за назвою",
|
|
393
397
|
'facets.show_all': "Показати все ({n})",
|
|
394
398
|
'facets.total': "Усього рядків {n}",
|
|
395
399
|
'smart.placeholder': "Пошук або фільтр…",
|
|
@@ -685,6 +689,64 @@ export const uk: Record<MessageKey, string> = {
|
|
|
685
689
|
'reporter.changed': "змінено {date}",
|
|
686
690
|
'reporter.no_match': "Немає звітів, що відповідають пошуку.",
|
|
687
691
|
'reporter.chart_tag': "діаграма",
|
|
692
|
+
// Form Designer (form-designer.ts)
|
|
693
|
+
'formdesigner.no_sources': "Для цієї області ще не налаштовано таблиць.",
|
|
694
|
+
'formdesigner.no_forms': "Форм поки немає.",
|
|
695
|
+
'formdesigner.new': "Нова форма",
|
|
696
|
+
'formdesigner.create': "Створити",
|
|
697
|
+
'formdesigner.draft': "Чернетка",
|
|
698
|
+
'formdesigner.design': "Змінити форму",
|
|
699
|
+
'formdesigner.design_of': "Конструктор: {name}",
|
|
700
|
+
'formdesigner.back': "Усі форми",
|
|
701
|
+
'formdesigner.add': "Додати",
|
|
702
|
+
'formdesigner.edit_row': "Змінити",
|
|
703
|
+
'formdesigner.rows': "Рядки: {form}",
|
|
704
|
+
'formdesigner.save': "Зберегти",
|
|
705
|
+
'formdesigner.delete': "Видалити",
|
|
706
|
+
'formdesigner.delete_confirm': "Видалити цей рядок?",
|
|
707
|
+
'formdesigner.cancel': "Скасувати",
|
|
708
|
+
'formdesigner.saved_row': "Збережено.",
|
|
709
|
+
'formdesigner.deleted_row': "Видалено.",
|
|
710
|
+
'formdesigner.required': "Поле «{field}» обов’язкове.",
|
|
711
|
+
'formdesigner.not_in_list': "{field}: виберіть значення зі списку.",
|
|
712
|
+
'formdesigner.changed': "Хтось інший тим часом змінив цей рядок. Перевірте його й збережіть знову.",
|
|
713
|
+
'formdesigner.row_not_found': "Цього рядка немає, або вам не можна його бачити.",
|
|
714
|
+
'formdesigner.not_allowed': "З цим рядком так робити не можна.",
|
|
715
|
+
'formdesigner.form_not_found': "Цієї форми немає (більше).",
|
|
716
|
+
'formdesigner.unknown_source': "Таблиця цієї форми більше не пропонується.",
|
|
717
|
+
'formdesigner.nothing': "У цій формі ще немає полів.",
|
|
718
|
+
'formdesigner.choose': "- Виберіть -",
|
|
719
|
+
'formdesigner.name': "Назва",
|
|
720
|
+
'formdesigner.description': "Опис",
|
|
721
|
+
'formdesigner.published': "Опубліковано: форму може використовувати кожен, хто бачить цю сторінку",
|
|
722
|
+
'formdesigner.fields': "Поля",
|
|
723
|
+
'formdesigner.field_on': "У формі",
|
|
724
|
+
'formdesigner.column': "Стовпець",
|
|
725
|
+
'formdesigner.key': "ключ",
|
|
726
|
+
'formdesigner.order': "Порядок",
|
|
727
|
+
'formdesigner.label': "Підпис",
|
|
728
|
+
'formdesigner.help': "Підказка",
|
|
729
|
+
'formdesigner.required_short': "Обов’язково",
|
|
730
|
+
'formdesigner.display': "Вигляд",
|
|
731
|
+
'formdesigner.default': "Типово",
|
|
732
|
+
'formdesigner.in_list': "У списку",
|
|
733
|
+
'formdesigner.display_auto': "Поле",
|
|
734
|
+
'formdesigner.display_textarea': "Багаторядкове поле",
|
|
735
|
+
'formdesigner.display_select': "Список вибору",
|
|
736
|
+
'formdesigner.display_radio': "Перемикачі",
|
|
737
|
+
'formdesigner.actions': "Користувачі можуть",
|
|
738
|
+
'formdesigner.allow_insert': "додавати рядки",
|
|
739
|
+
'formdesigner.allow_update': "змінювати рядки",
|
|
740
|
+
'formdesigner.allow_delete': "видаляти рядки",
|
|
741
|
+
'formdesigner.list_help': "Список рядків показує стовпці, позначені «У списку»; без них форма лише додає рядки.",
|
|
742
|
+
'formdesigner.save_form': "Зберегти форму",
|
|
743
|
+
'formdesigner.delete_form': "Видалити форму",
|
|
744
|
+
'formdesigner.delete_form_confirm': "Видалити форму «{name}»? Збережені нею рядки залишаться.",
|
|
745
|
+
'formdesigner.form_saved': "Форму «{name}» збережено.",
|
|
746
|
+
'formdesigner.form_deleted': "Форму видалено.",
|
|
747
|
+
'formdesigner.name_required': "Дайте формі назву.",
|
|
748
|
+
'formdesigner.no_fields': "Додайте до форми хоча б один стовпець.",
|
|
749
|
+
'formdesigner.changed_by': "змінив(ла) {user} {date}",
|
|
688
750
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
689
751
|
'assistant.conversation': "Розмова",
|
|
690
752
|
'assistant.you': "Ви",
|
package/src/i18n/zh.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const zh: Record<MessageKey, string> = {
|
|
|
63
63
|
'common.back': "返回",
|
|
64
64
|
'common.home': "转到首页",
|
|
65
65
|
'common.skip': "跳到内容",
|
|
66
|
+
'common.back_to_top': "返回顶部",
|
|
66
67
|
'common.toggle_nav': "显示或隐藏导航",
|
|
67
68
|
'common.dismiss': "关闭",
|
|
68
69
|
// messages and errors
|
|
@@ -390,6 +391,9 @@ export const zh: Record<MessageKey, string> = {
|
|
|
390
391
|
'facets.stars_up': "{n} 星及以上",
|
|
391
392
|
'facets.star_up': "1 星及以上",
|
|
392
393
|
'facets.clear_short': "清除",
|
|
394
|
+
'facets.menu': "{facet} 选项",
|
|
395
|
+
'facets.sort_count': "按数量排序",
|
|
396
|
+
'facets.sort_name': "按名称排序",
|
|
393
397
|
'facets.show_all': "显示全部 ({n})",
|
|
394
398
|
'facets.total': "总行数 {n}",
|
|
395
399
|
'smart.placeholder': "搜索或筛选…",
|
|
@@ -685,6 +689,64 @@ export const zh: Record<MessageKey, string> = {
|
|
|
685
689
|
'reporter.changed': "修改于 {date}",
|
|
686
690
|
'reporter.no_match': "没有与搜索匹配的报表。",
|
|
687
691
|
'reporter.chart_tag': "图表",
|
|
692
|
+
// Form Designer (form-designer.ts)
|
|
693
|
+
'formdesigner.no_sources': "此区域尚未设置任何表。",
|
|
694
|
+
'formdesigner.no_forms': "还没有表单。",
|
|
695
|
+
'formdesigner.new': "新建表单",
|
|
696
|
+
'formdesigner.create': "设计",
|
|
697
|
+
'formdesigner.draft': "草稿",
|
|
698
|
+
'formdesigner.design': "设计表单",
|
|
699
|
+
'formdesigner.design_of': "设计 {name}",
|
|
700
|
+
'formdesigner.back': "所有表单",
|
|
701
|
+
'formdesigner.add': "添加",
|
|
702
|
+
'formdesigner.edit_row': "编辑",
|
|
703
|
+
'formdesigner.rows': "{form} 的行",
|
|
704
|
+
'formdesigner.save': "保存",
|
|
705
|
+
'formdesigner.delete': "删除",
|
|
706
|
+
'formdesigner.delete_confirm': "删除这一行?",
|
|
707
|
+
'formdesigner.cancel': "取消",
|
|
708
|
+
'formdesigner.saved_row': "已保存。",
|
|
709
|
+
'formdesigner.deleted_row': "已删除。",
|
|
710
|
+
'formdesigner.required': "{field} 为必填项。",
|
|
711
|
+
'formdesigner.not_in_list': "{field}:请从列表中选择一个值。",
|
|
712
|
+
'formdesigner.changed': "在此期间其他人修改了这一行。请检查后重新保存。",
|
|
713
|
+
'formdesigner.row_not_found': "这一行不存在,或者你无权查看。",
|
|
714
|
+
'formdesigner.not_allowed': "你不能对这一行执行此操作。",
|
|
715
|
+
'formdesigner.form_not_found': "此表单不存在(或已不存在)。",
|
|
716
|
+
'formdesigner.unknown_source': "此表单的表已不再提供。",
|
|
717
|
+
'formdesigner.nothing': "此表单还没有字段。",
|
|
718
|
+
'formdesigner.choose': "- 请选择 -",
|
|
719
|
+
'formdesigner.name': "名称",
|
|
720
|
+
'formdesigner.description': "说明",
|
|
721
|
+
'formdesigner.published': "已发布:能看到此页面的每个人都可以使用此表单",
|
|
722
|
+
'formdesigner.fields': "字段",
|
|
723
|
+
'formdesigner.field_on': "在表单上",
|
|
724
|
+
'formdesigner.column': "列",
|
|
725
|
+
'formdesigner.key': "键",
|
|
726
|
+
'formdesigner.order': "顺序",
|
|
727
|
+
'formdesigner.label': "标签",
|
|
728
|
+
'formdesigner.help': "帮助文本",
|
|
729
|
+
'formdesigner.required_short': "必填",
|
|
730
|
+
'formdesigner.display': "显示方式",
|
|
731
|
+
'formdesigner.default': "默认值",
|
|
732
|
+
'formdesigner.in_list': "在列表中",
|
|
733
|
+
'formdesigner.display_auto': "字段",
|
|
734
|
+
'formdesigner.display_textarea': "文本区域",
|
|
735
|
+
'formdesigner.display_select': "选择列表",
|
|
736
|
+
'formdesigner.display_radio': "单选组",
|
|
737
|
+
'formdesigner.actions': "用户可以",
|
|
738
|
+
'formdesigner.allow_insert': "添加行",
|
|
739
|
+
'formdesigner.allow_update': "修改行",
|
|
740
|
+
'formdesigner.allow_delete': "删除行",
|
|
741
|
+
'formdesigner.list_help': "行列表显示勾选了“在列表中”的列;没有勾选时,表单只添加行。",
|
|
742
|
+
'formdesigner.save_form': "保存表单",
|
|
743
|
+
'formdesigner.delete_form': "删除表单",
|
|
744
|
+
'formdesigner.delete_form_confirm': "删除表单“{name}”?用它保存的行会保留。",
|
|
745
|
+
'formdesigner.form_saved': "已保存表单“{name}”。",
|
|
746
|
+
'formdesigner.form_deleted': "已删除表单。",
|
|
747
|
+
'formdesigner.name_required': "请为表单命名。",
|
|
748
|
+
'formdesigner.no_fields': "请在表单上至少放一列。",
|
|
749
|
+
'formdesigner.changed_by': "{user} 于 {date} 修改",
|
|
688
750
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
689
751
|
'assistant.conversation': "对话",
|
|
690
752
|
'assistant.you': "您",
|
package/src/i18n.ts
CHANGED
|
@@ -90,6 +90,7 @@ const en = {
|
|
|
90
90
|
'common.back': 'Go back',
|
|
91
91
|
'common.home': 'Go to the home page',
|
|
92
92
|
'common.skip': 'Skip to content',
|
|
93
|
+
'common.back_to_top': "Back to top",
|
|
93
94
|
'common.toggle_nav': 'Toggle navigation',
|
|
94
95
|
'common.dismiss': 'Dismiss',
|
|
95
96
|
// messages and errors
|
|
@@ -417,6 +418,9 @@ const en = {
|
|
|
417
418
|
'facets.stars_up': '{n} stars and up',
|
|
418
419
|
'facets.star_up': '1 star and up',
|
|
419
420
|
'facets.clear_short': 'Clear',
|
|
421
|
+
'facets.menu': 'Options of {facet}',
|
|
422
|
+
'facets.sort_count': 'Sort by count',
|
|
423
|
+
'facets.sort_name': 'Sort by name',
|
|
420
424
|
'facets.show_all': 'Show all ({n})',
|
|
421
425
|
'facets.total': 'Total row count {n}',
|
|
422
426
|
'smart.placeholder': 'Search or filter…',
|
|
@@ -712,6 +716,64 @@ const en = {
|
|
|
712
716
|
'reporter.changed': "changed {date}",
|
|
713
717
|
'reporter.no_match': "No reports match the search.",
|
|
714
718
|
'reporter.chart_tag': "chart",
|
|
719
|
+
// Form Designer (form-designer.ts)
|
|
720
|
+
'formdesigner.no_sources': "No tables are set up for this region yet.",
|
|
721
|
+
'formdesigner.no_forms': "There are no forms yet.",
|
|
722
|
+
'formdesigner.new': "New form",
|
|
723
|
+
'formdesigner.create': "Design",
|
|
724
|
+
'formdesigner.draft': "Draft",
|
|
725
|
+
'formdesigner.design': "Design form",
|
|
726
|
+
'formdesigner.design_of': "Design of {name}",
|
|
727
|
+
'formdesigner.back': "All forms",
|
|
728
|
+
'formdesigner.add': "Add",
|
|
729
|
+
'formdesigner.edit_row': "Edit",
|
|
730
|
+
'formdesigner.rows': "Rows of {form}",
|
|
731
|
+
'formdesigner.save': "Save",
|
|
732
|
+
'formdesigner.delete': "Delete",
|
|
733
|
+
'formdesigner.delete_confirm': "Delete this row?",
|
|
734
|
+
'formdesigner.cancel': "Cancel",
|
|
735
|
+
'formdesigner.saved_row': "Saved.",
|
|
736
|
+
'formdesigner.deleted_row': "Deleted.",
|
|
737
|
+
'formdesigner.required': "{field} is required.",
|
|
738
|
+
'formdesigner.not_in_list': "{field}: choose a value from the list.",
|
|
739
|
+
'formdesigner.changed': "Someone else changed this row in the meantime. Check it and save again.",
|
|
740
|
+
'formdesigner.row_not_found': "This row does not exist, or you may not see it.",
|
|
741
|
+
'formdesigner.not_allowed': "You may not do that with this row.",
|
|
742
|
+
'formdesigner.form_not_found': "This form does not exist (any more).",
|
|
743
|
+
'formdesigner.unknown_source': "This form's table is no longer offered.",
|
|
744
|
+
'formdesigner.nothing': "This form has no fields yet.",
|
|
745
|
+
'formdesigner.choose': "- Choose -",
|
|
746
|
+
'formdesigner.name': "Name",
|
|
747
|
+
'formdesigner.description': "Description",
|
|
748
|
+
'formdesigner.published': "Published: everyone who can see this page may use the form",
|
|
749
|
+
'formdesigner.fields': "Fields",
|
|
750
|
+
'formdesigner.field_on': "On the form",
|
|
751
|
+
'formdesigner.column': "Column",
|
|
752
|
+
'formdesigner.key': "key",
|
|
753
|
+
'formdesigner.order': "Order",
|
|
754
|
+
'formdesigner.label': "Label",
|
|
755
|
+
'formdesigner.help': "Help text",
|
|
756
|
+
'formdesigner.required_short': "Required",
|
|
757
|
+
'formdesigner.display': "Display",
|
|
758
|
+
'formdesigner.default': "Default",
|
|
759
|
+
'formdesigner.in_list': "In the list",
|
|
760
|
+
'formdesigner.display_auto': "Field",
|
|
761
|
+
'formdesigner.display_textarea': "Text area",
|
|
762
|
+
'formdesigner.display_select': "Select list",
|
|
763
|
+
'formdesigner.display_radio': "Radio group",
|
|
764
|
+
'formdesigner.actions': "Users may",
|
|
765
|
+
'formdesigner.allow_insert': "add rows",
|
|
766
|
+
'formdesigner.allow_update': "change rows",
|
|
767
|
+
'formdesigner.allow_delete': "delete rows",
|
|
768
|
+
'formdesigner.list_help': "The list of rows shows the columns ticked \"In the list\"; without any, the form only adds rows.",
|
|
769
|
+
'formdesigner.save_form': "Save form",
|
|
770
|
+
'formdesigner.delete_form': "Delete form",
|
|
771
|
+
'formdesigner.delete_form_confirm': "Delete the form \"{name}\"? The rows it saved stay.",
|
|
772
|
+
'formdesigner.form_saved': "Form \"{name}\" saved.",
|
|
773
|
+
'formdesigner.form_deleted': "Form deleted.",
|
|
774
|
+
'formdesigner.name_required': "Give the form a name.",
|
|
775
|
+
'formdesigner.no_fields': "Put at least one column on the form.",
|
|
776
|
+
'formdesigner.changed_by': "changed by {user} on {date}",
|
|
715
777
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
716
778
|
'assistant.conversation': "Conversation",
|
|
717
779
|
'assistant.you': "You",
|
|
@@ -799,6 +861,7 @@ const nl: Record<MessageKey, string> = {
|
|
|
799
861
|
'common.back': 'Terug',
|
|
800
862
|
'common.home': 'Naar de startpagina',
|
|
801
863
|
'common.skip': 'Naar de inhoud',
|
|
864
|
+
'common.back_to_top': "Naar boven",
|
|
802
865
|
'common.toggle_nav': 'Navigatie tonen of verbergen',
|
|
803
866
|
'common.dismiss': 'Sluiten',
|
|
804
867
|
'error.not_found': 'Niet gevonden',
|
|
@@ -1123,6 +1186,9 @@ const nl: Record<MessageKey, string> = {
|
|
|
1123
1186
|
'facets.stars_up': '{n} sterren of meer',
|
|
1124
1187
|
'facets.star_up': '1 ster of meer',
|
|
1125
1188
|
'facets.clear_short': 'Wissen',
|
|
1189
|
+
'facets.menu': 'Opties van {facet}',
|
|
1190
|
+
'facets.sort_count': 'Sorteren op aantal',
|
|
1191
|
+
'facets.sort_name': 'Sorteren op naam',
|
|
1126
1192
|
'facets.show_all': 'Alle tonen ({n})',
|
|
1127
1193
|
'facets.total': 'Totaal aantal rijen {n}',
|
|
1128
1194
|
'smart.placeholder': 'Zoeken of filteren…',
|
|
@@ -1417,6 +1483,64 @@ const nl: Record<MessageKey, string> = {
|
|
|
1417
1483
|
'reporter.changed': "gewijzigd {date}",
|
|
1418
1484
|
'reporter.no_match': "Geen rapporten gevonden.",
|
|
1419
1485
|
'reporter.chart_tag': "grafiek",
|
|
1486
|
+
// Form Designer (form-designer.ts)
|
|
1487
|
+
'formdesigner.no_sources': "Voor deze regio zijn nog geen tabellen ingesteld.",
|
|
1488
|
+
'formdesigner.no_forms': "Er zijn nog geen formulieren.",
|
|
1489
|
+
'formdesigner.new': "Nieuw formulier",
|
|
1490
|
+
'formdesigner.create': "Ontwerpen",
|
|
1491
|
+
'formdesigner.draft': "Concept",
|
|
1492
|
+
'formdesigner.design': "Formulier ontwerpen",
|
|
1493
|
+
'formdesigner.design_of': "Ontwerp van {name}",
|
|
1494
|
+
'formdesigner.back': "Alle formulieren",
|
|
1495
|
+
'formdesigner.add': "Toevoegen",
|
|
1496
|
+
'formdesigner.edit_row': "Bewerken",
|
|
1497
|
+
'formdesigner.rows': "Rijen van {form}",
|
|
1498
|
+
'formdesigner.save': "Opslaan",
|
|
1499
|
+
'formdesigner.delete': "Verwijderen",
|
|
1500
|
+
'formdesigner.delete_confirm': "Deze rij verwijderen?",
|
|
1501
|
+
'formdesigner.cancel': "Annuleren",
|
|
1502
|
+
'formdesigner.saved_row': "Opgeslagen.",
|
|
1503
|
+
'formdesigner.deleted_row': "Verwijderd.",
|
|
1504
|
+
'formdesigner.required': "{field} is verplicht.",
|
|
1505
|
+
'formdesigner.not_in_list': "{field}: kies een waarde uit de lijst.",
|
|
1506
|
+
'formdesigner.changed': "Iemand anders heeft deze rij intussen gewijzigd. Controleer haar en sla opnieuw op.",
|
|
1507
|
+
'formdesigner.row_not_found': "Deze rij bestaat niet, of je mag haar niet zien.",
|
|
1508
|
+
'formdesigner.not_allowed': "Dat mag je met deze rij niet doen.",
|
|
1509
|
+
'formdesigner.form_not_found': "Dit formulier bestaat niet (meer).",
|
|
1510
|
+
'formdesigner.unknown_source': "De tabel van dit formulier wordt niet meer aangeboden.",
|
|
1511
|
+
'formdesigner.nothing': "Dit formulier heeft nog geen velden.",
|
|
1512
|
+
'formdesigner.choose': "- Kies -",
|
|
1513
|
+
'formdesigner.name': "Naam",
|
|
1514
|
+
'formdesigner.description': "Omschrijving",
|
|
1515
|
+
'formdesigner.published': "Gepubliceerd: iedereen die deze pagina kan zien mag het formulier gebruiken",
|
|
1516
|
+
'formdesigner.fields': "Velden",
|
|
1517
|
+
'formdesigner.field_on': "Op het formulier",
|
|
1518
|
+
'formdesigner.column': "Kolom",
|
|
1519
|
+
'formdesigner.key': "sleutel",
|
|
1520
|
+
'formdesigner.order': "Volgorde",
|
|
1521
|
+
'formdesigner.label': "Label",
|
|
1522
|
+
'formdesigner.help': "Helptekst",
|
|
1523
|
+
'formdesigner.required_short': "Verplicht",
|
|
1524
|
+
'formdesigner.display': "Weergave",
|
|
1525
|
+
'formdesigner.default': "Standaard",
|
|
1526
|
+
'formdesigner.in_list': "In de lijst",
|
|
1527
|
+
'formdesigner.display_auto': "Veld",
|
|
1528
|
+
'formdesigner.display_textarea': "Tekstvak",
|
|
1529
|
+
'formdesigner.display_select': "Keuzelijst",
|
|
1530
|
+
'formdesigner.display_radio': "Keuzerondjes",
|
|
1531
|
+
'formdesigner.actions': "Gebruikers mogen",
|
|
1532
|
+
'formdesigner.allow_insert': "rijen toevoegen",
|
|
1533
|
+
'formdesigner.allow_update': "rijen wijzigen",
|
|
1534
|
+
'formdesigner.allow_delete': "rijen verwijderen",
|
|
1535
|
+
'formdesigner.list_help': "De lijst met rijen toont de kolommen met een vinkje bij \"In de lijst\"; zonder vinkjes voegt het formulier alleen rijen toe.",
|
|
1536
|
+
'formdesigner.save_form': "Formulier opslaan",
|
|
1537
|
+
'formdesigner.delete_form': "Formulier verwijderen",
|
|
1538
|
+
'formdesigner.delete_form_confirm': "Het formulier \"{name}\" verwijderen? De rijen die ermee zijn opgeslagen blijven bestaan.",
|
|
1539
|
+
'formdesigner.form_saved': "Formulier \"{name}\" opgeslagen.",
|
|
1540
|
+
'formdesigner.form_deleted': "Formulier verwijderd.",
|
|
1541
|
+
'formdesigner.name_required': "Geef het formulier een naam.",
|
|
1542
|
+
'formdesigner.no_fields': "Zet minstens één kolom op het formulier.",
|
|
1543
|
+
'formdesigner.changed_by': "gewijzigd door {user} op {date}",
|
|
1420
1544
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
1421
1545
|
'assistant.conversation': "Gesprek",
|
|
1422
1546
|
'assistant.you': "Jij",
|
package/src/metadata.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface App {
|
|
|
98
98
|
availability_users: string[];
|
|
99
99
|
/** (084) users can send feedback from the navigation bar */
|
|
100
100
|
feedback: boolean;
|
|
101
|
+
/** (107) the application's version (Release 1.0), shown in its pages' footer */
|
|
102
|
+
version: string | null;
|
|
101
103
|
/** (085) schema.function(jsonb) returns jsonb that rewrites error messages; seconds of the session timeout warning */
|
|
102
104
|
error_function: string | null;
|
|
103
105
|
session_warning_seconds: number;
|
|
@@ -106,6 +108,12 @@ export interface App {
|
|
|
106
108
|
authz_schemes: AuthzScheme[];
|
|
107
109
|
app_items: string[];
|
|
108
110
|
app_processes: AppProcess[];
|
|
111
|
+
/** (102) application computations: run for every page at their point, before the page's own */
|
|
112
|
+
app_computations: AppComputation[];
|
|
113
|
+
/** (105) defaults of the built-in component types (runtime/component-settings.ts) */
|
|
114
|
+
component_settings: import('./runtime/component-settings.ts').ComponentSettings;
|
|
115
|
+
/** (103) shortcuts: "NAME" in static content, help texts and confirm messages (runtime/shortcuts.ts) */
|
|
116
|
+
shortcuts?: { name: string; type: 'text' | 'html' | 'function_body'; value: string }[];
|
|
109
117
|
lovs: { name: string; query: string; rest_source?: string | null }[];
|
|
110
118
|
theme: {
|
|
111
119
|
accent?: string; header?: string; nav?: 'side' | 'top'; mode?: 'auto' | 'light' | 'dark'; user_choice?: boolean;
|
|
@@ -140,7 +148,7 @@ export interface Region {
|
|
|
140
148
|
title: string | null;
|
|
141
149
|
/** (0.31, 073) optional, unique on the page */
|
|
142
150
|
static_id?: string | null;
|
|
143
|
-
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search';
|
|
151
|
+
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer';
|
|
144
152
|
source: string | null;
|
|
145
153
|
table_name: string | null;
|
|
146
154
|
pk_column: string | null;
|
|
@@ -238,6 +246,11 @@ export interface Computation extends Condition {
|
|
|
238
246
|
authz: string | null;
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
/** (102) An application computation: like a page's, for every page of the application. */
|
|
250
|
+
export interface AppComputation extends Omit<Computation, 'point'> {
|
|
251
|
+
point: 'after_authentication' | 'before_header' | 'after_submit';
|
|
252
|
+
}
|
|
253
|
+
|
|
241
254
|
export interface Branch extends Condition {
|
|
242
255
|
id: number;
|
|
243
256
|
seq: number;
|
|
@@ -348,8 +361,8 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
|
|
|
348
361
|
// No caching on purpose: edits made in the builder show up on the next request.
|
|
349
362
|
export async function loadApp(alias: string) {
|
|
350
363
|
return runtime.one<App>(
|
|
351
|
-
`select a.id, a.alias, a.name, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
352
|
-
a.availability, a.availability_message, a.availability_users, a.feedback, a.error_function, a.session_warning_seconds,
|
|
364
|
+
`select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
365
|
+
a.availability, a.availability_message, a.availability_users, a.feedback, a.error_function, a.session_warning_seconds, a.component_settings,
|
|
353
366
|
a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
|
|
354
367
|
coalesce((select jsonb_agg(jsonb_build_object('name', l.name, 'query', l.query, 'rest_source', l.rest_source)) from meta.lov l where l.app_id = a.id), '[]') as lovs,
|
|
355
368
|
coalesce((select jsonb_agg(jsonb_build_object('page_no', p.page_no, 'name', p.name, 'title', p.title,
|
|
@@ -361,6 +374,8 @@ export async function loadApp(alias: string) {
|
|
|
361
374
|
from meta.authz_scheme s where s.app_id = a.id), '[]') as authz_schemes,
|
|
362
375
|
coalesce((select jsonb_agg(i.name) from meta.app_item i where i.app_id = a.id), '[]') as app_items,
|
|
363
376
|
${agg('meta.app_process', 'app_id', 'a', 'a.id')} as app_processes,
|
|
377
|
+
${agg('meta.app_computation', 'app_id', 'a', 'a.id')} as app_computations,
|
|
378
|
+
coalesce((select jsonb_agg(jsonb_build_object('name', s.name, 'type', s.type, 'value', s.value)) from meta.shortcut s where s.app_id = a.id), '[]') as shortcuts,
|
|
364
379
|
${includes('a.static_includes', 'a.id')} as static_includes,
|
|
365
380
|
coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'install_sql') from meta.plugin x where x.app_id = a.id), '[]') as plugins,
|
|
366
381
|
coalesce((select jsonb_agg(jsonb_build_object('name', f.name, 'v', floor(extract(epoch from f.updated_at))::bigint))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Component settings (migration 105; APEX: Shared Components → Component
|
|
2
|
+
// Settings): an application's defaults for its built-in component types, used
|
|
3
|
+
// where a component sets nothing itself. Every value is bounded here, so a
|
|
4
|
+
// hand-edited or imported setting can't make a page read without limits.
|
|
5
|
+
|
|
6
|
+
export interface ComponentSettings {
|
|
7
|
+
report_rows?: number;
|
|
8
|
+
grid_rows?: number;
|
|
9
|
+
popup_lov_rows?: number;
|
|
10
|
+
lov_max_rows?: number;
|
|
11
|
+
rating_max?: number;
|
|
12
|
+
}
|
|
13
|
+
export type ComponentSetting = keyof ComponentSettings;
|
|
14
|
+
|
|
15
|
+
export const COMPONENT_SETTINGS: { key: ComponentSetting; label: string; group: string; min: number; max: number; def: number; help: string }[] = [
|
|
16
|
+
{ key: 'report_rows', label: 'Rows per page', group: 'Reports', min: 1, max: 1000, def: 15, help: 'Of a classic or interactive report without its own page size (users can still choose).' },
|
|
17
|
+
{ key: 'grid_rows', label: 'Rows per page', group: 'Interactive grids', min: 1, max: 200, def: 25, help: 'Of an interactive grid without its own page size.' },
|
|
18
|
+
{ key: 'popup_lov_rows', label: 'Rows per page', group: 'Popup lists of values', min: 1, max: 100, def: 25, help: 'In a popup list of values\' dialog, for items without their own page size.' },
|
|
19
|
+
{ key: 'lov_max_rows', label: 'Maximum rows', group: 'Lists of values', min: 1, max: 50_000, def: 5000, help: 'Rows a select list, radio group, checkbox group or popup list reads, for items without their own maximum.' },
|
|
20
|
+
{ key: 'rating_max', label: 'Stars', group: 'Star ratings', min: 3, max: 10, def: 5, help: 'Of a star rating item without its own maximum.' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** The value of a setting: the application's (bounded), else the built-in default. */
|
|
24
|
+
export function componentSetting(settings: ComponentSettings | null | undefined, key: ComponentSetting): number {
|
|
25
|
+
const spec = COMPONENT_SETTINGS.find((s) => s.key === key)!;
|
|
26
|
+
const n = Math.floor(Number(settings?.[key]));
|
|
27
|
+
return Number.isFinite(n) && n >= spec.min && n <= spec.max ? n : spec.def;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The settings from the builder's form: whole numbers within bounds; empty leaves a setting at its default. */
|
|
31
|
+
export function componentSettingsFrom(form: Record<string, unknown>): { settings: ComponentSettings; problems: string[] } {
|
|
32
|
+
const settings: ComponentSettings = {};
|
|
33
|
+
const problems: string[] = [];
|
|
34
|
+
for (const s of COMPONENT_SETTINGS) {
|
|
35
|
+
const raw = String(form[s.key] ?? '').trim();
|
|
36
|
+
if (raw === '') continue;
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!Number.isInteger(n) || n < s.min || n > s.max) problems.push(`${s.group}, ${s.label}: a whole number from ${s.min} to ${s.max.toLocaleString('en')}.`);
|
|
39
|
+
else settings[s.key] = n;
|
|
40
|
+
}
|
|
41
|
+
return { settings, problems };
|
|
42
|
+
}
|
package/src/runtime/context.ts
CHANGED
|
@@ -66,6 +66,10 @@ export interface PageContext {
|
|
|
66
66
|
cacheRefresh?: boolean;
|
|
67
67
|
/** the lazy region the request renders (GET …/region/:id), not as a placeholder */
|
|
68
68
|
loadNow?: number;
|
|
69
|
+
/** (103) the values of the function-body shortcuts this request used */
|
|
70
|
+
shortcutValues?: Map<string, string>;
|
|
71
|
+
/** the whole page is rendered (renderPage), not one region on its own */
|
|
72
|
+
wholePage?: boolean;
|
|
69
73
|
/** a download process's file, sent instead of the page (processes.ts) */
|
|
70
74
|
download?: Download;
|
|
71
75
|
/** running a background chain (process-jobs.ts): no request, no browser */
|
package/src/runtime/devbar.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function devbarHtml(ctx: PageContext): Raw | '' {
|
|
|
97
97
|
<nav aria-label="Developer toolbar"><ul class="devbar-tools">
|
|
98
98
|
${tool(BUILDER, 'App Builder', 'home', 'Home')}
|
|
99
99
|
${tool(`${BUILDER}/apps/${ctx.app.id}`, `Application ${ctx.app.id}`, 'grid', `App ${ctx.app.id}`)}
|
|
100
|
-
${tool(`${BUILDER}/
|
|
100
|
+
${tool(`${BUILDER}/apps/${ctx.app.id}/pages/${page}`, `Edit page ${page}`, 'edit', `Page ${page}`)}
|
|
101
101
|
${tool(`${ctx.base}/${page}/devbar/session`, 'Session', 'database', 'Session')}
|
|
102
102
|
${menu(`Debug: ${level || appLevel ? 'on' : 'off'}`, 'bolt', `devbar-debug-menu${level || appLevel ? ' is-on' : ''}`, html`<form method="post" action="${ctx.base}/${page}/devbar/debug" class="devbar-debug">
|
|
103
103
|
<input type="hidden" name="__csrf" value="${ctx.session.csrf_token}">
|
package/src/runtime/documents.ts
CHANGED
|
@@ -4,21 +4,47 @@ import { isAuthorized, Forbidden } from './authz.ts';
|
|
|
4
4
|
import { bindValues, stripSemicolon, substitute, type PageContext } from './context.ts';
|
|
5
5
|
import { documentData, documentFilename, documentPdf, fillTemplate, type DocumentTemplate } from './document.ts';
|
|
6
6
|
import { layoutFor, layoutText } from './pdf.ts';
|
|
7
|
+
import { exportDownload, type ExportFormat } from './data-export.ts';
|
|
8
|
+
import type { Process } from '../metadata.ts';
|
|
7
9
|
|
|
8
10
|
// ?doc=NAME on a page: the document template filled with the page's session
|
|
9
|
-
// state (after the URL's checksummed item values), as the app's role
|
|
11
|
+
// state (after the URL's checksummed item values), as the app's role; (104)
|
|
12
|
+
// without a template of that name, the report query of it.
|
|
13
|
+
|
|
14
|
+
/** (104) A report query: a named query printed as PDF, Excel, CSV, JSON, XML or HTML. */
|
|
15
|
+
interface ReportQuery {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string | null;
|
|
18
|
+
query: string;
|
|
19
|
+
format: ExportFormat;
|
|
20
|
+
layout: string | null;
|
|
21
|
+
file_name: string | null;
|
|
22
|
+
title: string | null;
|
|
23
|
+
authz: string | null;
|
|
24
|
+
}
|
|
10
25
|
|
|
11
26
|
export async function renderDocument(ctx: PageContext, name: string) {
|
|
12
27
|
const c = ctx.client!;
|
|
13
28
|
// metadata through the runtime connection: the page's transaction already runs as the app's role
|
|
14
29
|
const tpl = await runtime.one<DocumentTemplate>('select * from meta.document_template where app_id = $1 and name = upper($2)', [ctx.app.id, name.slice(0, 100)]);
|
|
15
|
-
|
|
30
|
+
// (104) without a document template of that name, a report query of it
|
|
31
|
+
const rq = tpl ? null : await runtime.one<ReportQuery>('select * from meta.report_query where app_id = $1 and name = upper($2)', [ctx.app.id, name.slice(0, 100)]);
|
|
32
|
+
const found = tpl ?? rq;
|
|
33
|
+
if (!found || !(await isAuthorized(ctx, found.authz))) throw new Forbidden(ctx.locale.t('error.document_unavailable'));
|
|
16
34
|
// without signing in (a public page of an app that has sign-in) only the documents the page itself offers
|
|
17
35
|
// with a visible document button: ?doc= on any public page would otherwise hand out every template
|
|
18
36
|
if (ctx.app.authentication !== 'none' && !ctx.session.username) {
|
|
19
|
-
const offered = [...(ctx.vis?.buttons.values() ?? [])].some((b) => b.action === 'document' && (b.document ?? '').toUpperCase() ===
|
|
37
|
+
const offered = [...(ctx.vis?.buttons.values() ?? [])].some((b) => b.action === 'document' && (b.document ?? '').toUpperCase() === found.name);
|
|
20
38
|
if (!offered) throw new Forbidden(ctx.locale.t('error.document_unavailable'));
|
|
21
39
|
}
|
|
40
|
+
if (!tpl) {
|
|
41
|
+
const q = rq!;
|
|
42
|
+
const d = await exportDownload(ctx, { name: q.name, code: q.query } as Process, {
|
|
43
|
+
export: q.format, layout: q.layout ?? undefined, file_name: q.file_name ?? undefined, title: q.title ?? q.description ?? undefined,
|
|
44
|
+
});
|
|
45
|
+
// sent like a download process's file (sandboxed, never shown as a page of the application)
|
|
46
|
+
return { download: { ...d, inline: false } };
|
|
47
|
+
}
|
|
22
48
|
const now = new Date().toISOString();
|
|
23
49
|
const data = await savepoint(c, () =>
|
|
24
50
|
documentData(c, stripSemicolon(applyBinds(tpl.query, bindValues(ctx))), {
|
package/src/runtime/engine.ts
CHANGED
|
@@ -173,8 +173,8 @@ export async function validate(ctx: PageContext) {
|
|
|
173
173
|
const v = state[i.name];
|
|
174
174
|
if (!v || !vis.editable.has(i.name) || errors.items[i.name]) continue;
|
|
175
175
|
const label = i.label ?? i.name;
|
|
176
|
-
if (i.type === 'rating' && !(/^\d{1,2}$/.test(v) && Number(v) >= 1 && Number(v) <= ratingMax(i)))
|
|
177
|
-
fail(i.name, ctx.locale.t('error.rating', { label, max: ratingMax(i) }));
|
|
176
|
+
if (i.type === 'rating' && !(/^\d{1,2}$/.test(v) && Number(v) >= 1 && Number(v) <= ratingMax(i, ctx.app.component_settings)))
|
|
177
|
+
fail(i.name, ctx.locale.t('error.rating', { label, max: ratingMax(i, ctx.app.component_settings) }));
|
|
178
178
|
// a list item posts return values: each must be one its list of values returns (config.any_value: true allows others)
|
|
179
179
|
if (LOV_CHECKED.has(i.type) && i.lov && i.config?.any_value !== true && !(await lovContains(ctx, i.lov, MULTI_VALUE.has(i.type) ? splitValues(v) : [v])))
|
|
180
180
|
fail(i.name, ctx.locale.t('error.lov_value', { label }));
|
|
@@ -174,7 +174,10 @@ export const isFacetParam = (reportId: number, name: string) => Object.values(fa
|
|
|
174
174
|
* The facet filters on a report that its facets allow (except those on one
|
|
175
175
|
* column: a facet's own counts ignore its own selection).
|
|
176
176
|
*/
|
|
177
|
-
|
|
177
|
+
/** (sprint 84) Reads a custom range's bound typed in the application's date format as YYYY-MM-DD (null: not a date in it). */
|
|
178
|
+
export type DateReader = (text: string) => string | null;
|
|
179
|
+
|
|
180
|
+
export function facetFilters(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, except?: string, readDate?: DateReader): FacetFilter[] {
|
|
178
181
|
const k = facetKeys(reportId);
|
|
179
182
|
const out: FacetFilter[] = [];
|
|
180
183
|
for (const def of defs.values()) {
|
|
@@ -194,8 +197,10 @@ export function facetFilters(params: URLSearchParams, reportId: number, defs: Ma
|
|
|
194
197
|
const r = parseRange(chosen);
|
|
195
198
|
if (r && def.ranges.some((x) => x.from === r.from && x.to === r.to)) out.push({ column, kind: 'range', from: r.from, to: r.to, custom: false });
|
|
196
199
|
} else if (def.custom) {
|
|
197
|
-
|
|
198
|
-
const
|
|
200
|
+
// (sprint 84) a date bound in the date format is read as YYYY-MM-DD; a number stays as it is
|
|
201
|
+
const bound = (v: string) => (v && readDate && !/^-?\d+(\.\d+)?$/.test(v) ? (readDate(v) ?? v) : v);
|
|
202
|
+
const from = bound((params.get(k.from + column) ?? '').trim());
|
|
203
|
+
const to = bound((params.get(k.to + column) ?? '').trim());
|
|
199
204
|
if ((from || to) && anyBound(from) && anyBound(to)) out.push({ column, kind: 'range', from, to, custom: true });
|
|
200
205
|
}
|
|
201
206
|
}
|
|
@@ -235,8 +240,8 @@ export function rangeSql(c: string, oid: number | undefined, r: { from: string;
|
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
/** The conditions of all applicable filters. */
|
|
238
|
-
export function facetWhere(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, cols: Map<string, number>, p: SqlParams, except?: string): string[] {
|
|
239
|
-
return facetFilters(params, reportId, defs, except)
|
|
243
|
+
export function facetWhere(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, cols: Map<string, number>, p: SqlParams, except?: string, readDate?: DateReader): string[] {
|
|
244
|
+
return facetFilters(params, reportId, defs, except, readDate)
|
|
240
245
|
.map((f) => facetFilterSql(f, cols, p))
|
|
241
246
|
.filter((s): s is string => s !== null);
|
|
242
247
|
}
|