Skip to content

Commit f8b8ac0

Browse files
authored
Merge pull request DSpace#5097 from 4Science/task/main/DURACOM-444
[DSpace-CRIS] Nested / Basic Hierarchical Metadata (Frontend)
2 parents 02fb704 + a3005a7 commit f8b8ac0

57 files changed

Lines changed: 2716 additions & 418 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/config.example.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ form:
164164
validatorMap:
165165
required: required
166166
regex: pattern
167+
# If true it enables the button "Duplicate" inside inline form groups.
168+
# The button will give the possibility to duplicate the whole form section copying the metadata values as well.
169+
showInlineGroupDuplicateButton: false
167170

168171
# Notification settings
169172
notifications:
@@ -202,6 +205,10 @@ submission:
202205
# default configuration
203206
- name: default
204207
style: ''
208+
# Icons that should remain visible even when no authority value is present for the metadata field.
209+
# This is useful for fields where you want to display an icon regardless of whether the value has an authority link.
210+
# Example: ['fas fa-user'] will show the user icon for author fields even without authority data.
211+
iconsVisibleWithNoAuthority: ['fas fa-user']
205212
authority:
206213
confidence:
207214
# NOTE: example of configuration

src/app/core/json-patch/builder/json-patch-operations-builder.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export class JsonPatchOperationsBuilder {
6464
* the value to update the referenced path
6565
* @param plain
6666
* a boolean representing if the value to be added is a plain text value
67-
* @param securityLevel
6867
* @param language
6968
*/
7069
replace(path: JsonPatchOperationPathObject, value, plain = false, language = null) {

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
[class.d-none]="model.hidden"
33
[formGroup]="group"
44
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
5+
@let showHint = shouldShowHint();
6+
@let showVirtualHint = shouldShowVirtualMetadataHint();
7+
@let showClearfix = shouldShowClearfix();
8+
@let showErrors = shouldShowErrorMessages();
9+
510
@if (!isCheckbox && hasLabel) {
611
<label
712
[id]="'label_' + model.id"
@@ -19,16 +24,15 @@
1924
<ng-container #componentViewContainer></ng-container>
2025
</div>
2126

22-
@if (hasHint && (formBuilderService.hasArrayGroupValue(model) || (!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)) {
23-
<small
24-
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
27+
@if (showHint) {
28+
<small class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
2529
}
2630
<!-- In case of repeatable fields show empty space for all elements except the first -->
27-
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {
31+
@if (showClearfix) {
2832
<div class="clearfix w-100 mb-2"></div>
2933
}
3034

31-
@if (!model.hideErrorMessages && showErrorMessages) {
35+
@if (showErrors) {
3236
<div [id]="id + '_errors'"
3337
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
3438
@for (message of errorMessages; track message) {
@@ -99,11 +103,11 @@
99103
>
100104
</ds-existing-relation-list-element>
101105
}
102-
@if (hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)) {
106+
@if (showVirtualHint) {
103107
<small
104108
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
105109
}
106-
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {
110+
@if (showClearfix) {
107111
<div class="clearfix w-100 mb-2"></div>
108112
}
109113
}

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import { SubmissionObjectService } from '../../../../submission/submission-objec
8181
import { LiveRegionService } from '../../../live-region/live-region.service';
8282
import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub';
8383
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
84+
import { getMockFormBuilderService } from '../../testing/form-builder-service.mock';
8485
import { FormBuilderService } from '../form-builder.service';
8586
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
8687
import { dsDynamicFormControlMapFn } from './ds-dynamic-form-control-map-fn';
@@ -101,6 +102,7 @@ import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.compone
101102
import { DynamicOneboxModel } from './models/onebox/dynamic-onebox.model';
102103
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
103104
import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model';
105+
import { DsDynamicRelationInlineGroupComponent } from './models/relation-inline-group/dynamic-relation-inline-group.components';
104106
import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
105107
import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
106108
import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component';
@@ -186,6 +188,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
186188
submissionId: '1234',
187189
id: 'relationGroup',
188190
formConfiguration: [],
191+
isInlineGroup: false,
189192
mandatoryField: '',
190193
name: 'relationGroup',
191194
relationFields: [],
@@ -195,6 +198,20 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
195198
metadataFields: [],
196199
hasSelectableMetadata: false,
197200
}),
201+
new DynamicRelationGroupModel({
202+
submissionId: '1234',
203+
id: 'inlineRelationGroup',
204+
formConfiguration: [],
205+
isInlineGroup: true,
206+
mandatoryField: '',
207+
name: 'inlineRelationGroup',
208+
relationFields: [],
209+
scopeUUID: '',
210+
submissionScope: '',
211+
repeatable: false,
212+
metadataFields: [],
213+
hasSelectableMetadata: false,
214+
}),
198215
new DynamicDsDatePickerModel({ id: 'datepicker', repeatable: false }),
199216
new DynamicLookupModel({
200217
id: 'lookup',
@@ -244,7 +261,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
244261
{ provide: Store, useValue: {} },
245262
{ provide: RelationshipDataService, useValue: {} },
246263
{ provide: SelectableListService, useValue: {} },
247-
{ provide: FormBuilderService, useValue: {} },
264+
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
248265
{ provide: SubmissionService, useValue: {} },
249266
{
250267
provide: SubmissionObjectService,
@@ -392,10 +409,11 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
392409
expect(testFn(formModel[19])).toEqual(DsDynamicListComponent);
393410
expect(testFn(formModel[20])).toEqual(DsDynamicListComponent);
394411
expect(testFn(formModel[21])).toEqual(DsDynamicRelationGroupComponent);
395-
expect(testFn(formModel[22])).toEqual(DsDatePickerComponent);
396-
expect(testFn(formModel[23])).toEqual(DsDynamicLookupComponent);
412+
expect(testFn(formModel[22])).toEqual(DsDynamicRelationInlineGroupComponent);
413+
expect(testFn(formModel[23])).toEqual(DsDatePickerComponent);
397414
expect(testFn(formModel[24])).toEqual(DsDynamicLookupComponent);
398-
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
415+
expect(testFn(formModel[25])).toEqual(DsDynamicLookupComponent);
416+
expect(testFn(formModel[26])).toEqual(DsDynamicFormGroupComponent);
399417
});
400418

401419
describe('store action subscriptions', () => {

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,4 +516,85 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
516516
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
517517

518518
}
519+
520+
/**
521+
* Determines whether a form field should be validated based on its parent group's state.
522+
* @returns {boolean} True if the field should be validated, false otherwise
523+
*/
524+
isNotRequiredGroupAndEmpty(): boolean {
525+
const parent = this.model.parent;
526+
// Check if the model is part of a group, the group needs to be an inner form and be in the submission form not in a nested form.
527+
// The check hasValue(parent.parent) tells if the parent is in the submission or in a modal (nested cases)
528+
if (hasValue(parent) && parent.type === 'GROUP' && this.model.isModelOfInnerForm && hasValue(parent.parent)) {
529+
530+
const groupHasSomeValue = parent.group.some(elem => !!elem.value);
531+
532+
if (!groupHasSomeValue && !parent.isRequired && parent.group?.length > 1) {
533+
this.group.reset();
534+
}
535+
536+
return (groupHasSomeValue && !parent.isRequired) || (hasValue(parent.isRequired) && parent.isRequired);
537+
} else {
538+
return true;
539+
}
540+
}
541+
542+
/**
543+
* Determines whether the hint should be displayed for the current field.
544+
* Hint is shown when:
545+
* - The field has a hint
546+
* - It's the last element in a repeatable group OR non-repeatable field OR has array group value
547+
* - No error messages are currently displayed
548+
* @returns {boolean} True if hint should be displayed, false otherwise
549+
*/
550+
shouldShowHint(): boolean {
551+
return this.hasHint &&
552+
(this.formBuilderService.hasArrayGroupValue(this.model) ||
553+
((!this.model.repeatable && (!(this.model?.isModelOfNotRepeatableGroup) || this.model?.isModelOfNotRepeatableGroup && this.context?.index === this.context?.context?.groups?.length - 1)) && (this.isRelationship === false || this.value?.value === null)) ||
554+
(this.model.repeatable === true && this.context?.index === this.context?.context?.groups?.length - 1)) &&
555+
(!this.showErrorMessages || this.errorMessages.length === 0);
556+
}
557+
558+
/**
559+
* Determines whether error messages should be displayed for the current field.
560+
* Error messages are shown when:
561+
* - Error messages are not hidden by the model configuration
562+
* - There are error messages to show
563+
* - For non-repeatable groups: only shown on the last element
564+
* - The field passes the required group validation check
565+
* @returns {boolean} True if error messages should be displayed, false otherwise
566+
*/
567+
shouldShowErrorMessages(): boolean {
568+
return !this.model.hideErrorMessages &&
569+
this.showErrorMessages &&
570+
(!(this.model?.isModelOfNotRepeatableGroup) ||
571+
this.model?.isModelOfNotRepeatableGroup && this.context?.index === this.context?.context?.groups?.length - 1) &&
572+
this.isNotRequiredGroupAndEmpty();
573+
}
574+
575+
/**
576+
* Determines whether the hint should be displayed for virtual metadata fields.
577+
* Hint is shown when:
578+
* - The field has a hint
579+
* - It's non-repeatable OR the last element in a repeatable group
580+
* - No error messages are currently displayed
581+
* @returns {boolean} True if hint should be displayed for virtual metadata, false otherwise
582+
*/
583+
shouldShowVirtualMetadataHint(): boolean {
584+
return this.hasHint &&
585+
(this.model.repeatable === false || this.context?.index === this.context?.context?.groups?.length - 1) &&
586+
(!this.showErrorMessages || this.errorMessages.length === 0);
587+
}
588+
589+
/**
590+
* Determines whether a clearfix spacer should be displayed after the field.
591+
* Clearfix is shown when:
592+
* - The parent has multiple groups (more than 1)
593+
* - No error messages are currently displayed
594+
* @returns {boolean} True if clearfix should be displayed, false otherwise
595+
*/
596+
shouldShowClearfix(): boolean {
597+
return this.context?.parent?.groups?.length > 1 &&
598+
(!this.showErrorMessages || this.errorMessages.length === 0);
599+
}
519600
}

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-l
4444
import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.component';
4545
import { DYNAMIC_FORM_CONTROL_TYPE_ONEBOX } from './models/onebox/dynamic-onebox.model';
4646
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
47+
import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model';
48+
import { DsDynamicRelationInlineGroupComponent } from './models/relation-inline-group/dynamic-relation-inline-group.components';
4749
import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
4850
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
4951
import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component';
@@ -93,7 +95,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
9395
return DsDynamicTagComponent;
9496

9597
case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP:
96-
return DsDynamicRelationGroupComponent;
98+
return (model as DynamicRelationGroupModel).isInlineGroup ? DsDynamicRelationInlineGroupComponent : DsDynamicRelationGroupComponent;
9799

98100
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
99101
return DsDatePickerComponent;

src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,13 @@
7171
background-color: var(--bs-gray-400);
7272
}
7373
}
74+
75+
76+
.grey-background{
77+
background-color: #f3f3f3;
78+
margin-bottom: 10px;
79+
padding-top: 10px;
80+
margin-left: -1rem;
81+
margin-right: -1rem;
82+
padding-right: 0px;
83+
}

src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
120120
* If the drag feature is disabled for this DynamicRowArrayModel.
121121
*/
122122
get dragDisabled(): boolean {
123-
return this.model.groups.length === 1 || !this.model.isDraggable;
123+
return this.model.groups.length === 1 || !this.model.isDraggable || this.model.notRepeatable;
124124
}
125125

126126
/**

src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { LanguageCode } from '@dspace/core/shared/form/models/form-field-languag
22
import { FormFieldMetadataValueObject } from '@dspace/core/shared/form/models/form-field-metadata-value.model';
33
import { RelationshipOptions } from '@dspace/core/shared/relationship-options.model';
44
import { VocabularyOptions } from '@dspace/core/submission/vocabularies/models/vocabulary-options.model';
5-
import { hasValue } from '@dspace/shared/utils/empty.util';
65
import {
6+
hasValue,
7+
isNotUndefined,
8+
} from '@dspace/shared/utils/empty.util';
9+
import {
10+
AUTOCOMPLETE_OFF,
711
DynamicFormControlLayout,
812
DynamicFormControlRelation,
913
DynamicInputModel,
@@ -27,6 +31,7 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
2731
metadataValue?: FormFieldMetadataValueObject;
2832
isModelOfInnerForm?: boolean;
2933
hideErrorMessages?: boolean;
34+
isModelOfNotRepeatableGroup?: boolean;
3035
}
3136

3237
export class DsDynamicInputModel extends DynamicInputModel {
@@ -46,10 +51,12 @@ export class DsDynamicInputModel extends DynamicInputModel {
4651
@serializable() metadataValue: FormFieldMetadataValueObject;
4752
@serializable() isModelOfInnerForm: boolean;
4853
@serializable() hideErrorMessages?: boolean;
54+
@serializable() isModelOfNotRepeatableGroup = false;
4955

5056

5157
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
5258
super(config, layout);
59+
this.autoComplete = AUTOCOMPLETE_OFF;
5360
this.repeatable = config.repeatable;
5461
this.metadataFields = config.metadataFields;
5562
this.hint = config.hint;
@@ -61,6 +68,9 @@ export class DsDynamicInputModel extends DynamicInputModel {
6168
this.hasSelectableMetadata = config.hasSelectableMetadata;
6269
this.metadataValue = config.metadataValue;
6370
this.place = config.place;
71+
if (isNotUndefined(config.isModelOfNotRepeatableGroup)) {
72+
this.isModelOfNotRepeatableGroup = config.isModelOfNotRepeatableGroup;
73+
}
6474
this.isModelOfInnerForm = (hasValue(config.isModelOfInnerForm) ? config.isModelOfInnerForm : false);
6575
this.hideErrorMessages = config.hideErrorMessages;
6676

0 commit comments

Comments
 (0)