geonetwork-ui 2.10.0-dev.6fa5006eb → 2.10.0-dev.842517315
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 +695 -5757
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +165 -78
- package/index.d.ts.map +1 -1
- package/material-styles.css +483 -0
- package/package.json +25 -35
- package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +9 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +7 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +2 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +1 -0
- package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +8 -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 +1 -0
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +5 -4
- 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 +7 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -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 +54 -32
- 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 +48 -2
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +13 -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/fixtures/src/lib/records.fixtures.ts +5 -0
- package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -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 +67 -0
- package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
- 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 +6 -8
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +9 -2
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +12 -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.component.css +3 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -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 +34 -2
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -2
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +51 -33
- package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
- package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
- package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
- package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
- package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
- 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 +1 -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 +29 -4
- package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +51 -7
- 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/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.html +20 -10
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +29 -1
- package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
- package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
- 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/map/src/lib/components/spatial-extent/spatial-extent.component.ts +4 -54
- 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/widgets/src/lib/popover/popover.component.ts +7 -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 +1 -1
- 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/user-display.ts +9 -0
- package/style.css +0 -1
- package/tailwind.base.css +8 -0
- package/translations/de.json +31 -20
- package/translations/en.json +31 -20
- package/translations/es.json +31 -20
- package/translations/fr.json +31 -20
- package/translations/it.json +31 -20
- package/translations/nl.json +31 -20
- package/translations/pt.json +31 -20
- package/translations/sk.json +31 -20
package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts
CHANGED
|
@@ -128,7 +128,7 @@ export class OrganizationsFromMetadataService
|
|
|
128
128
|
) {}
|
|
129
129
|
|
|
130
130
|
private get langIndex() {
|
|
131
|
-
return `lang${toLang3(this.translateService.
|
|
131
|
+
return `lang${toLang3(this.translateService.getCurrentLang())}`
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
equalsNormalizedStrings(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpClient, HttpEventType } from '@angular/common/http'
|
|
2
|
-
import { Injectable, InjectionToken
|
|
2
|
+
import { inject, Injectable, InjectionToken } from '@angular/core'
|
|
3
3
|
import {
|
|
4
4
|
KeywordApiResponse,
|
|
5
5
|
ThesaurusApiResponse,
|
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
} from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
13
13
|
import { KeywordType } from '../../../../../../../libs/common/domain/src/lib/model/thesaurus'
|
|
14
14
|
import { UserModel } from '../../../../../../../libs/common/domain/src/lib/model/user/user.model'
|
|
15
|
+
import { GroupModel } from '../../../../../../../libs/common/domain/src/lib/model/user/group.model'
|
|
15
16
|
import {
|
|
16
17
|
PlatformServiceInterface,
|
|
17
18
|
UploadEvent,
|
|
18
19
|
} from '../../../../../../../libs/common/domain/src/lib/platform.service.interface'
|
|
19
20
|
import {
|
|
21
|
+
GroupsApiService,
|
|
20
22
|
MeApiService,
|
|
21
23
|
RecordsApiService,
|
|
22
24
|
RegistriesApiService,
|
|
@@ -57,6 +59,7 @@ export const DISABLE_AUTH = new InjectionToken<boolean>('gnDisableAuth', {
|
|
|
57
59
|
export class Gn4PlatformService implements PlatformServiceInterface {
|
|
58
60
|
private meApi = inject(MeApiService)
|
|
59
61
|
private usersApi = inject(UsersApiService)
|
|
62
|
+
private groupsApi = inject(GroupsApiService)
|
|
60
63
|
private mapper = inject(Gn4PlatformMapper)
|
|
61
64
|
private toolsApiService = inject(ToolsApiService)
|
|
62
65
|
private registriesApiService = inject(RegistriesApiService)
|
|
@@ -97,7 +100,7 @@ export class Gn4PlatformService implements PlatformServiceInterface {
|
|
|
97
100
|
private keywordsByThesauri: Record<string, Observable<Keyword[]>> = {}
|
|
98
101
|
|
|
99
102
|
private get lang3() {
|
|
100
|
-
return toLang3(this.translateService.
|
|
103
|
+
return toLang3(this.translateService.getCurrentLang())
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
constructor() {
|
|
@@ -155,6 +158,49 @@ export class Gn4PlatformService implements PlatformServiceInterface {
|
|
|
155
158
|
return this.users$
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
getUserPermissionsByGroup(): Observable<GroupModel[]> {
|
|
162
|
+
if (this.disableAuth) return of([])
|
|
163
|
+
return combineLatest([this.meApi.getMe(), this.groupsApi.getGroups()]).pipe(
|
|
164
|
+
map(([meResponse, groups]) => {
|
|
165
|
+
if (!meResponse) return []
|
|
166
|
+
if (meResponse.admin) {
|
|
167
|
+
return groups.map((group) => ({
|
|
168
|
+
groupId: group.id,
|
|
169
|
+
groupName: group.name,
|
|
170
|
+
isMember: true,
|
|
171
|
+
canEdit: true,
|
|
172
|
+
canApprove: true,
|
|
173
|
+
canAdministrate: true,
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
const reviewerIds = meResponse.groupsWithReviewer ?? []
|
|
177
|
+
const editorIds = meResponse.groupsWithEditor ?? []
|
|
178
|
+
const memberIds = meResponse.groupsWithRegisteredUser ?? []
|
|
179
|
+
const adminIds = meResponse.groupsWithUserAdmin ?? []
|
|
180
|
+
const groupsById = new Map(groups.map((g) => [g.id, g]))
|
|
181
|
+
return [
|
|
182
|
+
...new Set([
|
|
183
|
+
...reviewerIds,
|
|
184
|
+
...editorIds,
|
|
185
|
+
...groups.map((g) => g.id),
|
|
186
|
+
]),
|
|
187
|
+
]
|
|
188
|
+
.filter((id) => groupsById.has(id))
|
|
189
|
+
.map((id) => {
|
|
190
|
+
const group = groupsById.get(id)
|
|
191
|
+
return {
|
|
192
|
+
groupId: group.id,
|
|
193
|
+
groupName: group.name,
|
|
194
|
+
isMember: memberIds.includes(id),
|
|
195
|
+
canEdit: editorIds.includes(id),
|
|
196
|
+
canApprove: reviewerIds.includes(id),
|
|
197
|
+
canAdministrate: adminIds.includes(id),
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
translateKey(key: string): Observable<string> {
|
|
159
205
|
// if the key is a URI, use the registries API to look for the translation
|
|
160
206
|
if (key.match(/^https?:\/\//)) {
|
|
@@ -104,15 +104,12 @@ export interface Keyword {
|
|
|
104
104
|
type: KeywordType
|
|
105
105
|
thesaurus?: ThesaurusModel
|
|
106
106
|
bbox?: [number, number, number, number]
|
|
107
|
+
// thesaurus hierarchy path incl. the keyword as last element; unset for root-level keywords
|
|
108
|
+
hierarchyPath?: string[]
|
|
107
109
|
|
|
108
110
|
translations?: KeywordTranslations
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
export interface INSPIRE_topic {
|
|
112
|
-
value: string
|
|
113
|
-
label: string
|
|
114
|
-
}
|
|
115
|
-
|
|
116
113
|
export interface ResourceIdentifier {
|
|
117
114
|
code: string
|
|
118
115
|
codeSpace?: string
|
|
@@ -254,10 +251,20 @@ export interface DatasetTemporalExtent {
|
|
|
254
251
|
end?: Date
|
|
255
252
|
}
|
|
256
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Represents a source dataset referenced from a lineage entry.
|
|
256
|
+
*/
|
|
257
|
+
export interface SourceRecord {
|
|
258
|
+
uuid?: string
|
|
259
|
+
title?: string
|
|
260
|
+
href?: string
|
|
261
|
+
}
|
|
262
|
+
|
|
257
263
|
export interface DatasetRecord extends BaseRecord {
|
|
258
264
|
kind: 'dataset'
|
|
259
265
|
status: RecordStatus
|
|
260
266
|
lineage: string // Explanation of the origin of this record (e.g: how, why)"
|
|
267
|
+
sourceRecords: Array<SourceRecord>
|
|
261
268
|
onlineResources: Array<DatasetOnlineResource>
|
|
262
269
|
spatialExtents: Array<DatasetSpatialExtent>
|
|
263
270
|
temporalExtents: Array<DatasetTemporalExtent>
|
|
@@ -285,6 +292,7 @@ export interface ServiceRecord extends BaseRecord {
|
|
|
285
292
|
export interface ReuseRecord extends BaseRecord {
|
|
286
293
|
kind: 'reuse'
|
|
287
294
|
lineage: string // Explanation of the origin of this record (e.g: how, why)"
|
|
295
|
+
sourceRecords: Array<SourceRecord>
|
|
288
296
|
onlineResources: Array<DatasetOnlineResource>
|
|
289
297
|
reuseType: ReuseType
|
|
290
298
|
spatialExtents: Array<DatasetSpatialExtent>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Observable } from 'rxjs'
|
|
2
2
|
import type { UserModel } from './model/user/user.model'
|
|
3
|
+
import type { GroupModel } from './model/user/group.model'
|
|
3
4
|
import type { Organization } from './model/record/organization.model'
|
|
4
5
|
import { CatalogRecord, Keyword, UserFeedback } from './model/record'
|
|
5
6
|
import { KeywordType } from './model/thesaurus'
|
|
@@ -28,6 +29,7 @@ export abstract class PlatformServiceInterface {
|
|
|
28
29
|
abstract getMe(): Observable<UserModel>
|
|
29
30
|
abstract isAnonymous(): Observable<boolean>
|
|
30
31
|
abstract getUsers(): Observable<UserModel[]>
|
|
32
|
+
abstract getUserPermissionsByGroup(): Observable<GroupModel[]>
|
|
31
33
|
abstract getUsersByOrganization(
|
|
32
34
|
organisation: Organization
|
|
33
35
|
): Observable<UserModel[]>
|
|
@@ -134,6 +134,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
|
|
|
134
134
|
lineage: `This record was edited manually to test the conversion processes
|
|
135
135
|
|
|
136
136
|
As such, **it is not very interesting at all.**`,
|
|
137
|
+
sourceRecords: [],
|
|
137
138
|
licenses: [
|
|
138
139
|
{
|
|
139
140
|
text: 'Licence ODbL mai 2013 (basée sur ODbL 1.0)',
|
|
@@ -310,6 +311,7 @@ Malgré l'attention portée à la création de ces données, il est rappelé que
|
|
|
310
311
|
],
|
|
311
312
|
lineage: `Document d’urbanisme numérisé conformément aux prescriptions nationales du CNIG par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.
|
|
312
313
|
Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur de 2019 et contrôlé par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.`,
|
|
314
|
+
sourceRecords: [],
|
|
313
315
|
legalConstraints: [],
|
|
314
316
|
securityConstraints: [],
|
|
315
317
|
otherConstraints: [],
|
|
@@ -361,6 +363,7 @@ export const simpleDatasetRecordFixture = (): DatasetRecord => ({
|
|
|
361
363
|
overviews: [],
|
|
362
364
|
spatialExtents: [],
|
|
363
365
|
temporalExtents: [],
|
|
366
|
+
sourceRecords: [],
|
|
364
367
|
onlineResources: [
|
|
365
368
|
{
|
|
366
369
|
type: 'download',
|
|
@@ -413,6 +416,7 @@ export const simpleDatasetRecordWithFcatsFixture = (): DatasetRecord => ({
|
|
|
413
416
|
overviews: [],
|
|
414
417
|
spatialExtents: [],
|
|
415
418
|
temporalExtents: [],
|
|
419
|
+
sourceRecords: [],
|
|
416
420
|
onlineResources: [],
|
|
417
421
|
updateFrequency: { per: 'month', updatedTimes: 3 },
|
|
418
422
|
translations: {},
|
|
@@ -978,6 +982,7 @@ export const multilingualDatasetFixture: () => DatasetRecord = () => ({
|
|
|
978
982
|
title: 'English Title',
|
|
979
983
|
abstract: 'English Abstract',
|
|
980
984
|
lineage: 'English Lineage',
|
|
985
|
+
sourceRecords: [],
|
|
981
986
|
translations: {
|
|
982
987
|
title: { fr: 'Titre Français', de: 'Titel DE' },
|
|
983
988
|
abstract: { fr: 'Résumé Français', de: 'Beschreibung DE' },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inject, Injectable } from '@angular/core'
|
|
2
2
|
import { SourcesApiService } from '../../../../../../libs/data-access/gn4/src'
|
|
3
3
|
import { Observable } from 'rxjs'
|
|
4
4
|
import { filter, map, shareReplay } from 'rxjs/operators'
|
|
@@ -21,7 +21,10 @@ export class SourcesService {
|
|
|
21
21
|
return this.sources$.pipe(
|
|
22
22
|
map((sources) => sources.filter((source) => source.uuid === uuid)[0]),
|
|
23
23
|
filter((source) => !!source),
|
|
24
|
-
map(
|
|
24
|
+
map(
|
|
25
|
+
(source) =>
|
|
26
|
+
source.label[toLang3(this.translateService.getCurrentLang())]
|
|
27
|
+
)
|
|
25
28
|
)
|
|
26
29
|
}
|
|
27
30
|
}
|
package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { FormFieldRichComponent } from '../record-form/form-field/form-field-rich/form-field-rich.component'
|
|
10
10
|
import { ButtonComponent, UrlInputComponent } from '../../../../../../../libs/ui/inputs/src'
|
|
11
|
-
import { MatIconModule } from '@angular/material/icon'
|
|
12
11
|
import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
|
|
13
12
|
import { Constraint } from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
14
13
|
import {
|
|
@@ -26,7 +25,6 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
|
|
|
26
25
|
FormFieldRichComponent,
|
|
27
26
|
UrlInputComponent,
|
|
28
27
|
ButtonComponent,
|
|
29
|
-
MatIconModule,
|
|
30
28
|
TranslateDirective,
|
|
31
29
|
TranslatePipe,
|
|
32
30
|
NgIconComponent,
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div class="flex flex-col gap-7">
|
|
2
|
+
<div class="grid grid-cols-2 gap-4">
|
|
3
|
+
<div class="min-w-0">
|
|
4
|
+
<h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
|
|
5
|
+
editor.record.form.field.contactDetails.lastName
|
|
6
|
+
</h3>
|
|
7
|
+
<gn-ui-text-input
|
|
8
|
+
class="block w-full"
|
|
9
|
+
extraClass="w-full"
|
|
10
|
+
[(value)]="contact.lastName"
|
|
11
|
+
(valueChange)="emitContactChange()"
|
|
12
|
+
[placeholder]="
|
|
13
|
+
'editor.record.form.field.contactDetails.lastName.placeholder'
|
|
14
|
+
| translate
|
|
15
|
+
"
|
|
16
|
+
data-test="contactDetailsLastName"
|
|
17
|
+
></gn-ui-text-input>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="min-w-0">
|
|
20
|
+
<h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
|
|
21
|
+
editor.record.form.field.contactDetails.firstName
|
|
22
|
+
</h3>
|
|
23
|
+
<gn-ui-text-input
|
|
24
|
+
class="block w-full"
|
|
25
|
+
extraClass="w-full"
|
|
26
|
+
[(value)]="contact.firstName"
|
|
27
|
+
(valueChange)="emitContactChange()"
|
|
28
|
+
[placeholder]="
|
|
29
|
+
'editor.record.form.field.contactDetails.firstName.placeholder'
|
|
30
|
+
| translate
|
|
31
|
+
"
|
|
32
|
+
data-test="contactDetailsFirstName"
|
|
33
|
+
></gn-ui-text-input>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
|
|
38
|
+
editor.record.form.field.contactDetails.email
|
|
39
|
+
</h3>
|
|
40
|
+
<gn-ui-text-input
|
|
41
|
+
class="block w-full"
|
|
42
|
+
extraClass="w-full"
|
|
43
|
+
[value]="contact.organization?.email ?? ''"
|
|
44
|
+
(valueChange)="handleOrganizationChange({ email: $event })"
|
|
45
|
+
[placeholder]="
|
|
46
|
+
'editor.record.form.field.contactDetails.email.placeholder' | translate
|
|
47
|
+
"
|
|
48
|
+
data-test="contactDetailsEmail"
|
|
49
|
+
></gn-ui-text-input>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
|
|
53
|
+
editor.record.form.field.contactDetails.organization
|
|
54
|
+
</h3>
|
|
55
|
+
<gn-ui-text-input
|
|
56
|
+
class="block w-full"
|
|
57
|
+
extraClass="w-full"
|
|
58
|
+
[value]="contact.organization?.name ?? ''"
|
|
59
|
+
(valueChange)="handleOrganizationChange({ name: $event })"
|
|
60
|
+
[placeholder]="
|
|
61
|
+
'editor.record.form.field.contactDetails.organization.placeholder'
|
|
62
|
+
| translate
|
|
63
|
+
"
|
|
64
|
+
data-test="contactDetailsOrganization"
|
|
65
|
+
></gn-ui-text-input>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
Input,
|
|
6
|
+
Output,
|
|
7
|
+
} from '@angular/core'
|
|
8
|
+
import {
|
|
9
|
+
Individual,
|
|
10
|
+
Organization,
|
|
11
|
+
} from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
12
|
+
import { TextInputComponent } from '../../../../../../../libs/ui/inputs/src'
|
|
13
|
+
import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'gn-ui-contact-details-form',
|
|
17
|
+
templateUrl: './contact-details-form.component.html',
|
|
18
|
+
styleUrls: ['./contact-details-form.component.css'],
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
standalone: true,
|
|
21
|
+
imports: [TextInputComponent, TranslateDirective, TranslatePipe],
|
|
22
|
+
})
|
|
23
|
+
export class ContactDetailsFormComponent {
|
|
24
|
+
@Input() contact: Individual
|
|
25
|
+
@Output() contactChange = new EventEmitter<Individual>()
|
|
26
|
+
|
|
27
|
+
emitContactChange() {
|
|
28
|
+
this.contactChange.emit(this.contact)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
handleOrganizationChange(change: Partial<Organization>) {
|
|
32
|
+
this.contact.organization = {
|
|
33
|
+
...(this.contact.organization ?? ({} as Organization)),
|
|
34
|
+
...change,
|
|
35
|
+
} as Organization
|
|
36
|
+
|
|
37
|
+
this.emitContactChange()
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Directive, ElementRef, inject, Input } from '@angular/core'
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[gnUiFieldFocus]',
|
|
5
|
+
standalone: true,
|
|
6
|
+
exportAs: 'fieldFocus',
|
|
7
|
+
})
|
|
8
|
+
export class FieldFocusDirective {
|
|
9
|
+
@Input() gnUiFieldFocusGlowClass = 'gn-ui-field-focus-glow'
|
|
10
|
+
|
|
11
|
+
private el = inject(ElementRef)
|
|
12
|
+
|
|
13
|
+
public focusField(focusInnerTarget = true) {
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
const host = this.el.nativeElement as HTMLElement
|
|
16
|
+
const glowClass = this.gnUiFieldFocusGlowClass
|
|
17
|
+
|
|
18
|
+
host.classList.remove(glowClass)
|
|
19
|
+
void host.offsetWidth
|
|
20
|
+
host.classList.add(glowClass)
|
|
21
|
+
host.addEventListener(
|
|
22
|
+
'animationend',
|
|
23
|
+
() => host.classList.remove(glowClass),
|
|
24
|
+
{ once: true }
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
host.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
28
|
+
if (!focusInnerTarget) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const target =
|
|
32
|
+
host.querySelector<HTMLElement>(
|
|
33
|
+
'input, textarea, select, [contenteditable="true"]'
|
|
34
|
+
) ??
|
|
35
|
+
host.querySelector<HTMLElement>(
|
|
36
|
+
'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
37
|
+
)
|
|
38
|
+
target?.focus({ preventScroll: true })
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -131,15 +131,13 @@ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
|
|
|
131
131
|
map((c) => c.length > 0),
|
|
132
132
|
distinctUntilChanged()
|
|
133
133
|
)
|
|
134
|
-
combineLatest([
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
134
|
+
combineLatest([isConstraintNotEmpty$, this.anyToggleActivated$])
|
|
135
|
+
.pipe(takeUntil(this.onDestroy$))
|
|
136
|
+
.subscribe(([isNotEmpty, anyToggleActivated]) => {
|
|
137
|
+
const visible = isNotEmpty && !anyToggleActivated
|
|
138
|
+
this.editorFacade.setFieldVisibility({ model }, visible)
|
|
139
|
+
})
|
|
141
140
|
}
|
|
142
|
-
hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
|
|
143
141
|
hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
|
|
144
142
|
hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
|
|
145
143
|
})
|
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
(itemsOrderChange)="handleContactsChanged($event)"
|
|
16
16
|
[elementTemplate]="contactTemplate"
|
|
17
17
|
></gn-ui-sortable-list>
|
|
18
|
-
<ng-template #contactTemplate let-contact>
|
|
19
|
-
|
|
18
|
+
<ng-template #contactTemplate let-contact let-index="index">
|
|
19
|
+
@if (modelSpecifier === 'contact:editableDetails') {
|
|
20
|
+
<gn-ui-contact-details-form
|
|
21
|
+
[contact]="contact"
|
|
22
|
+
(contactChange)="handleContactChanged($event, index)"
|
|
23
|
+
></gn-ui-contact-details-form>
|
|
24
|
+
} @else {
|
|
25
|
+
<gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
|
|
26
|
+
}
|
|
20
27
|
</ng-template>
|
|
21
28
|
} @else {
|
|
22
29
|
<div
|
|
@@ -28,6 +28,7 @@ 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 { ContactDetailsFormComponent } from '../../../contact-details/contact-details-form.component'
|
|
31
32
|
import {
|
|
32
33
|
createFuzzyFilter,
|
|
33
34
|
getIndividualDisplayName,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
} from '../../../../../../../../../libs/util/shared/src'
|
|
36
37
|
import { map } from 'rxjs/operators'
|
|
37
38
|
import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
|
|
39
|
+
import { FieldModelSpecifier } from '../../../../models/editor-config.model'
|
|
38
40
|
|
|
39
41
|
@Component({
|
|
40
42
|
selector: 'gn-ui-form-field-contacts',
|
|
@@ -47,6 +49,7 @@ import { SortableListComponent } from '../../../../../../../../../libs/ui/layout
|
|
|
47
49
|
TranslateDirective,
|
|
48
50
|
TranslatePipe,
|
|
49
51
|
ContactCardComponent,
|
|
52
|
+
ContactDetailsFormComponent,
|
|
50
53
|
SortableListComponent,
|
|
51
54
|
],
|
|
52
55
|
})
|
|
@@ -56,6 +59,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
|
|
|
56
59
|
private changeDetectorRef = inject(ChangeDetectorRef)
|
|
57
60
|
|
|
58
61
|
@Input() value: Individual[]
|
|
62
|
+
@Input() modelSpecifier: FieldModelSpecifier
|
|
59
63
|
@Output() valueChange: EventEmitter<Individual[]> = new EventEmitter()
|
|
60
64
|
|
|
61
65
|
contacts: Individual[] = []
|
|
@@ -117,6 +121,14 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
|
|
|
117
121
|
this.valueChange.emit(contacts)
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
handleContactChanged(updatedContact: Individual, index: number) {
|
|
125
|
+
const contacts = this.contacts.map((contact, i) =>
|
|
126
|
+
i === index ? updatedContact : contact
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
this.handleContactsChanged(contacts)
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
/**
|
|
121
133
|
* gn-ui-autocomplete
|
|
122
134
|
*/
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
DropdownMultiselectComponent,
|
|
6
6
|
} from '../../../../../../../../../libs/ui/inputs/src'
|
|
7
7
|
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
|
|
8
|
-
import {
|
|
8
|
+
import { ISO_TOPICS } from '../../../../fields.config'
|
|
9
9
|
|
|
10
10
|
@Component({
|
|
11
11
|
selector: 'gn-ui-form-field-topics',
|
|
@@ -22,7 +22,7 @@ export class FormFieldTopicsComponent {
|
|
|
22
22
|
this.topics = topics
|
|
23
23
|
}
|
|
24
24
|
@Output() valueChange: EventEmitter<string[]> = new EventEmitter()
|
|
25
|
-
availableTopics =
|
|
25
|
+
availableTopics = ISO_TOPICS.map((topic) => {
|
|
26
26
|
return {
|
|
27
27
|
label: this.translateService.instant(topic.label),
|
|
28
28
|
value: topic.value,
|
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,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
import { FormFieldWrapperComponent } from '../../../../../../../../libs/ui/layout/src'
|
|
23
24
|
import { TranslatePipe } from '@ngx-translate/core'
|
|
24
25
|
import {
|
|
26
|
+
FieldFocusDirective,
|
|
25
27
|
FormFieldDateComponent,
|
|
26
28
|
FormFieldLicenseComponent,
|
|
27
29
|
FormFieldTemporalExtentsComponent,
|
|
@@ -77,6 +79,7 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
|
|
|
77
79
|
FormFieldTopicsComponent,
|
|
78
80
|
TextFieldModule,
|
|
79
81
|
],
|
|
82
|
+
hostDirectives: [FieldFocusDirective],
|
|
80
83
|
})
|
|
81
84
|
export class FormFieldComponent {
|
|
82
85
|
@Input() uniqueIdentifier: string
|
|
@@ -92,6 +95,8 @@ export class FormFieldComponent {
|
|
|
92
95
|
@ViewChild('titleInput') titleInput: ElementRef
|
|
93
96
|
isOpenData = false
|
|
94
97
|
|
|
98
|
+
fieldFocus = inject(FieldFocusDirective)
|
|
99
|
+
|
|
95
100
|
toggleIsOpenData(event: boolean) {
|
|
96
101
|
this.isOpenData = event
|
|
97
102
|
}
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
}
|
|
3
35
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
track sectionTracker($index, section)
|
|
5
5
|
) {
|
|
6
6
|
@if (!section.hidden) {
|
|
7
|
-
<div
|
|
7
|
+
<div
|
|
8
|
+
class="flex flex-col gap-6 border p-8 rounded-[8px] shadow scroll-mt-[90px]"
|
|
9
|
+
gnUiFieldFocus
|
|
10
|
+
#sectionFocus="fieldFocus"
|
|
11
|
+
>
|
|
8
12
|
<div class="flex flex-col gap-2">
|
|
9
13
|
@if (section.labelKey) {
|
|
10
14
|
<div class="text-2xl font-title text-black" translate>
|
|
@@ -24,7 +28,6 @@
|
|
|
24
28
|
) {
|
|
25
29
|
@if (!field.config.hidden) {
|
|
26
30
|
<gn-ui-form-field
|
|
27
|
-
[id]="anchorIdPrefix + field.config.model"
|
|
28
31
|
[ngClass]="
|
|
29
32
|
field.config.gridColumnSpan === 1
|
|
30
33
|
? 'col-span-1'
|