Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ form:
validatorMap:
required: required
regex: pattern
# If true it enables the button "Duplicate" inside inline form groups.
# The button will give the possibility to duplicate the whole form section copying the metadata values as well.
showInlineGroupDuplicateButton: false

# Notification settings
notifications:
Expand Down Expand Up @@ -202,6 +205,10 @@ submission:
# default configuration
- name: default
style: ''
# Icons that should remain visible even when no authority value is present for the metadata field.
# This is useful for fields where you want to display an icon regardless of whether the value has an authority link.
# Example: ['fas fa-user'] will show the user icon for author fields even without authority data.
iconsVisibleWithNoAuthority: ['fas fa-user']
authority:
confidence:
# NOTE: example of configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export class JsonPatchOperationsBuilder {
* the value to update the referenced path
* @param plain
* a boolean representing if the value to be added is a plain text value
* @param securityLevel
* @param language
*/
replace(path: JsonPatchOperationPathObject, value, plain = false, language = null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
[class.d-none]="model.hidden"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
@let showHint = shouldShowHint();
@let showVirtualHint = shouldShowVirtualMetadataHint();
@let showClearfix = shouldShowClearfix();
@let showErrors = shouldShowErrorMessages();

@if (!isCheckbox && hasLabel) {
<label
[id]="'label_' + model.id"
Expand All @@ -19,16 +24,15 @@
<ng-container #componentViewContainer></ng-container>
</div>

@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)) {
<small
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
@if (showHint) {
<small class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
}
<!-- In case of repeatable fields show empty space for all elements except the first -->
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {
@if (showClearfix) {
<div class="clearfix w-100 mb-2"></div>
}

@if (!model.hideErrorMessages && showErrorMessages) {
@if (showErrors) {
<div [id]="id + '_errors'"
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
@for (message of errorMessages; track message) {
Expand Down Expand Up @@ -99,11 +103,11 @@
>
</ds-existing-relation-list-element>
}
@if (hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)) {
@if (showVirtualHint) {
<small
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
}
@if (context?.parent?.groups?.length > 1 && (!showErrorMessages || errorMessages.length === 0)) {
@if (showClearfix) {
<div class="clearfix w-100 mb-2"></div>
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { SubmissionObjectService } from '../../../../submission/submission-objec
import { LiveRegionService } from '../../../live-region/live-region.service';
import { getLiveRegionServiceStub } from '../../../live-region/live-region.service.stub';
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
import { getMockFormBuilderService } from '../../testing/form-builder-service.mock';
import { FormBuilderService } from '../form-builder.service';
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
import { dsDynamicFormControlMapFn } from './ds-dynamic-form-control-map-fn';
Expand All @@ -101,6 +102,7 @@ import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.compone
import { DynamicOneboxModel } from './models/onebox/dynamic-onebox.model';
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model';
import { DsDynamicRelationInlineGroupComponent } from './models/relation-inline-group/dynamic-relation-inline-group.components';
import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component';
Expand Down Expand Up @@ -186,6 +188,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
submissionId: '1234',
id: 'relationGroup',
formConfiguration: [],
isInlineGroup: false,
mandatoryField: '',
name: 'relationGroup',
relationFields: [],
Expand All @@ -195,6 +198,20 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
metadataFields: [],
hasSelectableMetadata: false,
}),
new DynamicRelationGroupModel({
submissionId: '1234',
id: 'inlineRelationGroup',
formConfiguration: [],
isInlineGroup: true,
mandatoryField: '',
name: 'inlineRelationGroup',
relationFields: [],
scopeUUID: '',
submissionScope: '',
repeatable: false,
metadataFields: [],
hasSelectableMetadata: false,
}),
new DynamicDsDatePickerModel({ id: 'datepicker', repeatable: false }),
new DynamicLookupModel({
id: 'lookup',
Expand Down Expand Up @@ -244,7 +261,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
{ provide: Store, useValue: {} },
{ provide: RelationshipDataService, useValue: {} },
{ provide: SelectableListService, useValue: {} },
{ provide: FormBuilderService, useValue: {} },
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
{ provide: SubmissionService, useValue: {} },
{
provide: SubmissionObjectService,
Expand Down Expand Up @@ -392,10 +409,11 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
expect(testFn(formModel[19])).toEqual(DsDynamicListComponent);
expect(testFn(formModel[20])).toEqual(DsDynamicListComponent);
expect(testFn(formModel[21])).toEqual(DsDynamicRelationGroupComponent);
expect(testFn(formModel[22])).toEqual(DsDatePickerComponent);
expect(testFn(formModel[23])).toEqual(DsDynamicLookupComponent);
expect(testFn(formModel[22])).toEqual(DsDynamicRelationInlineGroupComponent);
expect(testFn(formModel[23])).toEqual(DsDatePickerComponent);
expect(testFn(formModel[24])).toEqual(DsDynamicLookupComponent);
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
expect(testFn(formModel[25])).toEqual(DsDynamicLookupComponent);
expect(testFn(formModel[26])).toEqual(DsDynamicFormGroupComponent);
});

describe('store action subscriptions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,85 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
this.subs.push(collection$.subscribe((collection) => this.collection = collection));

}

/**
* Determines whether a form field should be validated based on its parent group's state.
* @returns {boolean} True if the field should be validated, false otherwise
*/
isNotRequiredGroupAndEmpty(): boolean {
Comment thread
tdonohue marked this conversation as resolved.
const parent = this.model.parent;
// 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.
// The check hasValue(parent.parent) tells if the parent is in the submission or in a modal (nested cases)
if (hasValue(parent) && parent.type === 'GROUP' && this.model.isModelOfInnerForm && hasValue(parent.parent)) {

const groupHasSomeValue = parent.group.some(elem => !!elem.value);

if (!groupHasSomeValue && !parent.isRequired && parent.group?.length > 1) {
this.group.reset();
}

return (groupHasSomeValue && !parent.isRequired) || (hasValue(parent.isRequired) && parent.isRequired);
} else {
return true;
}
}

/**
* Determines whether the hint should be displayed for the current field.
* Hint is shown when:
* - The field has a hint
* - It's the last element in a repeatable group OR non-repeatable field OR has array group value
* - No error messages are currently displayed
* @returns {boolean} True if hint should be displayed, false otherwise
*/
shouldShowHint(): boolean {
return this.hasHint &&
(this.formBuilderService.hasArrayGroupValue(this.model) ||
((!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)) ||
(this.model.repeatable === true && this.context?.index === this.context?.context?.groups?.length - 1)) &&
(!this.showErrorMessages || this.errorMessages.length === 0);
}

/**
* Determines whether error messages should be displayed for the current field.
* Error messages are shown when:
* - Error messages are not hidden by the model configuration
* - There are error messages to show
* - For non-repeatable groups: only shown on the last element
* - The field passes the required group validation check
* @returns {boolean} True if error messages should be displayed, false otherwise
*/
shouldShowErrorMessages(): boolean {
return !this.model.hideErrorMessages &&
this.showErrorMessages &&
(!(this.model?.isModelOfNotRepeatableGroup) ||
this.model?.isModelOfNotRepeatableGroup && this.context?.index === this.context?.context?.groups?.length - 1) &&
this.isNotRequiredGroupAndEmpty();
}

/**
* Determines whether the hint should be displayed for virtual metadata fields.
* Hint is shown when:
* - The field has a hint
* - It's non-repeatable OR the last element in a repeatable group
* - No error messages are currently displayed
* @returns {boolean} True if hint should be displayed for virtual metadata, false otherwise
*/
shouldShowVirtualMetadataHint(): boolean {
return this.hasHint &&
(this.model.repeatable === false || this.context?.index === this.context?.context?.groups?.length - 1) &&
(!this.showErrorMessages || this.errorMessages.length === 0);
}

/**
* Determines whether a clearfix spacer should be displayed after the field.
* Clearfix is shown when:
* - The parent has multiple groups (more than 1)
* - No error messages are currently displayed
* @returns {boolean} True if clearfix should be displayed, false otherwise
*/
shouldShowClearfix(): boolean {
return this.context?.parent?.groups?.length > 1 &&
(!this.showErrorMessages || this.errorMessages.length === 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-l
import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.component';
import { DYNAMIC_FORM_CONTROL_TYPE_ONEBOX } from './models/onebox/dynamic-onebox.model';
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model';
import { DsDynamicRelationInlineGroupComponent } from './models/relation-inline-group/dynamic-relation-inline-group.components';
import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component';
Expand Down Expand Up @@ -93,7 +95,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
return DsDynamicTagComponent;

case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP:
return DsDynamicRelationGroupComponent;
return (model as DynamicRelationGroupModel).isInlineGroup ? DsDynamicRelationInlineGroupComponent : DsDynamicRelationGroupComponent;

case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
return DsDatePickerComponent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@
background-color: var(--bs-gray-400);
}
}


.grey-background{
background-color: #f3f3f3;
margin-bottom: 10px;
padding-top: 10px;
margin-left: -1rem;
margin-right: -1rem;
padding-right: 0px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
* If the drag feature is disabled for this DynamicRowArrayModel.
*/
get dragDisabled(): boolean {
return this.model.groups.length === 1 || !this.model.isDraggable;
return this.model.groups.length === 1 || !this.model.isDraggable || this.model.notRepeatable;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { LanguageCode } from '@dspace/core/shared/form/models/form-field-languag
import { FormFieldMetadataValueObject } from '@dspace/core/shared/form/models/form-field-metadata-value.model';
import { RelationshipOptions } from '@dspace/core/shared/relationship-options.model';
import { VocabularyOptions } from '@dspace/core/submission/vocabularies/models/vocabulary-options.model';
import { hasValue } from '@dspace/shared/utils/empty.util';
import {
hasValue,
isNotUndefined,
} from '@dspace/shared/utils/empty.util';
import {
AUTOCOMPLETE_OFF,
DynamicFormControlLayout,
DynamicFormControlRelation,
DynamicInputModel,
Expand All @@ -27,6 +31,7 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
metadataValue?: FormFieldMetadataValueObject;
isModelOfInnerForm?: boolean;
hideErrorMessages?: boolean;
isModelOfNotRepeatableGroup?: boolean;
}

export class DsDynamicInputModel extends DynamicInputModel {
Expand All @@ -46,10 +51,12 @@ export class DsDynamicInputModel extends DynamicInputModel {
@serializable() metadataValue: FormFieldMetadataValueObject;
@serializable() isModelOfInnerForm: boolean;
@serializable() hideErrorMessages?: boolean;
@serializable() isModelOfNotRepeatableGroup = false;


constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.repeatable = config.repeatable;
this.metadataFields = config.metadataFields;
this.hint = config.hint;
Expand All @@ -61,6 +68,9 @@ export class DsDynamicInputModel extends DynamicInputModel {
this.hasSelectableMetadata = config.hasSelectableMetadata;
this.metadataValue = config.metadataValue;
this.place = config.place;
if (isNotUndefined(config.isModelOfNotRepeatableGroup)) {
this.isModelOfNotRepeatableGroup = config.isModelOfNotRepeatableGroup;
}
this.isModelOfInnerForm = (hasValue(config.isModelOfInnerForm) ? config.isModelOfInnerForm : false);
this.hideErrorMessages = config.hideErrorMessages;

Expand Down
Loading
Loading