geonetwork-ui 2.10.0-dev.69145ed6a → 2.10.0-dev.7e58935b2
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 +240 -55
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +34 -6
- package/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +4 -0
- 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 +38 -0
- package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +0 -1
- 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.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.css +0 -3
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +0 -1
- package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +27 -25
- package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
- package/src/libs/ui/elements/src/index.ts +1 -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 +23 -2
- package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +51 -7
- 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 +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/util/shared/src/lib/utils/user-display.ts +9 -0
- package/translations/de.json +8 -1
- package/translations/en.json +8 -1
- package/translations/es.json +8 -1
- package/translations/fr.json +8 -1
- package/translations/it.json +8 -1
- package/translations/nl.json +8 -1
- package/translations/pt.json +8 -1
- package/translations/sk.json +8 -1
package/package.json
CHANGED
|
@@ -44,6 +44,10 @@ export class AuthService {
|
|
|
44
44
|
window.location.href
|
|
45
45
|
).toString()
|
|
46
46
|
)
|
|
47
|
+
.replace(
|
|
48
|
+
'${current_path}',
|
|
49
|
+
this.location.prepareExternalUrl(this.location.path())
|
|
50
|
+
)
|
|
47
51
|
.replace('${lang2}', toLang2(this.translateService.currentLang))
|
|
48
52
|
.replace('${lang3}', toLang3(this.translateService.currentLang))
|
|
49
53
|
}
|
|
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,38 @@
|
|
|
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() {
|
|
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
|
+
const target =
|
|
29
|
+
host.querySelector<HTMLElement>(
|
|
30
|
+
'input, textarea, select, [contenteditable="true"]'
|
|
31
|
+
) ??
|
|
32
|
+
host.querySelector<HTMLElement>(
|
|
33
|
+
'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
34
|
+
)
|
|
35
|
+
target?.focus({ preventScroll: true })
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -139,7 +139,6 @@ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
|
|
|
139
139
|
this.editorFacade.setFieldVisibility({ model }, visible)
|
|
140
140
|
})
|
|
141
141
|
}
|
|
142
|
-
hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
|
|
143
142
|
hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
|
|
144
143
|
hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
|
|
145
144
|
})
|
|
@@ -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
|
*/
|
package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
scroll-margin-top: 90px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
:host.gn-ui-field-focus-glow {
|
|
6
|
+
position: relative;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:host.gn-ui-field-focus-glow::after {
|
|
10
|
+
content: '';
|
|
11
|
+
position: absolute;
|
|
12
|
+
inset: -8px;
|
|
13
|
+
border-radius: 8px;
|
|
14
|
+
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
animation: gn-ui-field-focus-glow 3s ease-out forwards;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host.gn-ui-field-focus-glow::before {
|
|
20
|
+
content: '';
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: -16px;
|
|
23
|
+
border: 2px dashed var(--color-primary);
|
|
24
|
+
border-radius: 10px;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
animation: gn-ui-field-focus-glow 3s ease-out forwards;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@keyframes gn-ui-field-focus-glow {
|
|
30
|
+
0%,
|
|
31
|
+
55% {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
100% {
|
|
35
|
+
opacity: 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
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
|
}
|
|
@@ -2,10 +2,10 @@ import { CommonModule } from '@angular/common'
|
|
|
2
2
|
import {
|
|
3
3
|
ChangeDetectionStrategy,
|
|
4
4
|
Component,
|
|
5
|
-
ElementRef,
|
|
6
5
|
inject,
|
|
7
6
|
OnDestroy,
|
|
8
7
|
OnInit,
|
|
8
|
+
viewChildren,
|
|
9
9
|
} from '@angular/core'
|
|
10
10
|
import { EditorFacade } from '../../+state/editor.facade'
|
|
11
11
|
import { EditorFieldValue } from '../../models'
|
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
EditorFieldWithValue,
|
|
16
16
|
EditorSectionWithValues,
|
|
17
17
|
} from '../../+state/editor.models'
|
|
18
|
-
import {
|
|
18
|
+
import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
|
|
19
19
|
import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
20
|
+
import { switchMap } from 'rxjs/operators'
|
|
20
21
|
|
|
21
22
|
@Component({
|
|
22
23
|
selector: 'gn-ui-record-form',
|
|
@@ -27,41 +28,42 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
|
|
|
27
28
|
imports: [CommonModule, FormFieldComponent, TranslateDirective],
|
|
28
29
|
})
|
|
29
30
|
export class RecordFormComponent implements OnInit, OnDestroy {
|
|
30
|
-
anchorIdPrefix = 'gn-ui--field-'
|
|
31
31
|
facade = inject(EditorFacade)
|
|
32
|
-
private el = inject(ElementRef)
|
|
33
32
|
subscription = new Subscription()
|
|
34
33
|
|
|
35
34
|
recordUniqueIdentifier$ = this.facade.record$.pipe(
|
|
36
35
|
map((record) => record.uniqueIdentifier)
|
|
37
36
|
)
|
|
38
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
|
+
|
|
39
52
|
ngOnInit() {
|
|
40
53
|
this.subscription.add(
|
|
41
|
-
this.
|
|
54
|
+
this.focusFieldWithPage$
|
|
42
55
|
.pipe(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
field
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
),
|
|
49
|
-
}))
|
|
56
|
+
withLatestFrom(
|
|
57
|
+
this.facade.currentPage$,
|
|
58
|
+
([field, fieldPage], currentPage) =>
|
|
59
|
+
[field, fieldPage, currentPage] as const
|
|
60
|
+
)
|
|
50
61
|
)
|
|
51
|
-
.subscribe(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.facade.setCurrentPage(pageIndex)
|
|
55
|
-
this.el.nativeElement.scrollIntoView({
|
|
56
|
-
behavior: 'instant',
|
|
57
|
-
block: 'start',
|
|
58
|
-
})
|
|
62
|
+
.subscribe(([field, fieldPage, currentPage]) => {
|
|
63
|
+
if (fieldPage !== null && fieldPage !== currentPage) {
|
|
64
|
+
this.facade.setCurrentPage(fieldPage)
|
|
59
65
|
}
|
|
60
|
-
setTimeout(() =>
|
|
61
|
-
document
|
|
62
|
-
.getElementById(this.anchorIdPrefix + field)
|
|
63
|
-
?.scrollIntoView({ behavior: 'instant', block: 'start' })
|
|
64
|
-
)
|
|
66
|
+
setTimeout(() => this.focusField(field))
|
|
65
67
|
})
|
|
66
68
|
)
|
|
67
69
|
}
|
|
@@ -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'
|
|
@@ -19,6 +19,7 @@ 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
21
|
export * from './lib/contact-pill/contact-pill.component'
|
|
22
|
+
export * from './lib/contact-details/contact-details.component'
|
|
22
23
|
export * from './lib/notification/notification.component'
|
|
23
24
|
export * from './lib/record-api-form/record-api-form.component'
|
|
24
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
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<gn-ui-button
|
|
2
|
-
type="primary-light"
|
|
2
|
+
[type]="overlayOpen ? 'primary' : 'primary-light'"
|
|
3
3
|
extraClass="group w-full min-h-12 gap-3 justify-between py-2 pl-5 pr-4 rounded"
|
|
4
4
|
data-test="contact-pill"
|
|
5
|
+
(buttonClick)="toggleOverlay()"
|
|
6
|
+
cdkOverlayOrigin
|
|
7
|
+
#overlayOrigin="cdkOverlayOrigin"
|
|
5
8
|
>
|
|
6
9
|
<span
|
|
7
10
|
class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
|
|
@@ -11,6 +14,24 @@
|
|
|
11
14
|
<div
|
|
12
15
|
class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
|
|
13
16
|
>
|
|
14
|
-
|
|
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
|
+
}
|
|
15
22
|
</div>
|
|
16
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>
|