geonetwork-ui 2.10.0-dev.a66718398 → 2.10.0-dev.a9cc01fc7

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 (47) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1043 -907
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +121 -17
  4. package/index.d.ts.map +1 -1
  5. package/package.json +2 -2
  6. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +14 -2
  7. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  8. package/src/libs/common/domain/src/index.ts +1 -0
  9. package/src/libs/feature/editor/src/index.ts +2 -0
  10. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  11. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  12. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  13. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  14. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  15. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +6 -1
  16. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +16 -1
  17. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +34 -34
  18. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +8 -15
  19. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +6 -4
  20. 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
  21. 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
  22. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +3 -0
  23. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +1 -0
  24. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +57 -3
  25. package/src/libs/ui/elements/src/index.ts +1 -0
  26. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +16 -0
  27. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +26 -0
  28. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  29. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  30. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  31. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +27 -66
  32. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  33. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  34. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +4 -1
  35. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  36. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  37. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  38. package/src/libs/util/shared/src/lib/utils/user-display.ts +23 -0
  39. package/tailwind.base.css +11 -2
  40. package/translations/de.json +1 -0
  41. package/translations/en.json +1 -0
  42. package/translations/es.json +1 -0
  43. package/translations/fr.json +1 -0
  44. package/translations/it.json +1 -0
  45. package/translations/nl.json +1 -0
  46. package/translations/pt.json +1 -0
  47. package/translations/sk.json +1 -0
@@ -4,9 +4,10 @@ import * as EditorActions from './editor.actions'
4
4
  import * as EditorSelectors from './editor.selectors'
5
5
  import {
6
6
  CatalogRecord,
7
+ CatalogRecordKeys,
7
8
  LanguageCode,
8
9
  } from '../../../../../../libs/common/domain/src/lib/model/record'
9
- import { filter } from 'rxjs'
10
+ import { filter, map } from 'rxjs'
10
11
  import { Actions, ofType } from '@ngrx/effects'
11
12
  import { EditorConfig, EditorFieldIdentification } from '../models'
12
13
 
@@ -37,6 +38,10 @@ export class EditorFacade {
37
38
  )
38
39
  isPublished$ = this.store.pipe(select(EditorSelectors.selectIsPublished))
39
40
  canEditRecord$ = this.store.pipe(select(EditorSelectors.selectCanEditRecord))
41
+ focusedField$ = this.actions$.pipe(
42
+ ofType(EditorActions.setFocusedField),
43
+ map(({ model }) => model)
44
+ )
40
45
 
41
46
  openRecord(record: CatalogRecord, recordSource: string) {
42
47
  this.store.dispatch(
@@ -77,6 +82,10 @@ export class EditorFacade {
77
82
  this.store.dispatch(EditorActions.setCurrentPage({ page }))
78
83
  }
79
84
 
85
+ setFocusedField(model: CatalogRecordKeys) {
86
+ this.store.dispatch(EditorActions.setFocusedField({ model }))
87
+ }
88
+
80
89
  setFieldVisibility(field: EditorFieldIdentification, visible: boolean) {
81
90
  this.store.dispatch(EditorActions.setFieldVisibility({ field, visible }))
82
91
  }
@@ -7,11 +7,26 @@
7
7
  >editor.record.form.metadataQuality.title</span
8
8
  >
9
9
  </div>
10
- @for (properties of propertiesByPage; track properties; let i = $index) {
10
+ @for (
11
+ properties of (propertiesByPage$ | async) ?? [];
12
+ track properties;
13
+ let isLast = $last
14
+ ) {
11
15
  <div class="flex flex-col gap-2">
12
16
  @for (property of properties; track property) {
13
17
  <gn-ui-button
14
- [extraClass]="getExtraClass(property.value)"
18
+ style="
19
+ --gn-ui-button-justify: space-between;
20
+ --gn-ui-button-height: 34px;
21
+ --gn-ui-button-width: 100%;
22
+ --gn-ui-button-border-width: 0;
23
+ --gn-ui-button-color: black;
24
+ --gn-ui-button-background: transparent;
25
+ --gn-ui-button-bg-hover: #f3f4f6;
26
+ --gn-ui-button-disabled-opacity: 1;
27
+ "
28
+ [disabled]="property.value"
29
+ (buttonClick)="onCriterionClick(property)"
15
30
  type="outline"
16
31
  attr.data-cy="md-quality-btn-{{ property.label }}"
17
32
  >
@@ -28,7 +43,7 @@
28
43
  </div>
29
44
  </gn-ui-button>
30
45
  }
31
- @if (i !== propertiesByPage.length - 1) {
46
+ @if (!isLast) {
32
47
  <hr class="border-gray-300 w-11/12 mx-auto" />
33
48
  }
34
49
  </div>
@@ -1,5 +1,5 @@
1
- import { Component, Input, OnChanges } from '@angular/core'
2
- import { CatalogRecord } from '../../../../../../../libs/common/domain/src/lib/model/record'
1
+ import { Component, inject } from '@angular/core'
2
+ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
3
3
  import { ButtonComponent } from '../../../../../../../libs/ui/inputs/src'
4
4
  import {
5
5
  getAllKeysValidator,
@@ -13,8 +13,10 @@ import {
13
13
  } from '@ng-icons/core'
14
14
  import { iconoirBadgeCheck, iconoirSystemShut } from '@ng-icons/iconoir'
15
15
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
16
- import { EditorConfig } from '../../models'
17
16
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
17
+ import { EditorFacade } from '../../+state/editor.facade'
18
+ import { combineLatest, map } from 'rxjs'
19
+ import { AsyncPipe } from '@angular/common'
18
20
 
19
21
  //forced translations that are not available in fields.config.ts
20
22
  marker('editor.record.form.field.keywords')
@@ -29,6 +31,7 @@ marker('editor.record.form.field.organisation')
29
31
  TranslatePipe,
30
32
  ButtonComponent,
31
33
  NgIconComponent,
34
+ AsyncPipe,
32
35
  ],
33
36
  providers: [
34
37
  provideIcons({
@@ -42,47 +45,37 @@ marker('editor.record.form.field.organisation')
42
45
  templateUrl: './metadata-quality-panel.component.html',
43
46
  styleUrl: './metadata-quality-panel.component.css',
44
47
  })
45
- export class MetadataQualityPanelComponent implements OnChanges {
48
+ export class MetadataQualityPanelComponent {
49
+ facade = inject(EditorFacade)
46
50
  propsToValidate: ValidatorMapperKeys[] = getAllKeysValidator()
47
- propertiesByPage: { label: string; value: boolean }[][] = []
48
- @Input() editorConfig: EditorConfig
49
- @Input() record: CatalogRecord
50
51
 
51
- ngOnChanges() {
52
- if (this.editorConfig && this.record) {
53
- const fieldsByPage = this.editorConfig.pages.map((page) =>
54
- page.sections.flatMap((section) =>
55
- section.fields
56
- .filter((field) => this.propsToValidate.includes(field.model))
57
- .map((field) => field.model as ValidatorMapperKeys)
58
- )
59
- )
60
- // FIXME: temporarily add topics and organisation to the first and third page
61
- // as long as they are not handled by the editor
62
- if (fieldsByPage.length > 0) {
63
- fieldsByPage[0].includes('topics') || fieldsByPage[0].push('topics')
64
- fieldsByPage[2].includes('organisation') ||
65
- fieldsByPage[2].push('organisation')
66
- }
67
- this.propertiesByPage = fieldsByPage
68
- .map((fields) =>
69
- getQualityValidators(
70
- this.record,
71
- fields as ValidatorMapperKeys[]
72
- ).map(({ name, validator }) => ({
73
- label: `editor.record.form.field.${name}`, // use same translations as in fields.config.ts
74
- value: validator(),
75
- }))
52
+ propertiesByPage$ = combineLatest([
53
+ this.facade.editorConfig$,
54
+ this.facade.record$,
55
+ ]).pipe(
56
+ map(([editorConfig, record]) => {
57
+ if (!editorConfig || !record) return []
58
+ const validators = getQualityValidators(record, this.propsToValidate)
59
+ return editorConfig.pages
60
+ .map((page) =>
61
+ page.sections
62
+ .flatMap((section) => section.fields)
63
+ .flatMap(({ model }) =>
64
+ validators.filter((v) => (v.alias ?? v.name) === model)
65
+ )
66
+ .map(({ name, validator, alias }) => ({
67
+ label: `editor.record.form.field.${name}`,
68
+ value: validator(),
69
+ model: (alias ?? name) as CatalogRecordKeys,
70
+ }))
76
71
  )
77
72
  .filter((arr) => arr.length > 0)
78
- }
79
- }
73
+ })
74
+ )
80
75
 
81
- getExtraClass(checked: boolean): string {
82
- const baseClasses =
83
- 'flex flex-row justify-between rounded mb-1 h-[34px] w-full focus:ring-0 hover:border-none border-none hover:text-black text-black cursor-default'
84
- return checked
85
- ? `${baseClasses} bg-neutral-100 hover:bg-neutral-100`
86
- : `${baseClasses} bg-transparent hover:bg-transparent`
76
+ onCriterionClick(property: { value: boolean; model: CatalogRecordKeys }) {
77
+ if (!property.value) {
78
+ this.facade.setFocusedField(property.model)
79
+ }
87
80
  }
88
81
  }
@@ -20,7 +20,7 @@
20
20
  [disabled]="disabled"
21
21
  (change)="resetAllFormFields()"
22
22
  >
23
- @for (protocolOption of protocolOptions; track protocolOption) {
23
+ @for (protocolOption of availableProtocolOptions; track protocolOption) {
24
24
  <mat-radio-button [value]="protocolOption.value">
25
25
  {{ protocolOption.label | translate }}
26
26
  </mat-radio-button>
@@ -45,6 +45,11 @@
45
45
  }
46
46
  </gn-ui-url-input>
47
47
 
48
+ @if (loading) {
49
+ <div class="flex justify-center w-full py-4">
50
+ <gn-ui-spinning-loader></gn-ui-spinning-loader>
51
+ </div>
52
+ }
48
53
  @if (errorMessage) {
49
54
  <p class="text-sm text-red-500 pl-4" translate>
50
55
  editor.record.form.field.onlineResource.edit.identifier.error
@@ -22,6 +22,7 @@ import {
22
22
  TextInputComponent,
23
23
  UrlInputComponent,
24
24
  } from '../../../../../../../libs/ui/inputs/src'
25
+ import { SpinningLoaderComponent } from '../../../../../../../libs/ui/widgets/src'
25
26
  import { createFuzzyFilter, getLayers } from '../../../../../../../libs/util/shared/src'
26
27
  import {
27
28
  NgIconComponent,
@@ -57,6 +58,7 @@ marker(
57
58
  MatTooltipModule,
58
59
  MatRadioModule,
59
60
  NgIconComponent,
61
+ SpinningLoaderComponent,
60
62
  TextInputComponent,
61
63
  TranslateDirective,
62
64
  TranslatePipe,
@@ -79,17 +81,19 @@ export class OnlineServiceResourceInputComponent {
79
81
  @Input() protocolHint?: string
80
82
  @Input() disabled? = false
81
83
  @Input() modifyMode? = false
84
+ @Input() protocolOptions?: ServiceProtocol[]
82
85
  @Output() serviceChange: EventEmitter<DatasetServiceDistribution> =
83
86
  new EventEmitter()
84
87
 
85
88
  errorMessage = false
89
+ loading = false
86
90
  resetUrlOnChange = Math.random()
87
91
 
88
92
  layersSubject = new BehaviorSubject<{ name?: string; title?: string }[]>([])
89
93
  layers$: Observable<{ name?: string; title?: string }[]> =
90
94
  this.layersSubject.asObservable()
91
95
 
92
- protocolOptions: {
96
+ allProtocolOptions: {
93
97
  label: string
94
98
  value: ServiceProtocol
95
99
  }[] = [
@@ -123,6 +127,13 @@ export class OnlineServiceResourceInputComponent {
123
127
  },
124
128
  ]
125
129
 
130
+ get availableProtocolOptions() {
131
+ if (!this.protocolOptions) return this.allProtocolOptions
132
+ return this.protocolOptions.flatMap(
133
+ (v) => this.allProtocolOptions.find((o) => o.value === v) ?? []
134
+ )
135
+ }
136
+
126
137
  get activeLayerSuggestion() {
127
138
  return !['wps', 'GPFDL', 'esriRest', 'other'].includes(
128
139
  this._service.accessServiceProtocol
@@ -135,6 +146,8 @@ export class OnlineServiceResourceInputComponent {
135
146
  }
136
147
 
137
148
  async handleUploadClick(url: string) {
149
+ this.loading = true
150
+ this.cdr.detectChanges()
138
151
  try {
139
152
  const layers = await getLayers(url, this._service.accessServiceProtocol)
140
153
 
@@ -148,6 +161,7 @@ export class OnlineServiceResourceInputComponent {
148
161
  this.layersSubject.next([])
149
162
  }
150
163
 
164
+ this.loading = false
151
165
  this.cdr.detectChanges()
152
166
  }
153
167
 
@@ -159,6 +173,7 @@ export class OnlineServiceResourceInputComponent {
159
173
 
160
174
  resetLayersSuggestion() {
161
175
  this.errorMessage = false
176
+ this.loading = false
162
177
  this.layersSubject.next([])
163
178
  this._service.identifierInService = null
164
179
  }
@@ -2,7 +2,7 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
4
  OnDestroy,
5
- OnInit,
5
+ afterNextRender,
6
6
  inject,
7
7
  } from '@angular/core'
8
8
  import { CommonModule } from '@angular/common'
@@ -67,9 +67,7 @@ export type ConstraintChoice =
67
67
  styleUrls: ['./form-field-constraints-shortcuts.component.css'],
68
68
  changeDetection: ChangeDetectionStrategy.OnPush,
69
69
  })
70
- export class FormFieldConstraintsShortcutsComponent
71
- implements OnInit, OnDestroy
72
- {
70
+ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
73
71
  private editorFacade = inject(EditorFacade)
74
72
 
75
73
  legalConstraints$ = this.editorFacade.record$.pipe(
@@ -112,37 +110,39 @@ export class FormFieldConstraintsShortcutsComponent
112
110
 
113
111
  onDestroy$ = new Subject<void>()
114
112
 
115
- ngOnInit(): void {
116
- // hide all constraints if any toggle is activated
117
- this.anyToggleActivated$
118
- .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
- .subscribe((anyToggleActivated) => {
120
- if (anyToggleActivated) {
121
- this.hideAllConstraintSections()
122
- }
123
- })
113
+ constructor() {
114
+ // Deferred to afterNextRender to avoid dispatching store actions
115
+ // synchronously during Angular's change detection cycle (NG0100)
116
+ afterNextRender(() => {
117
+ this.anyToggleActivated$
118
+ .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
+ .subscribe((anyToggleActivated) => {
120
+ if (anyToggleActivated) {
121
+ this.hideAllConstraintSections()
122
+ }
123
+ })
124
124
 
125
- // also hide constraints which are empty arrays
126
- const hideEmptyConstraints = (
127
- constraints$: Observable<Constraint[]>,
128
- model: ConstraintChoice
129
- ) => {
130
- const isConstraintNotEmpty$ = constraints$.pipe(
131
- takeUntil(this.onDestroy$),
132
- map((c) => c.length > 0),
133
- distinctUntilChanged()
134
- )
135
- combineLatest([
136
- isConstraintNotEmpty$,
137
- this.anyToggleActivated$,
138
- ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
139
- const visible = isNotEmpty && !anyToggleActivated
140
- this.editorFacade.setFieldVisibility({ model }, visible)
141
- })
142
- }
143
- hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
144
- hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
145
- hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
125
+ const hideEmptyConstraints = (
126
+ constraints$: Observable<Constraint[]>,
127
+ model: ConstraintChoice
128
+ ) => {
129
+ const isConstraintNotEmpty$ = constraints$.pipe(
130
+ takeUntil(this.onDestroy$),
131
+ map((c) => c.length > 0),
132
+ distinctUntilChanged()
133
+ )
134
+ combineLatest([
135
+ isConstraintNotEmpty$,
136
+ this.anyToggleActivated$,
137
+ ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
138
+ const visible = isNotEmpty && !anyToggleActivated
139
+ this.editorFacade.setFieldVisibility({ model }, visible)
140
+ })
141
+ }
142
+ hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
143
+ hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
144
+ hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
145
+ })
146
146
  }
147
147
 
148
148
  ngOnDestroy() {
@@ -10,21 +10,14 @@
10
10
  </gn-ui-autocomplete>
11
11
 
12
12
  @if (contacts.length > 0) {
13
- @if (contacts.length === 1) {
14
- @for (contact of contacts; track contact; let index = $index) {
15
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
16
- }
17
- }
18
- @if (contacts.length > 1) {
19
- <gn-ui-sortable-list
20
- [items]="contacts"
21
- (itemsOrderChange)="handleContactsChanged($event)"
22
- [elementTemplate]="contactTemplate"
23
- ></gn-ui-sortable-list>
24
- <ng-template #contactTemplate let-contact>
25
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
26
- </ng-template>
27
- }
13
+ <gn-ui-sortable-list
14
+ [items]="contacts"
15
+ (itemsOrderChange)="handleContactsChanged($event)"
16
+ [elementTemplate]="contactTemplate"
17
+ ></gn-ui-sortable-list>
18
+ <ng-template #contactTemplate let-contact>
19
+ <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
20
+ </ng-template>
28
21
  } @else {
29
22
  <div
30
23
  class="p-4 text-sm border border-primary bg-primary-lightest rounded-lg"
@@ -28,7 +28,11 @@ import { UserModel } from '../../../../../../../../../libs/common/domain/src/lib
28
28
  import { PlatformServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/platform.service.interface'
29
29
  import { OrganizationsServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/organizations.service.interface'
30
30
  import { ContactCardComponent } from '../../../contact-card/contact-card.component'
31
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
31
+ import {
32
+ createFuzzyFilter,
33
+ getIndividualDisplayName,
34
+ toIndividual,
35
+ } from '../../../../../../../../../libs/util/shared/src'
32
36
  import { map } from 'rxjs/operators'
33
37
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
34
38
 
@@ -117,9 +121,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
117
121
  * gn-ui-autocomplete
118
122
  */
119
123
  displayWithFn: (user: UserModel) => string = (user) =>
120
- `${user.name} ${user.surname} ${
121
- user.organisation ? `(${user.organisation})` : ''
122
- }`
124
+ getIndividualDisplayName(toIndividual(user))
123
125
 
124
126
  /**
125
127
  * gn-ui-autocomplete
@@ -8,6 +8,14 @@
8
8
  </gn-ui-button>
9
9
  }
10
10
  </div>
11
+ @if (value.length === 0) {
12
+ <div
13
+ class="p-4 border border-primary bg-primary-lightest rounded-lg"
14
+ translate
15
+ >
16
+ editor.record.form.field.contactsForResource.noContact
17
+ </div>
18
+ }
11
19
  @if (roleSectionsToDisplay && roleSectionsToDisplay.length > 0) {
12
20
  <div class="mt-8" data-test="displayedRoles">
13
21
  @for (
@@ -53,12 +61,5 @@
53
61
  </div>
54
62
  }
55
63
  </div>
56
- } @else {
57
- <div
58
- class="p-4 border border-primary bg-primary-lightest rounded-lg"
59
- translate
60
- >
61
- editor.record.form.field.contactsForResource.noContact
62
- </div>
63
64
  }
64
65
  </div>
@@ -23,7 +23,11 @@ import {
23
23
  AutocompleteComponent,
24
24
  ButtonComponent,
25
25
  } from '../../../../../../../../../libs/ui/inputs/src'
26
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
26
+ import {
27
+ createFuzzyFilter,
28
+ getIndividualDisplayName,
29
+ toIndividual,
30
+ } from '../../../../../../../../../libs/util/shared/src'
27
31
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
28
32
  import {
29
33
  debounceTime,
@@ -169,11 +173,7 @@ export class FormFieldContactsForResourceComponent
169
173
  * gn-ui-autocomplete
170
174
  */
171
175
  displayWithFn: (user: UserModel) => string = (user) =>
172
- user.name
173
- ? `${user.name} ${user.surname} ${
174
- user.organisation ? `(${user.organisation})` : ''
175
- }`
176
- : ``
176
+ getIndividualDisplayName(toIndividual(user))
177
177
 
178
178
  /**
179
179
  * gn-ui-autocomplete
@@ -0,0 +1,3 @@
1
+ gn-ui-form-field {
2
+ scroll-margin-top: 90px;
3
+ }
@@ -24,6 +24,7 @@
24
24
  ) {
25
25
  @if (!field.config.hidden) {
26
26
  <gn-ui-form-field
27
+ [id]="anchorIdPrefix + field.config.model"
27
28
  [ngClass]="
28
29
  field.config.gridColumnSpan === 1
29
30
  ? 'col-span-1'
@@ -1,5 +1,12 @@
1
1
  import { CommonModule } from '@angular/common'
2
- import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ inject,
7
+ OnDestroy,
8
+ OnInit,
9
+ } from '@angular/core'
3
10
  import { EditorFacade } from '../../+state/editor.facade'
4
11
  import { EditorFieldValue } from '../../models'
5
12
  import { FormFieldComponent } from './form-field'
@@ -8,7 +15,7 @@ import {
8
15
  EditorFieldWithValue,
9
16
  EditorSectionWithValues,
10
17
  } from '../../+state/editor.models'
11
- import { map } from 'rxjs'
18
+ import { filter, firstValueFrom, map, Subscription, switchMap } from 'rxjs'
12
19
  import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
13
20
 
14
21
  @Component({
@@ -19,13 +26,50 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
19
26
  standalone: true,
20
27
  imports: [CommonModule, FormFieldComponent, TranslateDirective],
21
28
  })
22
- export class RecordFormComponent {
29
+ export class RecordFormComponent implements OnInit, OnDestroy {
30
+ anchorIdPrefix = 'gn-ui--field-'
23
31
  facade = inject(EditorFacade)
32
+ private el = inject(ElementRef)
33
+ subscription = new Subscription()
24
34
 
25
35
  recordUniqueIdentifier$ = this.facade.record$.pipe(
26
36
  map((record) => record.uniqueIdentifier)
27
37
  )
28
38
 
39
+ ngOnInit() {
40
+ this.subscription.add(
41
+ this.facade.focusedField$
42
+ .pipe(
43
+ filter((field) => !!field),
44
+ switchMap(async (field) => ({
45
+ field: field as CatalogRecordKeys,
46
+ pageIndex: await this.getPageIndexForField(
47
+ field as CatalogRecordKeys
48
+ ),
49
+ }))
50
+ )
51
+ .subscribe(async ({ field, pageIndex }) => {
52
+ const currentPage = await firstValueFrom(this.facade.currentPage$)
53
+ if (pageIndex !== null && pageIndex !== currentPage) {
54
+ this.facade.setCurrentPage(pageIndex)
55
+ this.el.nativeElement.scrollIntoView({
56
+ behavior: 'instant',
57
+ block: 'start',
58
+ })
59
+ }
60
+ setTimeout(() =>
61
+ document
62
+ .getElementById(this.anchorIdPrefix + field)
63
+ ?.scrollIntoView({ behavior: 'instant', block: 'start' })
64
+ )
65
+ })
66
+ )
67
+ }
68
+
69
+ ngOnDestroy() {
70
+ this.subscription.unsubscribe()
71
+ }
72
+
29
73
  handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
30
74
  if (!model) {
31
75
  return
@@ -40,4 +84,14 @@ export class RecordFormComponent {
40
84
  sectionTracker(index: number, section: EditorSectionWithValues) {
41
85
  return section.labelKey
42
86
  }
87
+
88
+ async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
89
+ const config = await firstValueFrom(this.facade.editorConfig$)
90
+ const pageIndex = config.pages.findIndex((page) =>
91
+ page.sections.some((section) =>
92
+ section.fields.some((field) => field.model === model)
93
+ )
94
+ )
95
+ return pageIndex >= 0 ? pageIndex : null
96
+ }
43
97
  }
@@ -18,6 +18,7 @@ export * from './lib/metadata-doi/metadata-doi.component'
18
18
  export * from './lib/metadata-info/metadata-info.component'
19
19
  export * from './lib/metadata-quality-item/metadata-quality-item.component'
20
20
  export * from './lib/metadata-quality/metadata-quality.component'
21
+ export * from './lib/contact-pill/contact-pill.component'
21
22
  export * from './lib/notification/notification.component'
22
23
  export * from './lib/record-api-form/record-api-form.component'
23
24
  export * from './lib/thumbnail/thumbnail.component'
@@ -0,0 +1,16 @@
1
+ <gn-ui-button
2
+ type="primary-light"
3
+ extraClass="group w-full min-h-12 gap-3 justify-between py-2 pl-5 pr-4 rounded"
4
+ data-test="contact-pill"
5
+ >
6
+ <span
7
+ class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
8
+ [title]="displayName"
9
+ >{{ displayName }}</span
10
+ >
11
+ <div
12
+ class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
13
+ >
14
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
15
+ </div>
16
+ </gn-ui-button>
@@ -0,0 +1,26 @@
1
+ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2
+ import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
3
+ import { NgIcon, provideIcons } from '@ng-icons/core'
4
+ import { matInfoOutline } from '@ng-icons/material-icons/outline'
5
+ import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
6
+ import { getIndividualDisplayName } from '../../../../../../libs/util/shared/src'
7
+
8
+ @Component({
9
+ selector: 'gn-ui-contact-pill',
10
+ templateUrl: './contact-pill.component.html',
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ standalone: true,
13
+ imports: [NgIcon, ButtonComponent],
14
+ viewProviders: [
15
+ provideIcons({
16
+ matInfoOutline,
17
+ }),
18
+ ],
19
+ })
20
+ export class ContactPillComponent {
21
+ @Input() contact: Individual
22
+
23
+ get displayName(): string {
24
+ return getIndividualDisplayName(this.contact)
25
+ }
26
+ }
@@ -1,6 +1,6 @@
1
1
  <a
2
2
  [attr.href]="linkHref"
3
- [target]="linkTarget"
3
+ target="_self"
4
4
  class="record-card"
5
5
  [ngClass]="cardClass"
6
6
  >
@@ -58,7 +58,6 @@ export class InternalLinkCardComponent implements OnInit {
58
58
  protected elementRef = inject(ElementRef)
59
59
 
60
60
  @Input() record: CatalogRecord
61
- @Input() linkTarget = '_blank'
62
61
  @Input() linkHref: string = null
63
62
  @Input() metadataQualityDisplay: boolean
64
63
  @Input() favoriteTemplate: TemplateRef<{ $implicit: CatalogRecord }>
@@ -16,7 +16,3 @@
16
16
  --gn-ui-badge-background-color: var(--color-primary-white);
17
17
  --gn-ui-badge-text-color: var(--color-primary-darkest);
18
18
  }
19
-
20
- :host .metadata-info-keywords ::ng-deep gn-ui-badge:hover {
21
- --gn-ui-badge-text-color: white;
22
- }