geonetwork-ui 2.10.0-dev.e97ea6950 → 2.10.0-dev.f1c00d5fe
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/fesm2022/geonetwork-ui.mjs +2558 -6985
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +299 -189
- package/index.d.ts.map +1 -1
- package/package.json +25 -35
- package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +20 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +4 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +23 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +8 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +4 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +23 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +4 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +9 -0
- package/src/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts +48 -7
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +6 -5
- package/src/libs/api/metadata-converter/src/lib/gn4/metadata-url.service.ts +2 -2
- package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +11 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +21 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +52 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +24 -1
- package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +73 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +79 -0
- package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +8 -4
- package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +2 -2
- package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +79 -41
- package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
- package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
- package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
- package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +37 -2
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +23 -5
- package/src/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts +2 -1
- package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
- package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
- package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
- package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -1
- package/src/libs/common/fixtures/src/lib/records.fixtures.ts +62 -0
- package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
- package/src/libs/feature/editor/src/index.ts +1 -0
- package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +10 -1
- package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +26 -20
- package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +15 -2
- package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
- package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
- package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +68 -0
- package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +41 -0
- package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html +5 -14
- package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.ts +3 -28
- package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
- package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
- package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +5 -5
- package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +2 -1
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +33 -34
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html +8 -7
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts +6 -6
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +1 -1
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.css +0 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.html +5 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.ts +89 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts +12 -5
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.html +4 -4
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.ts +7 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html +6 -2
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +3 -1
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +28 -16
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +16 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +18 -5
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +118 -5
- package/src/libs/feature/editor/src/lib/expressions.ts +380 -8
- package/src/libs/feature/editor/src/lib/fields.config.ts +354 -337
- package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +10 -3
- package/src/libs/feature/editor/src/lib/services/editor.service.ts +9 -13
- package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
- package/src/libs/feature/notify-reuse/src/index.ts +2 -0
- package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.css +0 -0
- package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.html +23 -0
- package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.ts +114 -0
- package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
- package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +111 -0
- package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +234 -0
- package/src/libs/feature/notify-reuse/src/lib/utils/url.ts +16 -0
- package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
- package/src/libs/feature/search/src/lib/results-table/results-table-container.component.ts +10 -5
- package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
- package/src/libs/ui/dataviz/src/index.ts +0 -1
- package/src/libs/ui/elements/src/index.ts +3 -0
- package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +88 -0
- package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
- package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +41 -0
- package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
- package/src/libs/ui/elements/src/lib/external-link-card/external-link-card.component.html +2 -1
- package/src/libs/ui/elements/src/lib/external-link-card/external-link-card.component.ts +2 -2
- package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
- package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
- package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
- package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
- package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
- package/src/libs/ui/elements/src/lib/keyword-badge/keyword-badge.component.html +14 -0
- package/src/libs/ui/elements/src/lib/keyword-badge/keyword-badge.component.ts +85 -0
- package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
- package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +3 -2
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +26 -68
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
- package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html +4 -5
- package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +4 -0
- package/src/libs/ui/inputs/src/lib/badge/badge.component.html +33 -26
- package/src/libs/ui/inputs/src/lib/badge/badge.component.ts +9 -0
- package/src/libs/ui/inputs/src/lib/button/button.component.ts +8 -0
- package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
- package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
- package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +3 -3
- package/src/libs/ui/inputs/src/lib/text-input/text-input.component.css +0 -3
- package/src/libs/ui/inputs/src/lib/text-input/text-input.component.html +11 -7
- package/src/libs/ui/inputs/src/lib/text-input/text-input.component.ts +56 -30
- package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
- package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
- package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.html +5 -5
- package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.ts +9 -1
- package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +1 -1
- package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts +3 -8
- package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.html +7 -7
- package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.ts +14 -1
- package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
- package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +15 -64
- package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
- package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
- package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
- package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html +12 -8
- package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts +3 -1
- package/src/libs/ui/search/src/lib/results-table/results-table.component.html +1 -0
- package/src/libs/ui/widgets/src/lib/popover/popover.component.ts +7 -1
- package/src/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.html +1 -1
- package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
- package/src/libs/util/app-config/src/lib/model.ts +7 -0
- package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
- package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
- package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
- package/src/libs/util/i18n/src/index.ts +0 -1
- package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
- package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
- package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
- package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
- package/src/libs/util/shared/src/lib/links/link-utils.ts +23 -1
- package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
- package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
- package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
- package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
- package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
- package/style.css +0 -1
- package/tailwind.base.css +26 -2
- package/translations/de.json +112 -50
- package/translations/en.json +117 -55
- package/translations/es.json +86 -24
- package/translations/fr.json +114 -52
- package/translations/it.json +113 -51
- package/translations/nl.json +85 -23
- package/translations/pt.json +86 -24
- package/translations/sk.json +86 -24
- package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.css +0 -3
package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Component,
|
|
5
5
|
ElementRef,
|
|
6
6
|
EventEmitter,
|
|
7
|
+
inject,
|
|
7
8
|
Input,
|
|
8
9
|
Output,
|
|
9
10
|
ViewChild,
|
|
@@ -17,11 +18,18 @@ import {
|
|
|
17
18
|
Individual,
|
|
18
19
|
Keyword,
|
|
19
20
|
OnlineResource,
|
|
21
|
+
RecordKind,
|
|
20
22
|
UpdateFrequency,
|
|
21
23
|
} from '../../../../../../../../libs/common/domain/src/lib/model/record'
|
|
22
24
|
import { FormFieldWrapperComponent } from '../../../../../../../../libs/ui/layout/src'
|
|
25
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core'
|
|
26
|
+
import {
|
|
27
|
+
matEditOutline,
|
|
28
|
+
matHelpOutline,
|
|
29
|
+
} from '@ng-icons/material-icons/outline'
|
|
23
30
|
import { TranslatePipe } from '@ngx-translate/core'
|
|
24
31
|
import {
|
|
32
|
+
FieldFocusDirective,
|
|
25
33
|
FormFieldDateComponent,
|
|
26
34
|
FormFieldLicenseComponent,
|
|
27
35
|
FormFieldTemporalExtentsComponent,
|
|
@@ -36,6 +44,7 @@ import { FormFieldContactsComponent } from './form-field-contacts/form-field-con
|
|
|
36
44
|
import { FormFieldKeywordsComponent } from './form-field-keywords/form-field-keywords.component'
|
|
37
45
|
import { FormFieldOnlineLinkResourcesComponent } from './form-field-online-link-resources/form-field-online-link-resources.component'
|
|
38
46
|
import { FormFieldOnlineResourcesComponent } from './form-field-online-resources/form-field-online-resources.component'
|
|
47
|
+
import { FormFieldOnlineSingleLinkResourceComponent } from './form-field-online-single-link-resource/form-field-online-single-link-resource.component'
|
|
39
48
|
import { FormFieldOverviewsComponent } from './form-field-overviews/form-field-overviews.component'
|
|
40
49
|
import { FormFieldRichComponent } from './form-field-rich/form-field-rich.component'
|
|
41
50
|
import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.component'
|
|
@@ -70,16 +79,21 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
|
|
|
70
79
|
FormFieldContactsForResourceComponent,
|
|
71
80
|
FormFieldOnlineResourcesComponent,
|
|
72
81
|
FormFieldOnlineLinkResourcesComponent,
|
|
82
|
+
FormFieldOnlineSingleLinkResourceComponent,
|
|
73
83
|
FormFieldContactsComponent,
|
|
74
84
|
FormFieldConstraintsComponent,
|
|
75
85
|
FormFieldConstraintsShortcutsComponent,
|
|
76
86
|
FormFieldSpatialToggleComponent,
|
|
77
87
|
FormFieldTopicsComponent,
|
|
78
88
|
TextFieldModule,
|
|
89
|
+
NgIconComponent,
|
|
79
90
|
],
|
|
91
|
+
providers: [provideIcons({ matEditOutline, matHelpOutline })],
|
|
92
|
+
hostDirectives: [FieldFocusDirective],
|
|
80
93
|
})
|
|
81
94
|
export class FormFieldComponent {
|
|
82
95
|
@Input() uniqueIdentifier: string
|
|
96
|
+
@Input() recordKind: RecordKind
|
|
83
97
|
@Input() model: CatalogRecordKeys
|
|
84
98
|
@Input() modelSpecifier: FieldModelSpecifier
|
|
85
99
|
@Input() componentName: FormFieldComponentName
|
|
@@ -92,6 +106,8 @@ export class FormFieldComponent {
|
|
|
92
106
|
@ViewChild('titleInput') titleInput: ElementRef
|
|
93
107
|
isOpenData = false
|
|
94
108
|
|
|
109
|
+
fieldFocus = inject(FieldFocusDirective)
|
|
110
|
+
|
|
95
111
|
toggleIsOpenData(event: boolean) {
|
|
96
112
|
this.isOpenData = event
|
|
97
113
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Glow applied by FieldFocusDirective (gnUiFieldFocus) to a focused field or,
|
|
2
|
+
when the field is hidden, to its whole section. */
|
|
3
|
+
.gn-ui-field-focus-glow {
|
|
4
|
+
position: relative;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.gn-ui-field-focus-glow::after {
|
|
8
|
+
content: '';
|
|
9
|
+
position: absolute;
|
|
10
|
+
inset: -8px;
|
|
11
|
+
border-radius: 8px;
|
|
12
|
+
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
animation: gn-ui-field-focus-glow 3s ease-out forwards;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.gn-ui-field-focus-glow::before {
|
|
18
|
+
content: '';
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset: -16px;
|
|
21
|
+
border: 2px dashed var(--color-primary);
|
|
22
|
+
border-radius: 10px;
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
animation: gn-ui-field-focus-glow 3s ease-out forwards;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@keyframes gn-ui-field-focus-glow {
|
|
28
|
+
0%,
|
|
29
|
+
55% {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
100% {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
section of facade.currentSections$ | async;
|
|
4
4
|
track sectionTracker($index, section)
|
|
5
5
|
) {
|
|
6
|
-
@if (
|
|
7
|
-
<div
|
|
6
|
+
@if (evaluateExpression(section.hidden)() !== true) {
|
|
7
|
+
<div
|
|
8
|
+
class="flex flex-col gap-6 border p-8 rounded-[8px] shadow scroll-mt-[90px] bg-white"
|
|
9
|
+
gnUiFieldFocus
|
|
10
|
+
#sectionFocus="fieldFocus"
|
|
11
|
+
>
|
|
8
12
|
<div class="flex flex-col gap-2">
|
|
9
13
|
@if (section.labelKey) {
|
|
10
|
-
<div
|
|
14
|
+
<div
|
|
15
|
+
class="text-2xl font-title text-black"
|
|
16
|
+
translate
|
|
17
|
+
[translateParams]="{ recordKind: recordKind$ | async }"
|
|
18
|
+
>
|
|
11
19
|
{{ section.labelKey }}
|
|
12
20
|
</div>
|
|
13
21
|
}
|
|
14
22
|
@if (section.descriptionKey) {
|
|
15
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
class="text-gray-800 text-sm"
|
|
25
|
+
translate
|
|
26
|
+
[translateParams]="{ recordKind: recordKind$ | async }"
|
|
27
|
+
>
|
|
16
28
|
{{ section.descriptionKey }}
|
|
17
29
|
</div>
|
|
18
30
|
}
|
|
@@ -22,7 +34,7 @@
|
|
|
22
34
|
field of section.fieldsWithValues;
|
|
23
35
|
track fieldTracker($index, field)
|
|
24
36
|
) {
|
|
25
|
-
@if (
|
|
37
|
+
@if (evaluateExpression(field.config.hidden)() !== true) {
|
|
26
38
|
<gn-ui-form-field
|
|
27
39
|
[ngClass]="
|
|
28
40
|
field.config.gridColumnSpan === 1
|
|
@@ -30,6 +42,7 @@
|
|
|
30
42
|
: 'col-span-2'
|
|
31
43
|
"
|
|
32
44
|
[uniqueIdentifier]="recordUniqueIdentifier$ | async"
|
|
45
|
+
[recordKind]="recordKind$ | async"
|
|
33
46
|
[model]="field.config.model!"
|
|
34
47
|
[modelSpecifier]="field.config.modelSpecifier!"
|
|
35
48
|
[config]="field.config.formFieldConfig"
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { CommonModule } from '@angular/common'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
computed,
|
|
6
|
+
inject,
|
|
7
|
+
OnDestroy,
|
|
8
|
+
OnInit,
|
|
9
|
+
Signal,
|
|
10
|
+
signal,
|
|
11
|
+
viewChildren,
|
|
12
|
+
} from '@angular/core'
|
|
3
13
|
import { EditorFacade } from '../../+state/editor.facade'
|
|
4
14
|
import { EditorFieldValue } from '../../models'
|
|
5
|
-
import { FormFieldComponent } from './form-field'
|
|
15
|
+
import { FieldFocusDirective, FormFieldComponent } from './form-field'
|
|
6
16
|
import { TranslateDirective } from '@ngx-translate/core'
|
|
7
17
|
import {
|
|
8
18
|
EditorFieldWithValue,
|
|
9
19
|
EditorSectionWithValues,
|
|
10
20
|
} from '../../+state/editor.models'
|
|
11
|
-
import { map } from 'rxjs'
|
|
21
|
+
import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
|
|
12
22
|
import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
23
|
+
import { switchMap } from 'rxjs/operators'
|
|
24
|
+
import { toSignal } from '@angular/core/rxjs-interop'
|
|
25
|
+
import { evaluate, isExpression } from '../../expressions'
|
|
26
|
+
import { NotificationsService } from '../../../../../../../libs/feature/notifications/src'
|
|
13
27
|
|
|
14
28
|
@Component({
|
|
15
29
|
selector: 'gn-ui-record-form',
|
|
@@ -17,14 +31,64 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
|
|
|
17
31
|
styleUrls: ['./record-form.component.css'],
|
|
18
32
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
33
|
standalone: true,
|
|
20
|
-
imports: [
|
|
34
|
+
imports: [
|
|
35
|
+
CommonModule,
|
|
36
|
+
FormFieldComponent,
|
|
37
|
+
FieldFocusDirective,
|
|
38
|
+
TranslateDirective,
|
|
39
|
+
],
|
|
21
40
|
})
|
|
22
|
-
export class RecordFormComponent {
|
|
41
|
+
export class RecordFormComponent implements OnInit, OnDestroy {
|
|
23
42
|
facade = inject(EditorFacade)
|
|
43
|
+
notifications = inject(NotificationsService)
|
|
44
|
+
subscription = new Subscription()
|
|
24
45
|
|
|
25
46
|
recordUniqueIdentifier$ = this.facade.record$.pipe(
|
|
26
47
|
map((record) => record.uniqueIdentifier)
|
|
27
48
|
)
|
|
49
|
+
recordKind$ = this.facade.record$.pipe(map((record) => record.kind))
|
|
50
|
+
|
|
51
|
+
focusFieldWithPage$ = this.facade.focusedField$.pipe(
|
|
52
|
+
switchMap(async (field) => {
|
|
53
|
+
const location = await this.getFieldLocation(field)
|
|
54
|
+
return [field, location?.page ?? null, location?.section ?? -1] as const
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
formFields = viewChildren(FormFieldComponent)
|
|
59
|
+
sectionFocusDirectives = viewChildren<FieldFocusDirective>('sectionFocus')
|
|
60
|
+
|
|
61
|
+
focusField(model: CatalogRecordKeys, sectionIndex: number) {
|
|
62
|
+
const field = this.formFields().find((f) => f.model === model)
|
|
63
|
+
if (field) {
|
|
64
|
+
field.fieldFocus.focusField()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
this.sectionFocusDirectives()[sectionIndex]?.focusField(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ngOnInit() {
|
|
71
|
+
this.subscription.add(
|
|
72
|
+
this.focusFieldWithPage$
|
|
73
|
+
.pipe(
|
|
74
|
+
withLatestFrom(
|
|
75
|
+
this.facade.currentPage$,
|
|
76
|
+
([field, fieldPage, fieldSection], currentPage) =>
|
|
77
|
+
[field, fieldPage, fieldSection, currentPage] as const
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
.subscribe(([field, fieldPage, fieldSection, currentPage]) => {
|
|
81
|
+
if (fieldPage !== null && fieldPage !== currentPage) {
|
|
82
|
+
this.facade.setCurrentPage(fieldPage)
|
|
83
|
+
}
|
|
84
|
+
setTimeout(() => this.focusField(field, fieldSection))
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ngOnDestroy() {
|
|
90
|
+
this.subscription.unsubscribe()
|
|
91
|
+
}
|
|
28
92
|
|
|
29
93
|
handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
|
|
30
94
|
if (!model) {
|
|
@@ -40,4 +104,53 @@ export class RecordFormComponent {
|
|
|
40
104
|
sectionTracker(index: number, section: EditorSectionWithValues) {
|
|
41
105
|
return section.labelKey
|
|
42
106
|
}
|
|
107
|
+
|
|
108
|
+
async getFieldLocation(
|
|
109
|
+
model: CatalogRecordKeys
|
|
110
|
+
): Promise<{ page: number; section: number } | null> {
|
|
111
|
+
const config = await firstValueFrom(this.facade.editorConfig$)
|
|
112
|
+
const page = config.pages.findIndex((p) =>
|
|
113
|
+
p.sections.some((s) => s.fields.some((f) => f.model === model))
|
|
114
|
+
)
|
|
115
|
+
if (page < 0) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
const section = config.pages[page].sections
|
|
119
|
+
.filter((s) => !s.hidden)
|
|
120
|
+
.findIndex((s) => s.fields.some((f) => f.model === model))
|
|
121
|
+
return { page, section }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private recordSignal = toSignal(this.facade.record$, { requireSync: true })
|
|
125
|
+
private expressionCache = new Map<string, Signal<EditorFieldValue>>()
|
|
126
|
+
|
|
127
|
+
// only compiles the expression into an evaluator the first time;
|
|
128
|
+
// then we store a computed() signal in the cache so that the evaluator is
|
|
129
|
+
// run again if the record changes
|
|
130
|
+
evaluateExpression(
|
|
131
|
+
expressionOrValue: string | EditorFieldValue
|
|
132
|
+
): Signal<EditorFieldValue> {
|
|
133
|
+
if (!isExpression(expressionOrValue)) {
|
|
134
|
+
return signal(expressionOrValue as EditorFieldValue)
|
|
135
|
+
}
|
|
136
|
+
const expression = expressionOrValue as string
|
|
137
|
+
if (this.expressionCache.has(expression)) {
|
|
138
|
+
return this.expressionCache.get(expression)
|
|
139
|
+
}
|
|
140
|
+
const { evaluator, errors } = evaluate(expression)
|
|
141
|
+
if (errors.length) {
|
|
142
|
+
console.error(`The following errors happened while evaluating the expression '${expression}':
|
|
143
|
+
${errors.map((err) => `> ${err}`).join('\n')}`)
|
|
144
|
+
this.notifications.showNotification({
|
|
145
|
+
type: 'warning',
|
|
146
|
+
title: 'Technical error',
|
|
147
|
+
text: 'An error happened while evaluating an expression inside the editor configuration; open the developer console for more information',
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
this.expressionCache.set(
|
|
151
|
+
expression,
|
|
152
|
+
computed(() => evaluator({ globals: { record: this.recordSignal() } }))
|
|
153
|
+
)
|
|
154
|
+
return this.expressionCache.get(expression)
|
|
155
|
+
}
|
|
43
156
|
}
|
|
@@ -1,16 +1,388 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EditorFieldValue } from './models/'
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface EvaluationContext {
|
|
4
|
+
globals: Record<string, object>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ExpressionEvaluator = (
|
|
8
|
+
context: EvaluationContext
|
|
9
|
+
) => EditorFieldValue
|
|
10
|
+
|
|
11
|
+
const TOKEN_FUNCTION_CALL_START = '('
|
|
12
|
+
const TOKEN_FUNCTION_CALL_END = ')'
|
|
13
|
+
const TOKEN_PROPERTY_ACCESS = '.'
|
|
14
|
+
const TOKEN_OPERATOR_TERNARY_FIRST = '?'
|
|
15
|
+
const TOKEN_OPERATOR_TERNARY_SECOND = ':'
|
|
16
|
+
const TOKEN_OPERATOR_LOGICAL = ['==', '!=']
|
|
17
|
+
const TOKEN_STRING_LITERAL = ["'", '"']
|
|
18
|
+
const TOKEN_GROUP_START = '('
|
|
19
|
+
const TOKEN_GROUP_END = ')'
|
|
20
|
+
|
|
21
|
+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence
|
|
22
|
+
const OPERATORS_BY_PRECEDENCE: string[][] = [
|
|
23
|
+
[TOKEN_OPERATOR_TERNARY_FIRST, TOKEN_OPERATOR_TERNARY_SECOND],
|
|
24
|
+
TOKEN_OPERATOR_LOGICAL,
|
|
25
|
+
]
|
|
26
|
+
const ALL_OPERATORS = OPERATORS_BY_PRECEDENCE.flat()
|
|
27
|
+
|
|
28
|
+
export interface CompilationContext {
|
|
29
|
+
errors: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isExpression(input: unknown): boolean {
|
|
33
|
+
return typeof input === 'string' && /^\${.*}$/.test(input)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readNext(input: string, start: number, length = 1): string {
|
|
37
|
+
return input.substring(start, start + length)
|
|
38
|
+
}
|
|
4
39
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Will continue looking until an operator is found at the same depth level
|
|
42
|
+
*/
|
|
43
|
+
function readUntilNextOperator(
|
|
44
|
+
input: string,
|
|
45
|
+
start: number,
|
|
46
|
+
currentOperator: string
|
|
47
|
+
): string {
|
|
48
|
+
let pos = start
|
|
49
|
+
let result = ''
|
|
50
|
+
let depth = 0
|
|
51
|
+
while (pos < input.length) {
|
|
52
|
+
const isOperator = ALL_OPERATORS.some(
|
|
53
|
+
(token) => readNext(input, pos, token.length) === token
|
|
54
|
+
)
|
|
55
|
+
if (isOperator && depth === 0) {
|
|
56
|
+
const operatorPrecedence = OPERATORS_BY_PRECEDENCE.findIndex((tokens) =>
|
|
57
|
+
tokens.some((token) => readNext(input, pos, token.length) === token)
|
|
58
|
+
)
|
|
59
|
+
const currentPrecedence =
|
|
60
|
+
OPERATORS_BY_PRECEDENCE.findIndex((tokens) =>
|
|
61
|
+
tokens.some((token) => currentOperator === token)
|
|
62
|
+
) ?? -1
|
|
63
|
+
// the new operator has a lower precedence than the current one: we stop here
|
|
64
|
+
if (operatorPrecedence < currentPrecedence) {
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (input[pos] === TOKEN_GROUP_START) {
|
|
69
|
+
depth++
|
|
70
|
+
} else if (input[pos] === TOKEN_GROUP_END) {
|
|
71
|
+
depth--
|
|
72
|
+
}
|
|
73
|
+
if (depth < 0) {
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
result += input[pos]
|
|
77
|
+
pos++
|
|
8
78
|
}
|
|
9
|
-
|
|
10
|
-
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Will look for a specific character or string situated at the same depth level!
|
|
84
|
+
* Returns null if nothing found
|
|
85
|
+
*/
|
|
86
|
+
function readUntil(input: string, start: number, char: string): string | null {
|
|
87
|
+
let pos = start
|
|
88
|
+
let result = ''
|
|
89
|
+
let depth = 0
|
|
90
|
+
while (pos < input.length) {
|
|
91
|
+
if (depth === 0 && readNext(input, pos, char.length) === char) {
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
if (input[pos] === TOKEN_GROUP_START) {
|
|
95
|
+
depth++
|
|
96
|
+
} else if (input[pos] === TOKEN_GROUP_END) {
|
|
97
|
+
depth--
|
|
98
|
+
}
|
|
99
|
+
if (depth < 0) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
result += input[pos]
|
|
103
|
+
pos++
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getFunctionEvaluator(
|
|
109
|
+
functionName: string,
|
|
110
|
+
context: CompilationContext
|
|
111
|
+
): ExpressionEvaluator {
|
|
112
|
+
switch (functionName) {
|
|
11
113
|
case 'dateNow':
|
|
12
114
|
return () => new Date()
|
|
13
115
|
default:
|
|
14
|
-
|
|
116
|
+
context.errors.push(`Unknown function: ${functionName}`)
|
|
117
|
+
return () => undefined
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* This evaluates an expression using a very light subset of Javascript.
|
|
123
|
+
*
|
|
124
|
+
* Things allowed in expressions are:
|
|
125
|
+
* - comparing values with == and !=
|
|
126
|
+
* - ternary comparisons with a ? b : c
|
|
127
|
+
* - grouping with (...)
|
|
128
|
+
* - functions: for now only dateNow() is supported and returns a timestamp
|
|
129
|
+
* - access to globals such as record.type
|
|
130
|
+
*
|
|
131
|
+
* Expressions are turned into an evaluator that can be called with a context containing the globals.
|
|
132
|
+
* Expressions can return any primitive Javascript type
|
|
133
|
+
* Valid expressions should always be enclosed in ${...}
|
|
134
|
+
*/
|
|
135
|
+
export function evaluate(expression: string): {
|
|
136
|
+
evaluator: ExpressionEvaluator
|
|
137
|
+
errors: string[]
|
|
138
|
+
} {
|
|
139
|
+
if (!/^\${.*}$/.test(expression)) {
|
|
140
|
+
throw new Error('Malformed expression: missing ${} enclosure')
|
|
141
|
+
}
|
|
142
|
+
const context = {
|
|
143
|
+
errors: [],
|
|
144
|
+
}
|
|
145
|
+
const trimmedExpression = expression.slice(2, -1)
|
|
146
|
+
return evaluateInternal(trimmedExpression, context)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function evaluateInternal(
|
|
150
|
+
expression: string,
|
|
151
|
+
currentContext: CompilationContext
|
|
152
|
+
): { evaluator: ExpressionEvaluator; errors: string[] } {
|
|
153
|
+
let pos = 0
|
|
154
|
+
let currentSymbol = null
|
|
155
|
+
let currentEvaluator: ExpressionEvaluator = () => undefined
|
|
156
|
+
|
|
157
|
+
while (pos < expression.length) {
|
|
158
|
+
const isWhitespace = /\s/.test(readNext(expression, pos))
|
|
159
|
+
if (isWhitespace) {
|
|
160
|
+
pos++
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// SYMBOL (NAME)
|
|
165
|
+
const isSymbol = /[a-zA-Z]/.test(readNext(expression, pos))
|
|
166
|
+
if (isSymbol) {
|
|
167
|
+
currentSymbol = ''
|
|
168
|
+
while (pos < expression.length) {
|
|
169
|
+
if (!/[a-zA-Z]/.test(readNext(expression, pos))) {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
currentSymbol += readNext(expression, pos)
|
|
173
|
+
pos++
|
|
174
|
+
}
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// FUNCTION CALL
|
|
179
|
+
const isFunctionCall =
|
|
180
|
+
currentSymbol && readNext(expression, pos) === TOKEN_FUNCTION_CALL_START
|
|
181
|
+
if (isFunctionCall) {
|
|
182
|
+
pos++
|
|
183
|
+
// no support for arguments for now (not needed)
|
|
184
|
+
if (readNext(expression, pos) !== TOKEN_FUNCTION_CALL_END) {
|
|
185
|
+
currentContext.errors.push(
|
|
186
|
+
`Expected token '${TOKEN_FUNCTION_CALL_END}' at position ${pos}, got '${readNext(expression, pos)}' instead`
|
|
187
|
+
)
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
pos++
|
|
191
|
+
currentEvaluator = getFunctionEvaluator(currentSymbol, currentContext)
|
|
192
|
+
currentSymbol = null
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// PROPERTY ACCESS (single level)
|
|
197
|
+
const isPropertyAccess =
|
|
198
|
+
currentSymbol && readNext(expression, pos) === TOKEN_PROPERTY_ACCESS
|
|
199
|
+
if (isPropertyAccess) {
|
|
200
|
+
pos++
|
|
201
|
+
if (!/[a-zA-Z]/.test(readNext(expression, pos))) {
|
|
202
|
+
currentContext.errors.push(
|
|
203
|
+
`Expected a property name after '${TOKEN_PROPERTY_ACCESS}' at position ${pos}, got '${readNext(expression, pos)}' instead`
|
|
204
|
+
)
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
let propertyName = ''
|
|
208
|
+
while (pos < expression.length) {
|
|
209
|
+
if (!/[a-zA-Z]/.test(readNext(expression, pos))) {
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
propertyName += readNext(expression, pos)
|
|
213
|
+
pos++
|
|
214
|
+
}
|
|
215
|
+
const variableName = currentSymbol
|
|
216
|
+
currentEvaluator = (context) => {
|
|
217
|
+
if (
|
|
218
|
+
!Object.prototype.hasOwnProperty.call(context.globals, variableName)
|
|
219
|
+
) {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
!Object.prototype.hasOwnProperty.call(
|
|
224
|
+
context.globals[variableName],
|
|
225
|
+
propertyName
|
|
226
|
+
)
|
|
227
|
+
) {
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
return context.globals[variableName][propertyName]
|
|
231
|
+
}
|
|
232
|
+
currentSymbol = null
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// we have a symbol but did not do either function call or property access => error
|
|
237
|
+
if (currentSymbol) {
|
|
238
|
+
currentContext.errors.push(
|
|
239
|
+
`Expected a function call or property access after symbol '${currentSymbol}' at position ${pos}, got '${readNext(expression, pos)}' instead`
|
|
240
|
+
)
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// GROUP OPERATOR
|
|
245
|
+
const isGroupStart = readNext(expression, pos) === TOKEN_GROUP_START
|
|
246
|
+
if (isGroupStart) {
|
|
247
|
+
pos++
|
|
248
|
+
const groupExpression = readUntil(expression, pos, TOKEN_GROUP_END)
|
|
249
|
+
if (groupExpression === null) {
|
|
250
|
+
currentContext.errors.push(
|
|
251
|
+
`Reached end of expression before finding expected token '${TOKEN_GROUP_END}'`
|
|
252
|
+
)
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
pos += groupExpression.length + 1
|
|
256
|
+
currentEvaluator = evaluateInternal(
|
|
257
|
+
groupExpression,
|
|
258
|
+
currentContext
|
|
259
|
+
).evaluator
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// LOGICAL OPERATORS
|
|
264
|
+
const isLogicalOperator = TOKEN_OPERATOR_LOGICAL.some(
|
|
265
|
+
(token) => readNext(expression, pos, token.length) === token
|
|
266
|
+
)
|
|
267
|
+
if (isLogicalOperator) {
|
|
268
|
+
const matchingOperator = TOKEN_OPERATOR_LOGICAL.find(
|
|
269
|
+
(token) => readNext(expression, pos, token.length) === token
|
|
270
|
+
)
|
|
271
|
+
const isEqual = matchingOperator.startsWith('=')
|
|
272
|
+
pos += matchingOperator.length
|
|
273
|
+
const comparedEvaluator = currentEvaluator
|
|
274
|
+
const comparedToExpression = readUntilNextOperator(
|
|
275
|
+
expression,
|
|
276
|
+
pos,
|
|
277
|
+
matchingOperator
|
|
278
|
+
)
|
|
279
|
+
pos += comparedToExpression.length
|
|
280
|
+
const comparedToEvaluator = evaluateInternal(
|
|
281
|
+
comparedToExpression,
|
|
282
|
+
currentContext
|
|
283
|
+
).evaluator
|
|
284
|
+
|
|
285
|
+
currentEvaluator = (context) => {
|
|
286
|
+
return (
|
|
287
|
+
(comparedEvaluator(context) == comparedToEvaluator(context)) ===
|
|
288
|
+
isEqual
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// TERNARY OPERATOR
|
|
295
|
+
const isTernary = readNext(expression, pos) === TOKEN_OPERATOR_TERNARY_FIRST
|
|
296
|
+
if (isTernary) {
|
|
297
|
+
if (!currentEvaluator) {
|
|
298
|
+
currentContext.errors.push(
|
|
299
|
+
`Expected an expression before '${TOKEN_OPERATOR_TERNARY_FIRST}' at position ${pos} but could not find any`
|
|
300
|
+
)
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
pos++
|
|
304
|
+
const ifTrueExpression = readUntil(
|
|
305
|
+
expression,
|
|
306
|
+
pos,
|
|
307
|
+
TOKEN_OPERATOR_TERNARY_SECOND
|
|
308
|
+
)
|
|
309
|
+
if (ifTrueExpression === null) {
|
|
310
|
+
currentContext.errors.push(
|
|
311
|
+
`Reached end of expression before finding expected token '${TOKEN_OPERATOR_TERNARY_SECOND}'`
|
|
312
|
+
)
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
pos += ifTrueExpression.length + 1
|
|
316
|
+
|
|
317
|
+
const ifFalseExpression = readUntilNextOperator(
|
|
318
|
+
expression,
|
|
319
|
+
pos,
|
|
320
|
+
TOKEN_OPERATOR_TERNARY_FIRST
|
|
321
|
+
)
|
|
322
|
+
pos += ifFalseExpression.length
|
|
323
|
+
|
|
324
|
+
const conditionEvaluator = currentEvaluator
|
|
325
|
+
const ifTrueEvaluator = evaluateInternal(
|
|
326
|
+
ifTrueExpression,
|
|
327
|
+
currentContext
|
|
328
|
+
).evaluator
|
|
329
|
+
const ifFalseEvaluator = evaluateInternal(
|
|
330
|
+
ifFalseExpression,
|
|
331
|
+
currentContext
|
|
332
|
+
).evaluator
|
|
333
|
+
currentEvaluator = (context) => {
|
|
334
|
+
if (conditionEvaluator(context)) {
|
|
335
|
+
return ifTrueEvaluator(context)
|
|
336
|
+
}
|
|
337
|
+
return ifFalseEvaluator(context)
|
|
338
|
+
}
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// STRING LITERAL
|
|
343
|
+
const isStringLiteral = TOKEN_STRING_LITERAL.some(
|
|
344
|
+
(token) => readNext(expression, pos) === token
|
|
345
|
+
)
|
|
346
|
+
if (isStringLiteral) {
|
|
347
|
+
const openingToken = readNext(expression, pos)
|
|
348
|
+
pos++
|
|
349
|
+
const literal = readUntil(expression, pos, openingToken)
|
|
350
|
+
if (literal === null) {
|
|
351
|
+
currentContext.errors.push(
|
|
352
|
+
`Reached end of expression before finding corresponding closing token ${openingToken} for string literal`
|
|
353
|
+
)
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
pos += literal.length + 1
|
|
357
|
+
currentEvaluator = () => literal
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// NUMBER LITERAL
|
|
362
|
+
const isNumberLiteral = /[0-9]/.test(readNext(expression, pos))
|
|
363
|
+
if (isNumberLiteral) {
|
|
364
|
+
let literal = ''
|
|
365
|
+
while (pos < expression.length) {
|
|
366
|
+
if (!/[0-9]/.test(readNext(expression, pos))) {
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
literal += readNext(expression, pos)
|
|
370
|
+
pos++
|
|
371
|
+
}
|
|
372
|
+
const numberLiteral = parseInt(literal, 10)
|
|
373
|
+
currentEvaluator = () => numberLiteral
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// could not figure out what that was: leave
|
|
378
|
+
currentContext.errors.push(
|
|
379
|
+
`Unexpected character '${readNext(expression, pos)}' at position ${pos}`
|
|
380
|
+
)
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
evaluator: currentEvaluator,
|
|
386
|
+
errors: currentContext.errors,
|
|
15
387
|
}
|
|
16
388
|
}
|