geonetwork-ui 2.10.0-dev.88fb568d7 → 2.10.0-dev.89f0dfe6f
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 +520 -127
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +107 -29
- package/index.d.ts.map +1 -1
- package/package.json +2 -2
- 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/gn4.converter.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 +4 -0
- package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +14 -2
- package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -0
- package/src/libs/common/fixtures/src/lib/records.fixtures.ts +7 -0
- package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
- package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
- package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
- 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/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/record-form/form-field/field-focus.directive.ts +38 -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-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.component.css +37 -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.ts +59 -3
- package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
- package/src/libs/feature/notify-reuse/src/index.ts +1 -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 +1 -0
- package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
- package/src/libs/ui/elements/src/index.ts +2 -0
- package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -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 +37 -0
- package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
- 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/metadata-contact/metadata-contact.component.ts +2 -5
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
- package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
- package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
- 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/map-container/map-container.component.ts +2 -1
- package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +11 -10
- package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
- package/src/libs/util/app-config/src/lib/app-config.ts +36 -0
- package/src/libs/util/app-config/src/lib/model.ts +4 -0
- package/src/libs/util/app-config/src/lib/parse-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/utils/index.ts +1 -0
- package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
- package/tailwind.base.css +11 -2
- package/translations/de.json +10 -1
- package/translations/en.json +10 -1
- package/translations/es.json +10 -1
- package/translations/fr.json +10 -1
- package/translations/it.json +10 -1
- package/translations/nl.json +10 -1
- package/translations/pt.json +10 -1
- package/translations/sk.json +10 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { CommonModule } from '@angular/common'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
inject,
|
|
6
|
+
OnDestroy,
|
|
7
|
+
OnInit,
|
|
8
|
+
viewChildren,
|
|
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,8 +15,9 @@ import {
|
|
|
8
15
|
EditorFieldWithValue,
|
|
9
16
|
EditorSectionWithValues,
|
|
10
17
|
} from '../../+state/editor.models'
|
|
11
|
-
import { map } from 'rxjs'
|
|
18
|
+
import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
|
|
12
19
|
import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
20
|
+
import { switchMap } from 'rxjs/operators'
|
|
13
21
|
|
|
14
22
|
@Component({
|
|
15
23
|
selector: 'gn-ui-record-form',
|
|
@@ -19,13 +27,51 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
|
|
|
19
27
|
standalone: true,
|
|
20
28
|
imports: [CommonModule, FormFieldComponent, TranslateDirective],
|
|
21
29
|
})
|
|
22
|
-
export class RecordFormComponent {
|
|
30
|
+
export class RecordFormComponent implements OnInit, OnDestroy {
|
|
23
31
|
facade = inject(EditorFacade)
|
|
32
|
+
subscription = new Subscription()
|
|
24
33
|
|
|
25
34
|
recordUniqueIdentifier$ = this.facade.record$.pipe(
|
|
26
35
|
map((record) => record.uniqueIdentifier)
|
|
27
36
|
)
|
|
28
37
|
|
|
38
|
+
focusFieldWithPage$ = this.facade.focusedField$.pipe(
|
|
39
|
+
switchMap(
|
|
40
|
+
async (field) => [field, await this.getPageIndexForField(field)] as const
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
formFields = viewChildren(FormFieldComponent)
|
|
45
|
+
|
|
46
|
+
focusField(model: CatalogRecordKeys) {
|
|
47
|
+
const fields = this.formFields()
|
|
48
|
+
const field = fields.find((f) => f.model === model)
|
|
49
|
+
field?.fieldFocus.focusField()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ngOnInit() {
|
|
53
|
+
this.subscription.add(
|
|
54
|
+
this.focusFieldWithPage$
|
|
55
|
+
.pipe(
|
|
56
|
+
withLatestFrom(
|
|
57
|
+
this.facade.currentPage$,
|
|
58
|
+
([field, fieldPage], currentPage) =>
|
|
59
|
+
[field, fieldPage, currentPage] as const
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
.subscribe(([field, fieldPage, currentPage]) => {
|
|
63
|
+
if (fieldPage !== null && fieldPage !== currentPage) {
|
|
64
|
+
this.facade.setCurrentPage(fieldPage)
|
|
65
|
+
}
|
|
66
|
+
setTimeout(() => this.focusField(field))
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ngOnDestroy() {
|
|
72
|
+
this.subscription.unsubscribe()
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
|
|
30
76
|
if (!model) {
|
|
31
77
|
return
|
|
@@ -40,4 +86,14 @@ export class RecordFormComponent {
|
|
|
40
86
|
sectionTracker(index: number, section: EditorSectionWithValues) {
|
|
41
87
|
return section.labelKey
|
|
42
88
|
}
|
|
89
|
+
|
|
90
|
+
async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
|
|
91
|
+
const config = await firstValueFrom(this.facade.editorConfig$)
|
|
92
|
+
const pageIndex = config.pages.findIndex((page) =>
|
|
93
|
+
page.sections.some((section) =>
|
|
94
|
+
section.fields.some((field) => field.model === model)
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return pageIndex >= 0 ? pageIndex : null
|
|
98
|
+
}
|
|
43
99
|
}
|
|
@@ -19,9 +19,13 @@ export interface FormFieldConfig {
|
|
|
19
19
|
// This is used for instance to target only certain online resources in a field
|
|
20
20
|
type OnlineLinkResourceSpecifier = `onlineResourceType:link`
|
|
21
21
|
type DatasetDistributionsSpecifier = `onlineResourceType:!link`
|
|
22
|
+
// When set on the `contacts` field, contacts are rendered as editable detail
|
|
23
|
+
// fields (ContactDetailsFormComponent) instead of cards (ContactCardComponent)
|
|
24
|
+
type EditableContactDetailsSpecifier = `contact:editableDetails`
|
|
22
25
|
export type FieldModelSpecifier =
|
|
23
26
|
| OnlineLinkResourceSpecifier
|
|
24
27
|
| DatasetDistributionsSpecifier
|
|
28
|
+
| EditableContactDetailsSpecifier
|
|
25
29
|
|
|
26
30
|
export type FormFieldComponentName =
|
|
27
31
|
| 'form-field-constraints-shortcuts'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/notify-reuse-form/notify-reuse-form.component'
|
package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css
ADDED
|
File without changes
|
package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@if (record) {}
|
package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
Input,
|
|
6
|
+
Output,
|
|
7
|
+
} from '@angular/core'
|
|
8
|
+
import { ReuseRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'gn-ui-notify-reuse-form',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [],
|
|
14
|
+
templateUrl: './notify-reuse-form.component.html',
|
|
15
|
+
styleUrl: './notify-reuse-form.component.css',
|
|
16
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
|
+
})
|
|
18
|
+
export class NotifyReuseFormComponent {
|
|
19
|
+
@Input() record: ReuseRecord | null = null
|
|
20
|
+
@Output() recordChange = new EventEmitter<ReuseRecord>()
|
|
21
|
+
}
|
|
@@ -18,6 +18,8 @@ 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'
|
|
22
|
+
export * from './lib/contact-details/contact-details.component'
|
|
21
23
|
export * from './lib/notification/notification.component'
|
|
22
24
|
export * from './lib/record-api-form/record-api-form.component'
|
|
23
25
|
export * from './lib/thumbnail/thumbnail.component'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="bg-gray-50 rounded border border-gray-200 shadow-md p-4 flex flex-col gap-3 w-full"
|
|
3
|
+
data-test="contact-details"
|
|
4
|
+
>
|
|
5
|
+
@if (displayName) {
|
|
6
|
+
<div class="flex items-center gap-3">
|
|
7
|
+
@if (organization?.logoUrl?.href) {
|
|
8
|
+
<div
|
|
9
|
+
class="flex items-center justify-center rounded-md bg-white w-14 h-14 shrink-0 overflow-hidden"
|
|
10
|
+
>
|
|
11
|
+
<gn-ui-thumbnail
|
|
12
|
+
class="relative h-full w-full"
|
|
13
|
+
[thumbnailUrl]="organization.logoUrl.href"
|
|
14
|
+
fit="contain"
|
|
15
|
+
></gn-ui-thumbnail>
|
|
16
|
+
</div>
|
|
17
|
+
}
|
|
18
|
+
<span
|
|
19
|
+
class="font-title text-xl leading-tight"
|
|
20
|
+
data-test="contact-details-name"
|
|
21
|
+
>{{ displayName }}</span
|
|
22
|
+
>
|
|
23
|
+
</div>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
<!-- Email + phone: always shown; icon dimmed when field is absent -->
|
|
27
|
+
<div class="grid grid-cols-2 gap-1 rounded-md overflow-hidden">
|
|
28
|
+
<div class="bg-gray-100 flex items-center gap-2 px-3 py-2">
|
|
29
|
+
<ng-icon
|
|
30
|
+
class="!w-5 !h-5 !text-[20px] shrink-0"
|
|
31
|
+
[class.opacity-30]="!contact.email"
|
|
32
|
+
[class.opacity-75]="!!contact.email"
|
|
33
|
+
name="matMailOutline"
|
|
34
|
+
></ng-icon>
|
|
35
|
+
@if (contact.email) {
|
|
36
|
+
<a
|
|
37
|
+
[href]="'mailto:' + contact.email"
|
|
38
|
+
class="text-sm break-all hover:underline"
|
|
39
|
+
data-test="contact-details-email"
|
|
40
|
+
>{{ contact.email }}</a
|
|
41
|
+
>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
<div class="bg-gray-100 flex items-center gap-2 px-3 py-2">
|
|
45
|
+
<ng-icon
|
|
46
|
+
class="!w-5 !h-5 !text-[20px] shrink-0"
|
|
47
|
+
[class.opacity-30]="!contact.phone"
|
|
48
|
+
[class.opacity-75]="!!contact.phone"
|
|
49
|
+
name="matCallOutline"
|
|
50
|
+
></ng-icon>
|
|
51
|
+
@if (contact.phone) {
|
|
52
|
+
<span class="text-sm" data-test="contact-details-phone">{{
|
|
53
|
+
contact.phone
|
|
54
|
+
}}</span>
|
|
55
|
+
}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Address: always shown; icon dimmed when absent -->
|
|
60
|
+
<div class="bg-gray-100 rounded-md flex items-start gap-2 px-3 py-2">
|
|
61
|
+
<ng-icon
|
|
62
|
+
class="!w-5 !h-5 !text-[20px] shrink-0 mt-0.5"
|
|
63
|
+
[class.opacity-30]="!addressLines.length"
|
|
64
|
+
[class.opacity-75]="!!addressLines.length"
|
|
65
|
+
name="matLocationOnOutline"
|
|
66
|
+
></ng-icon>
|
|
67
|
+
@if (addressLines.length) {
|
|
68
|
+
<div class="flex flex-col" data-test="contact-details-address">
|
|
69
|
+
@for (line of addressLines; track line) {
|
|
70
|
+
<p class="text-sm m-0">{{ line }}</p>
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Website: only shown when org exists; icon dimmed when absent -->
|
|
77
|
+
@if (organization) {
|
|
78
|
+
<div class="bg-gray-100 rounded-md flex items-center gap-2 px-3 py-2">
|
|
79
|
+
<ng-icon
|
|
80
|
+
class="!w-5 !h-5 !text-[20px] shrink-0"
|
|
81
|
+
[class.opacity-30]="!organization.website"
|
|
82
|
+
[class.opacity-75]="!!organization.website"
|
|
83
|
+
name="matOpenInNew"
|
|
84
|
+
></ng-icon>
|
|
85
|
+
@if (organization.website) {
|
|
86
|
+
<a
|
|
87
|
+
[href]="organization.website.href"
|
|
88
|
+
target="_blank"
|
|
89
|
+
class="text-sm break-all hover:underline"
|
|
90
|
+
data-test="contact-details-website"
|
|
91
|
+
>{{ organization.website.href }}</a
|
|
92
|
+
>
|
|
93
|
+
}
|
|
94
|
+
</div>
|
|
95
|
+
}
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|
2
|
+
import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
3
|
+
import {
|
|
4
|
+
getAddressLines,
|
|
5
|
+
getIndividualDisplayName,
|
|
6
|
+
} from '../../../../../../libs/util/shared/src'
|
|
7
|
+
import { NgIcon, provideIcons } from '@ng-icons/core'
|
|
8
|
+
import { matMailOutline, matOpenInNew } from '@ng-icons/material-icons/baseline'
|
|
9
|
+
import {
|
|
10
|
+
matCallOutline,
|
|
11
|
+
matLocationOnOutline,
|
|
12
|
+
} from '@ng-icons/material-icons/outline'
|
|
13
|
+
import { ThumbnailComponent } from '../thumbnail/thumbnail.component'
|
|
14
|
+
import { TranslateModule } from '@ngx-translate/core'
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'gn-ui-contact-details',
|
|
18
|
+
templateUrl: './contact-details.component.html',
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
standalone: true,
|
|
21
|
+
imports: [NgIcon, ThumbnailComponent, TranslateModule],
|
|
22
|
+
viewProviders: [
|
|
23
|
+
provideIcons({
|
|
24
|
+
matCallOutline,
|
|
25
|
+
matLocationOnOutline,
|
|
26
|
+
matMailOutline,
|
|
27
|
+
matOpenInNew,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
export class ContactDetailsComponent {
|
|
32
|
+
@Input() contact: Individual
|
|
33
|
+
|
|
34
|
+
get organization() {
|
|
35
|
+
return this.contact?.organization
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get displayName(): string {
|
|
39
|
+
return getIndividualDisplayName(this.contact)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get addressLines(): string[] {
|
|
43
|
+
return getAddressLines(this.contact?.address)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<gn-ui-button
|
|
2
|
+
[type]="overlayOpen ? 'primary' : '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
|
+
(buttonClick)="toggleOverlay()"
|
|
6
|
+
cdkOverlayOrigin
|
|
7
|
+
#overlayOrigin="cdkOverlayOrigin"
|
|
8
|
+
>
|
|
9
|
+
<span
|
|
10
|
+
class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
|
|
11
|
+
[title]="displayName"
|
|
12
|
+
>{{ displayName }}</span
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
|
|
16
|
+
>
|
|
17
|
+
@if (overlayOpen) {
|
|
18
|
+
<ng-icon class="!w-6 !h-6 !text-[24px]" name="matClose"></ng-icon>
|
|
19
|
+
} @else {
|
|
20
|
+
<ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
|
|
21
|
+
}
|
|
22
|
+
</div>
|
|
23
|
+
</gn-ui-button>
|
|
24
|
+
|
|
25
|
+
<ng-template
|
|
26
|
+
cdkConnectedOverlay
|
|
27
|
+
[cdkConnectedOverlayOrigin]="overlayOrigin"
|
|
28
|
+
[cdkConnectedOverlayOpen]="overlayOpen"
|
|
29
|
+
[cdkConnectedOverlayPositions]="overlayPositions"
|
|
30
|
+
[cdkConnectedOverlayOffsetX]="overlayOffsetX"
|
|
31
|
+
(overlayOutsideClick)="closeOverlay()"
|
|
32
|
+
(detach)="closeOverlay()"
|
|
33
|
+
>
|
|
34
|
+
<div [style.width.px]="overlayWidth">
|
|
35
|
+
<gn-ui-contact-details [contact]="contact"></gn-ui-contact-details>
|
|
36
|
+
</div>
|
|
37
|
+
</ng-template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay'
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
ElementRef,
|
|
6
|
+
inject,
|
|
7
|
+
Input,
|
|
8
|
+
} from '@angular/core'
|
|
9
|
+
import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
10
|
+
import { NgIcon, provideIcons } from '@ng-icons/core'
|
|
11
|
+
import { matClose } from '@ng-icons/material-icons/baseline'
|
|
12
|
+
import { matInfoOutline } from '@ng-icons/material-icons/outline'
|
|
13
|
+
import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
|
|
14
|
+
import { getIndividualDisplayName } from '../../../../../../libs/util/shared/src'
|
|
15
|
+
import { ContactDetailsComponent } from '../contact-details/contact-details.component'
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'gn-ui-contact-pill',
|
|
19
|
+
templateUrl: './contact-pill.component.html',
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [NgIcon, ButtonComponent, OverlayModule, ContactDetailsComponent],
|
|
23
|
+
viewProviders: [provideIcons({ matClose, matInfoOutline })],
|
|
24
|
+
})
|
|
25
|
+
export class ContactPillComponent {
|
|
26
|
+
@Input() contact: Individual
|
|
27
|
+
|
|
28
|
+
private host = inject(ElementRef<HTMLElement>)
|
|
29
|
+
|
|
30
|
+
overlayOpen = false
|
|
31
|
+
overlayWidth = 0
|
|
32
|
+
overlayOffsetX = 0
|
|
33
|
+
overlayPositions: ConnectedPosition[] = [
|
|
34
|
+
{
|
|
35
|
+
originX: 'start',
|
|
36
|
+
originY: 'bottom',
|
|
37
|
+
overlayX: 'start',
|
|
38
|
+
overlayY: 'top',
|
|
39
|
+
offsetY: 4,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
originX: 'start',
|
|
43
|
+
originY: 'top',
|
|
44
|
+
overlayX: 'start',
|
|
45
|
+
overlayY: 'bottom',
|
|
46
|
+
offsetY: -4,
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
get displayName(): string {
|
|
51
|
+
return getIndividualDisplayName(this.contact)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toggleOverlay() {
|
|
55
|
+
if (!this.overlayOpen) {
|
|
56
|
+
// Calculate the width and horizontal offset of the overlay to align it with the parent element
|
|
57
|
+
const parent =
|
|
58
|
+
this.host.nativeElement.parentElement.getBoundingClientRect()
|
|
59
|
+
const pill = this.host.nativeElement.getBoundingClientRect()
|
|
60
|
+
this.overlayWidth = parent.width
|
|
61
|
+
this.overlayOffsetX = parent.left - pill.left
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.overlayOpen = !this.overlayOpen
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
closeOverlay() {
|
|
68
|
+
this.overlayOpen = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -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 }>
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
matCallOutline,
|
|
22
22
|
matLocationOnOutline,
|
|
23
23
|
} from '@ng-icons/material-icons/outline'
|
|
24
|
-
|
|
24
|
+
import { getAddressLines } from '../../../../../../libs/util/shared/src'
|
|
25
25
|
import { TranslateDirective } from '@ngx-translate/core'
|
|
26
26
|
|
|
27
27
|
@Component({
|
|
@@ -59,10 +59,7 @@ export class MetadataContactComponent {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
get address() {
|
|
62
|
-
|
|
63
|
-
.split(',')
|
|
64
|
-
.map((part) => part.trim())
|
|
65
|
-
return addressParts
|
|
62
|
+
return getAddressLines(this.contacts[0]?.address)
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
onOrganizationClick() {
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
<gn-ui-max-lines [maxLines]="7">
|
|
29
29
|
<div class="metadata-info-keywords sm:pb-4 flex flex-wrap gap-2">
|
|
30
30
|
@for (keyword of metadata.keywords; track keyword) {
|
|
31
|
-
<gn-ui-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
<gn-ui-button
|
|
32
|
+
type="primary-light"
|
|
33
|
+
class="inline-block opacity-70"
|
|
34
|
+
extraClass="lowercase text-sm py-1 px-2"
|
|
35
|
+
(buttonClick)="onKeywordClick(keyword)"
|
|
36
|
+
>{{ keyword.label }}</gn-ui-button
|
|
36
37
|
>
|
|
37
38
|
}
|
|
38
39
|
</div>
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
data-test="usage-panel"
|
|
53
54
|
>
|
|
54
55
|
<div class="flex flex-col gap-[10px] mr-4 py-[12px] rounded text-gray-900">
|
|
55
|
-
@for (license of licenses; track
|
|
56
|
+
@for (license of licenses; track $index) {
|
|
56
57
|
@if (license.url) {
|
|
57
58
|
<div class="text-primary">
|
|
58
59
|
<a
|
|
@@ -105,9 +106,29 @@
|
|
|
105
106
|
</div>
|
|
106
107
|
</gn-ui-expandable-panel>
|
|
107
108
|
}
|
|
109
|
+
@if (metadata.contactsForResource?.length) {
|
|
110
|
+
<gn-ui-expandable-panel
|
|
111
|
+
[title]="'record.metadata.resource.contacts' | translate"
|
|
112
|
+
data-test="contacts-panel"
|
|
113
|
+
>
|
|
114
|
+
<div class="flex flex-col gap-1 pt-3 pb-4">
|
|
115
|
+
@for (group of contactGroups; track group.role) {
|
|
116
|
+
<div class="flex flex-col gap-1 rounded bg-gray-50 py-4 px-2">
|
|
117
|
+
<p class="text-xs font-normal text-black">
|
|
118
|
+
{{ group.roleLabel | translate }}
|
|
119
|
+
</p>
|
|
120
|
+
<div class="grid gap-1 grid-cols-1 md:grid-cols-2">
|
|
121
|
+
@for (contact of group.contacts; track contact.email) {
|
|
122
|
+
<gn-ui-contact-pill [contact]="contact"></gn-ui-contact-pill>
|
|
123
|
+
}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
}
|
|
127
|
+
</div>
|
|
128
|
+
</gn-ui-expandable-panel>
|
|
129
|
+
}
|
|
108
130
|
@if (
|
|
109
131
|
(metadata.kind === 'dataset' && metadata.lineage) ||
|
|
110
|
-
resourceContact ||
|
|
111
132
|
metadata.resourceCreated ||
|
|
112
133
|
metadata.resourcePublished ||
|
|
113
134
|
metadata.resourceUpdated ||
|
|
@@ -129,66 +150,6 @@
|
|
|
129
150
|
</p>
|
|
130
151
|
</div>
|
|
131
152
|
}
|
|
132
|
-
@if (resourceContact) {
|
|
133
|
-
<div
|
|
134
|
-
class="flex flex-row gap-6 mt-5 mb-8 resource-contact"
|
|
135
|
-
data-test="details-panel-resource-contact"
|
|
136
|
-
>
|
|
137
|
-
@if (resourceContact.organization?.logoUrl?.href) {
|
|
138
|
-
<div
|
|
139
|
-
class="flex items-center justify-center border-solid border border-gray-300 rounded-md bg-white h-32 overflow-hidden"
|
|
140
|
-
>
|
|
141
|
-
<gn-ui-thumbnail
|
|
142
|
-
class="relative h-full w-full"
|
|
143
|
-
[thumbnailUrl]="resourceContact.organization.logoUrl.href"
|
|
144
|
-
fit="contain"
|
|
145
|
-
></gn-ui-thumbnail>
|
|
146
|
-
</div>
|
|
147
|
-
}
|
|
148
|
-
<div class="flex flex-col gap-1">
|
|
149
|
-
<p class="text-sm font-medium" translate>record.metadata.producer</p>
|
|
150
|
-
<div
|
|
151
|
-
class="text-primary font-title text-21 mr-2 cursor-pointer hover:underline"
|
|
152
|
-
data-cy="organization-name"
|
|
153
|
-
>
|
|
154
|
-
{{ resourceContact.organization?.name }}
|
|
155
|
-
</div>
|
|
156
|
-
@if (resourceContact.organization?.website) {
|
|
157
|
-
<div>
|
|
158
|
-
<a
|
|
159
|
-
[href]="resourceContact.organization.website"
|
|
160
|
-
target="_blank"
|
|
161
|
-
class="contact-website text-primary text-sm cursor-pointer hover:underline transition-all"
|
|
162
|
-
>{{ resourceContact.organization.website }}
|
|
163
|
-
<ng-icon
|
|
164
|
-
class="!w-[12px] !h-[12px] !text-[12px] opacity-75 shrink-0"
|
|
165
|
-
name="matOpenInNew"
|
|
166
|
-
></ng-icon>
|
|
167
|
-
</a>
|
|
168
|
-
</div>
|
|
169
|
-
}
|
|
170
|
-
@if (resourceContact.email) {
|
|
171
|
-
<div class="mt-4">
|
|
172
|
-
<div class="flex">
|
|
173
|
-
<ng-icon
|
|
174
|
-
class="!w-5 !h-5 !text-[20px] opacity-75 shrink-0"
|
|
175
|
-
name="matMailOutline"
|
|
176
|
-
></ng-icon>
|
|
177
|
-
@if (resourceContact.email) {
|
|
178
|
-
<a
|
|
179
|
-
[href]="'mailto:' + resourceContact.email"
|
|
180
|
-
class="text-sm hover:underline ml-2"
|
|
181
|
-
target="_blank"
|
|
182
|
-
data-cy="contact-email"
|
|
183
|
-
>{{ resourceContact?.email }}</a
|
|
184
|
-
>
|
|
185
|
-
}
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
}
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
}
|
|
192
153
|
<div
|
|
193
154
|
class="py-6 px-6 rounded bg-gray-100 grid grid-cols-2 gap-y-6 gap-x-[20px] text-gray-700"
|
|
194
155
|
>
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
} from '@angular/core'
|
|
9
9
|
import {
|
|
10
10
|
CatalogRecord,
|
|
11
|
+
Individual,
|
|
11
12
|
Keyword,
|
|
13
|
+
Role,
|
|
14
|
+
RoleLabels,
|
|
12
15
|
} from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
13
16
|
import { DateService, getTemporalRangeUnion } from '../../../../../../libs/util/shared/src'
|
|
14
17
|
import { MarkdownParserComponent } from '../markdown-parser/markdown-parser.component'
|
|
@@ -19,17 +22,18 @@ import {
|
|
|
19
22
|
import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
|
|
20
23
|
import {
|
|
21
24
|
BadgeComponent,
|
|
25
|
+
ButtonComponent,
|
|
22
26
|
CopyTextButtonComponent,
|
|
23
27
|
} from '../../../../../../libs/ui/inputs/src'
|
|
24
28
|
import { ContentGhostComponent } from '../content-ghost/content-ghost.component'
|
|
25
29
|
import { NgIcon, provideIcons } from '@ng-icons/core'
|
|
26
30
|
import { matOpenInNew } from '@ng-icons/material-icons/baseline'
|
|
27
31
|
import { matMailOutline } from '@ng-icons/material-icons/outline'
|
|
28
|
-
import { ThumbnailComponent } from '../thumbnail/thumbnail.component'
|
|
29
32
|
import { GnUiLinkifyDirective } from './linkify.directive'
|
|
30
33
|
import { GnUiHumanizeDateDirective } from '../../../../../../libs/util/shared/src'
|
|
31
34
|
|
|
32
35
|
import { SpatialExtentComponent } from '../../../../../../libs/ui/map/src'
|
|
36
|
+
import { ContactPillComponent } from '../contact-pill/contact-pill.component'
|
|
33
37
|
|
|
34
38
|
@Component({
|
|
35
39
|
selector: 'gn-ui-metadata-info',
|
|
@@ -42,15 +46,16 @@ import { SpatialExtentComponent } from '../../../../../../libs/ui/map/src'
|
|
|
42
46
|
TranslatePipe,
|
|
43
47
|
MarkdownParserComponent,
|
|
44
48
|
ExpandablePanelComponent,
|
|
49
|
+
ButtonComponent,
|
|
45
50
|
BadgeComponent,
|
|
46
51
|
ContentGhostComponent,
|
|
47
|
-
ThumbnailComponent,
|
|
48
52
|
MaxLinesComponent,
|
|
49
53
|
CopyTextButtonComponent,
|
|
50
54
|
NgIcon,
|
|
51
55
|
GnUiLinkifyDirective,
|
|
52
56
|
GnUiHumanizeDateDirective,
|
|
53
57
|
SpatialExtentComponent,
|
|
58
|
+
ContactPillComponent,
|
|
54
59
|
],
|
|
55
60
|
viewProviders: [
|
|
56
61
|
provideIcons({
|
|
@@ -132,18 +137,33 @@ export class MetadataInfoComponent {
|
|
|
132
137
|
return getTemporalRangeUnion(temporalExtents, this.dateService)
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
get shownOrganization() {
|
|
136
|
-
return this.metadata.ownerOrganization
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
get resourceContact() {
|
|
140
|
-
return this.metadata.contactsForResource?.[0]
|
|
141
|
-
}
|
|
142
|
-
|
|
143
140
|
fieldReady(propName: string) {
|
|
144
141
|
return !this.incomplete || propName in this.metadata
|
|
145
142
|
}
|
|
146
143
|
|
|
144
|
+
get contactGroups(): {
|
|
145
|
+
role: Role
|
|
146
|
+
roleLabel: string
|
|
147
|
+
contacts: Individual[]
|
|
148
|
+
}[] {
|
|
149
|
+
const groups: { role: Role; roleLabel: string; contacts: Individual[] }[] =
|
|
150
|
+
[]
|
|
151
|
+
const indexByRole = new Map<Role, number>()
|
|
152
|
+
for (const contact of this.metadata.contactsForResource ?? []) {
|
|
153
|
+
if (indexByRole.has(contact.role)) {
|
|
154
|
+
groups[indexByRole.get(contact.role)].contacts.push(contact)
|
|
155
|
+
} else {
|
|
156
|
+
indexByRole.set(contact.role, groups.length)
|
|
157
|
+
groups.push({
|
|
158
|
+
role: contact.role,
|
|
159
|
+
roleLabel: RoleLabels.get(contact.role),
|
|
160
|
+
contacts: [contact],
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return groups
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
onKeywordClick(keyword: Keyword) {
|
|
148
168
|
this.keyword.emit(keyword)
|
|
149
169
|
}
|
|
@@ -26,6 +26,7 @@ export class ButtonComponent {
|
|
|
26
26
|
| 'light'
|
|
27
27
|
| 'gray'
|
|
28
28
|
| 'black'
|
|
29
|
+
| 'primary-light'
|
|
29
30
|
) {
|
|
30
31
|
// btn-classes are written in full to be picked up by tailwind
|
|
31
32
|
switch (value) {
|
|
@@ -47,6 +48,9 @@ export class ButtonComponent {
|
|
|
47
48
|
case 'black':
|
|
48
49
|
this.btnClass = 'gn-ui-btn-black'
|
|
49
50
|
break
|
|
51
|
+
case 'primary-light':
|
|
52
|
+
this.btnClass = 'gn-ui-btn-primary-light'
|
|
53
|
+
break
|
|
50
54
|
case 'default':
|
|
51
55
|
default:
|
|
52
56
|
this.btnClass = 'gn-ui-btn-default'
|