geonetwork-ui 2.11.0-dev.7f3cfba04 → 2.11.0-dev.840dcc932

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.
Files changed (39) hide show
  1. package/fesm2022/geonetwork-ui.mjs +233 -18
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +43 -16
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/api/metadata-converter/src/lib/fixtures/mel.records.dataset+featurecatalog.ts +83 -5
  7. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +3 -3
  8. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +15 -6
  9. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +20 -8
  10. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +131 -0
  11. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +2 -0
  12. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +78 -1
  13. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +11 -0
  14. package/src/libs/common/domain/src/lib/model/record/flags.model.ts +1 -0
  15. package/src/libs/common/domain/src/lib/model/record/index.ts +1 -0
  16. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -8
  17. package/src/libs/common/domain/src/lib/platform.service.interface.ts +1 -0
  18. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +19 -0
  19. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +3 -3
  20. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints/form-field-constraints.component.ts +3 -5
  21. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +2 -2
  22. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.css +0 -0
  23. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.html +18 -0
  24. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.ts +86 -0
  25. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +100 -88
  26. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +11 -4
  27. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +18 -4
  28. package/src/libs/feature/editor/src/lib/expressions.ts +67 -0
  29. package/src/libs/feature/editor/src/lib/fields.config.ts +13 -0
  30. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +10 -4
  31. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +1 -1
  32. package/translations/de.json +4 -0
  33. package/translations/en.json +4 -0
  34. package/translations/es.json +4 -0
  35. package/translations/fr.json +4 -0
  36. package/translations/it.json +4 -0
  37. package/translations/nl.json +4 -0
  38. package/translations/pt.json +4 -0
  39. package/translations/sk.json +4 -0
@@ -36,7 +36,7 @@ import {
36
36
  AssociatedRecord,
37
37
  DatasetFeatureAttribute,
38
38
  DatasetFeatureAttributeValue,
39
- DatasetFeatureCatalog,
39
+ DatasetFeatureType,
40
40
  Individual,
41
41
  LanguageCode,
42
42
  SourceRecord,
@@ -428,11 +428,13 @@ function extractListedValue(): ChainableFunction<
428
428
  return pipe(
429
429
  combine(
430
430
  pipe(findChildElement('gfc:code', false), extractCharacterString()),
431
- pipe(findChildElement('gfc:label', false), extractCharacterString())
431
+ pipe(findChildElement('gfc:label', false), extractCharacterString()),
432
+ pipe(findChildElement('gfc:definition', false), extractCharacterString())
432
433
  ),
433
- map(([code, label]) => ({
434
- ...(code && { code }),
434
+ map(([code, label, description]) => ({
435
+ value: code ?? '',
435
436
  ...(label && { label }),
437
+ ...(description && { description }),
436
438
  }))
437
439
  )
438
440
  }
@@ -447,6 +449,10 @@ function extractFeatureAttribute(): ChainableFunction<
447
449
  findChildElement('gfc:memberName', false),
448
450
  extractCharacterStringOrText()
449
451
  ),
452
+ pipe(
453
+ findChildElement('gfc:designation', false),
454
+ extractCharacterString()
455
+ ),
450
456
  pipe(findChildElement('gfc:definition', false), extractCharacterString()),
451
457
  pipe(findChildElement('gfc:code', false), extractCharacterString()),
452
458
  pipe(
@@ -456,11 +462,12 @@ function extractFeatureAttribute(): ChainableFunction<
456
462
  pipe(
457
463
  findNestedElements('gfc:listedValue', 'gfc:FC_ListedValue'),
458
464
  mapArray(extractListedValue()),
459
- filterArray((value) => !!(value.code || value.label))
465
+ filterArray((value) => !!(value.value || value.label))
460
466
  )
461
467
  ),
462
- map(([name, description, code, type, values]) => ({
468
+ map(([name, label, description, code, type, values]) => ({
463
469
  name: name ?? '',
470
+ ...(label && { label }),
464
471
  ...(description && { description }),
465
472
  ...(code && { code }),
466
473
  ...(type && { type }),
@@ -471,7 +478,7 @@ function extractFeatureAttribute(): ChainableFunction<
471
478
 
472
479
  export function readFeatureTypeDescriptions(
473
480
  rootEl: XmlElement
474
- ): DatasetFeatureCatalog['featureTypes'] {
481
+ ): Array<DatasetFeatureType> {
475
482
  return pipe(
476
483
  findNestedElements(
477
484
  'mdb:contentInfo',
@@ -488,6 +495,10 @@ export function readFeatureTypeDescriptions(
488
495
  findChildElement('gfc:typeName', false),
489
496
  extractCharacterStringOrText()
490
497
  ),
498
+ pipe(
499
+ findChildElement('gfc:designation', false),
500
+ extractCharacterString()
501
+ ),
491
502
  pipe(
492
503
  findChildElement('gfc:definition', false),
493
504
  extractCharacterString()
@@ -500,8 +511,9 @@ export function readFeatureTypeDescriptions(
500
511
  mapArray(extractFeatureAttribute())
501
512
  )
502
513
  ),
503
- map(([name, description, attributes]) => ({
514
+ map(([name, label, description, attributes]) => ({
504
515
  name: name ?? '',
516
+ ...(label && { label }),
505
517
  ...(description && { description }),
506
518
  attributes,
507
519
  }))
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  CatalogRecord,
3
+ DatasetFeatureAttribute,
4
+ DatasetFeatureAttributeValue,
5
+ DatasetFeatureType,
3
6
  DatasetRecord,
4
7
  Individual,
5
8
  LanguageCode,
@@ -616,3 +619,131 @@ export function writeAssociatedRecords(
616
619
  )
617
620
  )(rootEl)
618
621
  }
622
+
623
+ function createNilElement(name: string, nilReason: string) {
624
+ return pipe(createElement(name), writeAttribute('gco:nilReason', nilReason))
625
+ }
626
+
627
+ function createFeatureCatalogueShell() {
628
+ return pipe(
629
+ createNestedElement(
630
+ 'mdb:contentInfo',
631
+ 'mrc:MD_FeatureCatalogue',
632
+ 'mrc:featureCatalogue',
633
+ 'gfc:FC_FeatureCatalogue'
634
+ ),
635
+ // create mandatory elements which are not part of the model yet
636
+ appendChildren(
637
+ createNilElement('cat:name', 'missing'),
638
+ createNilElement('cat:scope', 'missing'),
639
+ createNilElement('cat:versionNumber', 'missing'),
640
+ createNilElement('cat:versionDate', 'missing'),
641
+ createNilElement('gfc:producer', 'missing')
642
+ )
643
+ )
644
+ }
645
+
646
+ function createListedValue(value: DatasetFeatureAttributeValue) {
647
+ return pipe(
648
+ createNestedElement('gfc:listedValue', 'gfc:FC_ListedValue'),
649
+ appendChildren(
650
+ value.label
651
+ ? pipe(createElement('gfc:label'), writeCharacterString(value.label))
652
+ : createNilElement('gfc:label', 'missing'),
653
+ value.value
654
+ ? pipe(createElement('gfc:code'), writeCharacterString(value.value))
655
+ : null,
656
+ value.description
657
+ ? pipe(
658
+ createElement('gfc:definition'),
659
+ writeCharacterString(value.description)
660
+ )
661
+ : null
662
+ )
663
+ )
664
+ }
665
+
666
+ function createFeatureAttribute(attribute: DatasetFeatureAttribute) {
667
+ return pipe(
668
+ createNestedElement(
669
+ 'gfc:carrierOfCharacteristics',
670
+ 'gfc:FC_FeatureAttribute'
671
+ ),
672
+ appendChildren(
673
+ pipe(createElement('gfc:memberName'), setTextContent(attribute.name)),
674
+ attribute.description
675
+ ? pipe(
676
+ createElement('gfc:definition'),
677
+ writeCharacterString(attribute.description)
678
+ )
679
+ : null,
680
+ createNilElement('gfc:cardinality', 'missing'),
681
+ attribute.label
682
+ ? pipe(
683
+ createElement('gfc:designation'),
684
+ writeCharacterString(attribute.label)
685
+ )
686
+ : null,
687
+ attribute.code
688
+ ? pipe(createElement('gfc:code'), writeCharacterString(attribute.code))
689
+ : null,
690
+ attribute.type
691
+ ? pipe(
692
+ createNestedElement('gfc:valueType', 'gco:TypeName', 'gco:aName'),
693
+ writeCharacterString(attribute.type)
694
+ )
695
+ : null,
696
+ ...(attribute.values ?? [])
697
+ .filter((value) => value.value || value.label)
698
+ .map(createListedValue)
699
+ )
700
+ )
701
+ }
702
+
703
+ function createFeatureType(featureType: DatasetFeatureType) {
704
+ return pipe(
705
+ createNestedElement('gfc:featureType', 'gfc:FC_FeatureType'),
706
+ appendChildren(
707
+ pipe(createElement('gfc:typeName'), setTextContent(featureType.name)),
708
+ featureType.description
709
+ ? pipe(
710
+ createElement('gfc:definition'),
711
+ writeCharacterString(featureType.description)
712
+ )
713
+ : null,
714
+ pipe(
715
+ createNestedElement('gfc:isAbstract', 'gco:Boolean'),
716
+ setTextContent('false')
717
+ ),
718
+ featureType.label
719
+ ? pipe(
720
+ createElement('gfc:designation'),
721
+ writeCharacterString(featureType.label)
722
+ )
723
+ : null,
724
+ ...featureType.attributes.map(createFeatureAttribute),
725
+ createNilElement('gfc:featureCatalogue', 'missing')
726
+ )
727
+ )
728
+ }
729
+
730
+ export function writeFeatureTypeDescriptions(
731
+ record: DatasetRecord,
732
+ rootEl: XmlElement
733
+ ) {
734
+ const featureTypes = record.featureTypeDescriptions ?? []
735
+ const catalogues = findNestedElements(
736
+ 'mdb:contentInfo',
737
+ 'mrc:MD_FeatureCatalogue',
738
+ 'mrc:featureCatalogue',
739
+ 'gfc:FC_FeatureCatalogue'
740
+ )(rootEl)
741
+ catalogues.forEach((catalogue) =>
742
+ removeChildrenByName('gfc:featureType')(catalogue)
743
+ )
744
+ if (!featureTypes.length) return
745
+
746
+ const catalogue =
747
+ catalogues[0] ?? appendChildTree(createFeatureCatalogueShell())(rootEl)
748
+ appendChildren(...featureTypes.map(createFeatureType))(catalogue)
749
+ }
@@ -423,6 +423,8 @@ export class Iso19139Converter extends BaseConverter<string> {
423
423
 
424
424
  if (record.kind === 'dataset') {
425
425
  fieldChanged('status') && this.writers['status'](record, rootEl)
426
+ fieldChanged('featureTypeDescriptions') &&
427
+ this.writers['featureTypeDescriptions'](record, rootEl)
426
428
  }
427
429
  if (record.kind === 'dataset' || record.kind === 'reuse') {
428
430
  fieldChanged('updateFrequency') &&
@@ -19,6 +19,7 @@ import {
19
19
  DatasetFeatureCatalog,
20
20
  LanguageCode,
21
21
  LinkedRecord,
22
+ RecordFlag,
22
23
  RecordRelation,
23
24
  } from '../../../../../../libs/common/domain/src/lib/model/record'
24
25
  import {
@@ -36,6 +37,7 @@ import {
36
37
  LanguagesApiService,
37
38
  RecordsApiService,
38
39
  SearchApiService,
40
+ TagsApiService,
39
41
  } from '../../../../../../libs/data-access/gn4/src'
40
42
  import {
41
43
  combineLatest,
@@ -48,7 +50,13 @@ import {
48
50
  switchMap,
49
51
  throwError,
50
52
  } from 'rxjs'
51
- import { catchError, map, tap } from 'rxjs/operators'
53
+ import {
54
+ catchError,
55
+ map,
56
+ shareReplay,
57
+ tap,
58
+ withLatestFrom,
59
+ } from 'rxjs/operators'
52
60
  import { lt } from 'semver'
53
61
  import { ElasticsearchService } from './elasticsearch'
54
62
  import { toLang2 } from '../../../../../../libs/util/i18n/src'
@@ -80,6 +88,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
80
88
  private platformService = inject(PlatformServiceInterface)
81
89
  private gn4LanguagesApi = inject(LanguagesApiService)
82
90
  private settingsService = inject(Gn4SettingsService)
91
+ private gn4TagsApi = inject(TagsApiService)
83
92
  private disableDraft = inject(DISABLE_DRAFT, { optional: true }) ?? false
84
93
  private defaultConverter = inject(DEFAULT_RECORD_CONVERTER)
85
94
 
@@ -675,4 +684,72 @@ export class Gn4Repository implements RecordsRepositoryInterface {
675
684
  private removeRecordFromLocalStorage(recordId: string): void {
676
685
  window.localStorage.removeItem(this.getLocalStorageKeyForRecord(recordId))
677
686
  }
687
+
688
+ /*
689
+ * FLAGS
690
+ * Note: in GN4 API, categories are named "tags"
691
+ */
692
+
693
+ categoriesIdentifierForFlags: Observable<Record<RecordFlag, number>> =
694
+ this.gn4TagsApi.getTags().pipe(
695
+ map((categories) =>
696
+ categories.reduce(
697
+ (acc, category) => {
698
+ if (category.name.startsWith('geonetwork-ui-flag:')) {
699
+ acc[
700
+ category.name.replace(/^geonetwork-ui-flag:/, '') as RecordFlag
701
+ ] = category.id
702
+ }
703
+ return acc
704
+ },
705
+ {} as Record<RecordFlag, number>
706
+ )
707
+ ),
708
+ shareReplay(1)
709
+ )
710
+
711
+ private getCategoryIdForFlag(
712
+ flagName: RecordFlag
713
+ ): Observable<number | null> {
714
+ return this.categoriesIdentifierForFlags.pipe(
715
+ map((categories) => {
716
+ const categoryId = categories[flagName]
717
+ if (categoryId === undefined) {
718
+ console.warn(
719
+ `The record flag ${flagName} is not created on GeoNetwork backend; please create a category with the name geonetwork-ui-flag:${flagName} for this to work.`
720
+ )
721
+ return null
722
+ }
723
+ return categoryId
724
+ })
725
+ )
726
+ }
727
+
728
+ setRecordFlag(
729
+ uniqueIdentifier: string,
730
+ flagName: RecordFlag,
731
+ value: boolean
732
+ ): Observable<void> {
733
+ return this.getCategoryIdForFlag(flagName).pipe(
734
+ switchMap((categoryId) => {
735
+ if (categoryId === null) return of()
736
+ return value
737
+ ? this.gn4RecordsApi.tagRecord(uniqueIdentifier, [categoryId], false)
738
+ : this.gn4RecordsApi.deleteTags(uniqueIdentifier, [categoryId])
739
+ })
740
+ )
741
+ }
742
+
743
+ getRecordFlag(
744
+ uniqueIdentifier: string,
745
+ flagName: RecordFlag
746
+ ): Observable<boolean> {
747
+ return this.gn4RecordsApi.getRecordTags(uniqueIdentifier).pipe(
748
+ withLatestFrom(this.getCategoryIdForFlag(flagName)),
749
+ map(([tags, categoryId]) => {
750
+ if (categoryId === null) return false
751
+ return Array.from(tags).some((tag) => tag.id === categoryId)
752
+ })
753
+ )
754
+ }
678
755
  }
@@ -92,6 +92,13 @@ export class Gn4PlatformService implements PlatformServiceInterface {
92
92
  switchMap((apiUser) => this.mapper.userFromMeApi(apiUser)),
93
93
  shareReplay({ bufferSize: 1, refCount: true })
94
94
  )
95
+ private isAdmin$ = this.disableAuth
96
+ ? of(false)
97
+ : of(true).pipe(
98
+ switchMap(() => this.meApi.getMe()),
99
+ map((apiUser) => !!apiUser?.admin),
100
+ shareReplay({ bufferSize: 1, refCount: true })
101
+ )
95
102
 
96
103
  /**
97
104
  * A map of already loaded thesauri (groups of keywords); the key is a URI
@@ -146,6 +153,10 @@ export class Gn4PlatformService implements PlatformServiceInterface {
146
153
  return this.isUserAnonymous$
147
154
  }
148
155
 
156
+ isAdministrator(): Observable<boolean> {
157
+ return this.isAdmin$
158
+ }
159
+
149
160
  getOrganizations(): Observable<Organization[]> {
150
161
  return undefined
151
162
  }
@@ -0,0 +1 @@
1
+ export type RecordFlag = 'IS_REFERENCE_DATASET'
@@ -3,3 +3,4 @@ export * from './metadata.model'
3
3
  export * from './organization.model'
4
4
  export * from './translation.model'
5
5
  export * from './user-feedbacks.model'
6
+ export * from './flags.model'
@@ -316,7 +316,7 @@ export interface DatasetRecord extends BaseRecord {
316
316
  spatialExtents: Array<DatasetSpatialExtent>
317
317
  temporalExtents: Array<DatasetTemporalExtent>
318
318
  spatialRepresentation?: SpatialRepresentationType
319
- featureTypeDescriptions?: DatasetFeatureCatalog['featureTypes']
319
+ featureTypeDescriptions?: Array<DatasetFeatureType>
320
320
  }
321
321
 
322
322
  export interface ServiceEndpoint {
@@ -348,20 +348,23 @@ export interface ReuseRecord extends BaseRecord {
348
348
  temporalExtents: Array<DatasetTemporalExtent>
349
349
  }
350
350
 
351
+ export interface DatasetFeatureType {
352
+ name: string
353
+ label?: string
354
+ description?: string
355
+ attributes: DatasetFeatureAttribute[]
356
+ }
351
357
  export type DatasetFeatureCatalog = {
352
- featureTypes: Array<{
353
- name: string
354
- description?: string
355
- attributes: DatasetFeatureAttribute[]
356
- }>
358
+ featureTypes: Array<DatasetFeatureType>
357
359
  }
358
360
  export interface DatasetFeatureAttributeValue {
359
- code?: string
360
- description?: string
361
+ value: string // empty string when the value has no code
361
362
  label?: string
363
+ description?: string
362
364
  }
363
365
  export interface DatasetFeatureAttribute {
364
366
  name: string
367
+ label?: string
365
368
  description?: string
366
369
  type?: string
367
370
  code?: string
@@ -28,6 +28,7 @@ export abstract class PlatformServiceInterface {
28
28
  abstract supportsAuthentication(): boolean
29
29
  abstract getMe(): Observable<UserModel>
30
30
  abstract isAnonymous(): Observable<boolean>
31
+ abstract isAdministrator(): Observable<boolean>
31
32
  abstract getUsers(): Observable<UserModel[]>
32
33
  abstract getUserPermissionsByGroup(): Observable<GroupModel[]>
33
34
  abstract getUsersByOrganization(
@@ -11,6 +11,7 @@ import {
11
11
  DatasetFeatureCatalog,
12
12
  LanguageCode,
13
13
  LinkedRecord,
14
+ RecordFlag,
14
15
  } from '../model/record'
15
16
 
16
17
  export abstract class RecordsRepositoryInterface {
@@ -106,4 +107,22 @@ export abstract class RecordsRepositoryInterface {
106
107
  ): Observable<{ user: string; date: Date }>
107
108
  abstract getRecordPublicationStatus(uuid: string): Observable<boolean>
108
109
  abstract getApplicationLanguages(): Observable<LanguageCode[]>
110
+
111
+ /**
112
+ * Sets a given flag on a record. Flags are not part of the metadata document, which is why they are
113
+ * not part of the Record model.
114
+ */
115
+ abstract setRecordFlag(
116
+ uniqueIdentifier: string,
117
+ flagName: RecordFlag,
118
+ value: boolean
119
+ ): Observable<void>
120
+
121
+ /**
122
+ * Checks whether a given flag is enabled for a record.
123
+ */
124
+ abstract getRecordFlag(
125
+ uniqueIdentifier: string,
126
+ flagName: RecordFlag
127
+ ): Observable<boolean>
109
128
  }
@@ -182,9 +182,9 @@ As such, **it is not very interesting at all.**`,
182
182
  code: 'NOM',
183
183
  type: 'String (48)',
184
184
  values: [
185
- { code: 'Pomme', label: 'Les Pommiers' },
186
- { code: 'Cotton', label: 'Rue Cotton' },
187
- { code: "Passage de l'échiquier" },
185
+ { value: 'Pomme', label: 'Les Pommiers' },
186
+ { value: 'Cotton', label: 'Rue Cotton' },
187
+ { value: "Passage de l'échiquier" },
188
188
  ],
189
189
  },
190
190
  {
@@ -9,10 +9,7 @@ import {
9
9
 
10
10
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
11
11
  import { ConstraintCardComponent } from '../../../constraint-card/constraint-card.component'
12
- import {
13
- CatalogRecordKeys,
14
- Constraint,
15
- } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
12
+ import { Constraint } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
16
13
  import { ButtonComponent } from '../../../../../../../../../libs/ui/inputs/src'
17
14
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
18
15
  import { TranslatePipe } from '@ngx-translate/core'
@@ -22,6 +19,7 @@ import {
22
19
  provideNgIconsConfig,
23
20
  } from '@ng-icons/core'
24
21
  import { iconoirPlus } from '@ng-icons/iconoir'
22
+ import { FormFieldModel } from '../../../../models'
25
23
 
26
24
  marker('editor.record.form.constraint.add.legalConstraints')
27
25
  marker('editor.record.form.constraint.add.securityConstraints')
@@ -53,7 +51,7 @@ marker('editor.record.form.constraint.header.otherConstraints')
53
51
  export class FormFieldConstraintsComponent implements OnInit {
54
52
  @Input() label: string
55
53
  @Input() value: Constraint[]
56
- @Input() constraintType: CatalogRecordKeys
54
+ @Input() constraintType: FormFieldModel
57
55
  @Output() valueChange = new EventEmitter<Constraint[]>()
58
56
 
59
57
  constraintsHeader = ''
@@ -36,7 +36,7 @@ import {
36
36
  } from '../../../../../../../../../libs/util/shared/src'
37
37
  import { map } from 'rxjs/operators'
38
38
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
39
- import { FieldModelSpecifier } from '../../../../models/editor-config.model'
39
+ import { FormFieldModelSpecifier } from '../../../../models/editor-config.model'
40
40
 
41
41
  @Component({
42
42
  selector: 'gn-ui-form-field-contacts',
@@ -59,7 +59,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
59
59
  private changeDetectorRef = inject(ChangeDetectorRef)
60
60
 
61
61
  @Input() value: Individual[]
62
- @Input() modelSpecifier: FieldModelSpecifier
62
+ @Input() modelSpecifier: FormFieldModelSpecifier
63
63
  @Output() valueChange: EventEmitter<Individual[]> = new EventEmitter()
64
64
 
65
65
  contacts: Individual[] = []
@@ -0,0 +1,18 @@
1
+ <div class="flex-1 flex justify-between items-center">
2
+ <gn-ui-check-toggle
3
+ [label]="labelKey() | translate"
4
+ [value]="initialValue$ | async"
5
+ (toggled)="onToggle($event)"
6
+ [attr.data-cy]="'record-flag-toggle-' + flagName()"
7
+ ></gn-ui-check-toggle>
8
+
9
+ @if (hintKey()) {
10
+ <ng-icon
11
+ name="matHelpOutline"
12
+ size="16"
13
+ class="m-2"
14
+ [matTooltip]="hintKey() | translate"
15
+ matTooltipPosition="above"
16
+ ></ng-icon>
17
+ }
18
+ </div>
@@ -0,0 +1,86 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ inject,
5
+ input,
6
+ } from '@angular/core'
7
+ import { AsyncPipe } from '@angular/common'
8
+ import { CheckToggleComponent } from '../../../../../../../../../libs/ui/inputs/src'
9
+ import { EditorFacade } from '../../../../+state/editor.facade'
10
+ import { TranslatePipe, TranslateService } from '@ngx-translate/core'
11
+ import { RecordFlag } from '../../../../../../../../../libs/common/domain/src/lib/model'
12
+ import { RecordsRepositoryInterface } from '../../../../../../../../../libs/common/domain/src/lib/repository/records-repository.interface'
13
+ import { toSignal } from '@angular/core/rxjs-interop'
14
+ import { filter } from 'rxjs'
15
+ import { NgIcon, provideIcons } from '@ng-icons/core'
16
+ import { MatTooltip } from '@angular/material/tooltip'
17
+ import { map, switchMap } from 'rxjs/operators'
18
+ import { matHelpOutline } from '@ng-icons/material-icons/outline'
19
+ import { NotificationsService } from '../../../../../../../../../libs/feature/notifications/src'
20
+
21
+ @Component({
22
+ selector: 'gn-ui-form-field-record-flag',
23
+ standalone: true,
24
+ imports: [AsyncPipe, CheckToggleComponent, TranslatePipe, NgIcon, MatTooltip],
25
+ viewProviders: [
26
+ provideIcons({
27
+ matHelpOutline,
28
+ }),
29
+ ],
30
+ templateUrl: './form-field-record-flag-component.html',
31
+ styleUrls: ['./form-field-record-flag-component.css'],
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ })
34
+ export class FormFieldRecordFlagComponent {
35
+ flagName = input<RecordFlag>()
36
+ labelKey = input<string>()
37
+ hintKey = input<string>()
38
+
39
+ private editorFacade = inject(EditorFacade)
40
+ private repository = inject(RecordsRepositoryInterface)
41
+ private notificationsService = inject(NotificationsService)
42
+ private translateService = inject(TranslateService)
43
+
44
+ private record = toSignal(this.editorFacade.record$)
45
+
46
+ recordKind = toSignal(
47
+ this.editorFacade.record$.pipe(map((record) => record.kind))
48
+ )
49
+ initialValue$ = this.editorFacade.record$.pipe(
50
+ filter((record) => !!record),
51
+ switchMap((record) =>
52
+ this.repository.getRecordFlag(record.uniqueIdentifier, this.flagName())
53
+ )
54
+ )
55
+
56
+ onToggle(enabled: boolean) {
57
+ if (this.record() === null) {
58
+ return
59
+ }
60
+ this.repository
61
+ .setRecordFlag(this.record().uniqueIdentifier, this.flagName(), enabled)
62
+ .subscribe({
63
+ next: () => {
64
+ // do nothing
65
+ },
66
+ error: (error) => {
67
+ this.notificationsService.showNotification(
68
+ {
69
+ type: 'error',
70
+ title: this.translateService.instant(
71
+ 'editor.record.flag.saveError.title'
72
+ ),
73
+ text: `${this.translateService.instant(
74
+ 'editor.record.flag.saveError.body',
75
+ {
76
+ recordKind: this.recordKind(),
77
+ }
78
+ )} ${error.message}`,
79
+ },
80
+ undefined,
81
+ error
82
+ )
83
+ },
84
+ })
85
+ }
86
+ }