Merge pull request #2297 from 4Science/DURACOM-152

Fixed read-only visibility for submission form fields
This commit is contained in:
Tim Donohue
2023-06-16 14:08:47 -05:00
committed by GitHub
16 changed files with 107 additions and 20 deletions

View File

@@ -0,0 +1,4 @@
export enum SubmissionFieldScopeType {
WorkspaceItem = 'SUBMISSION',
WorkflowItem = 'WORKFLOW',
}

View File

@@ -1,4 +1,4 @@
export enum SubmissionScopeType { export enum SubmissionScopeType {
WorkspaceItem = 'WORKSPACE', WorkspaceItem = 'WORKSPACE',
WorkflowItem = 'WORKFLOW' WorkflowItem = 'WORKFLOW',
} }

View File

@@ -46,6 +46,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() submissionId: string; @serializable() submissionId: string;
@serializable() hasSelectableMetadata: boolean; @serializable() hasSelectableMetadata: boolean;
@serializable() metadataValue: MetadataValue; @serializable() metadataValue: MetadataValue;
@serializable() readOnly?: boolean;
isCustomGroup = true; isCustomGroup = true;
valueUpdates: Subject<string>; valueUpdates: Subject<string>;
@@ -65,6 +66,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
this.valueUpdates = new Subject<string>(); this.valueUpdates = new Subject<string>();
this.valueUpdates.subscribe((value: string) => this.value = value); this.valueUpdates.subscribe((value: string) => this.value = value);
this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
this.readOnly = config.disabled;
} }
get value() { get value() {

View File

@@ -55,6 +55,7 @@ export class DsDynamicInputModel extends DynamicInputModel {
this.metadataFields = config.metadataFields; this.metadataFields = config.metadataFields;
this.hint = config.hint; this.hint = config.hint;
this.readOnly = config.readOnly; this.readOnly = config.readOnly;
this.disabled = config.readOnly;
this.value = config.value; this.value = config.value;
this.relationship = config.relationship; this.relationship = config.relationship;
this.submissionId = config.submissionId; this.submissionId = config.submissionId;

View File

@@ -39,6 +39,7 @@
[ngbTypeahead]="search" [ngbTypeahead]="search"
[placeholder]="model.placeholder" [placeholder]="model.placeholder"
[readonly]="model.readOnly" [readonly]="model.readOnly"
[disabled]="model.readOnly"
[resultTemplate]="rt" [resultTemplate]="rt"
[type]="model.inputType" [type]="model.inputType"
[(ngModel)]="currentValue" [(ngModel)]="currentValue"
@@ -63,6 +64,7 @@
[name]="model.name" [name]="model.name"
[placeholder]="model.placeholder" [placeholder]="model.placeholder"
[readonly]="true" [readonly]="true"
[disabled]="model.readOnly"
[type]="model.inputType" [type]="model.inputType"
[value]="currentValue?.display" [value]="currentValue?.display"
(focus)="onFocus($event)" (focus)="onFocus($event)"

View File

@@ -216,6 +216,9 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple
* @param event The click event fired * @param event The click event fired
*/ */
openTree(event) { openTree(event) {
if (this.model.readOnly) {
return;
}
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
this.subs.push(this.vocabulary$.pipe( this.subs.push(this.vocabulary$.pipe(

View File

@@ -3,8 +3,10 @@
role="combobox" role="combobox"
[attr.aria-label]="model.label" [attr.aria-label]="model.label"
[attr.aria-owns]="'combobox_' + id + '_listbox'"> [attr.aria-owns]="'combobox_' + id + '_listbox'">
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle" <i *ngIf="!model.readOnly" ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
aria-hidden="true"></i> aria-hidden="true"></i>
<i *ngIf="model.readOnly" class="dropdown-toggle position-absolute toggle-icon"
aria-hidden="true"></i>
<input class="form-control" <input class="form-control"
[attr.aria-controls]="'combobox_' + id + '_listbox'" [attr.aria-controls]="'combobox_' + id + '_listbox'"
[attr.aria-activedescendant]="'combobox_' + id + '_selected'" [attr.aria-activedescendant]="'combobox_' + id + '_selected'"
@@ -15,6 +17,7 @@
[id]="id" [id]="id"
[name]="model.name" [name]="model.name"
[readonly]="true" [readonly]="true"
[disabled]="model.readOnly"
[type]="model.inputType" [type]="model.inputType"
[value]="(currentValue | async)" [value]="(currentValue | async)"
(blur)="onBlur($event)" (blur)="onBlur($event)"

View File

@@ -32,3 +32,7 @@
.scrollable-dropdown-input[readonly]{ .scrollable-dropdown-input[readonly]{
background-color: #fff; background-color: #fff;
} }
.toggle-icon {
padding: 0.7rem 0.7rem 0 0.7rem;
}

View File

@@ -1,3 +1,4 @@
import { SectionVisibility } from './../../../../submission/objects/section-visibility.model';
import { autoserialize } from 'cerialize'; import { autoserialize } from 'cerialize';
import { LanguageCode } from './form-field-language-value.model'; import { LanguageCode } from './form-field-language-value.model';
@@ -124,4 +125,7 @@ export class FormFieldModel {
*/ */
@autoserialize @autoserialize
value: any; value: any;
@autoserialize
visibility: SectionVisibility;
} }

View File

@@ -83,6 +83,8 @@ export class ConcatFieldParser extends FieldParser {
input1ModelConfig.required = true; input1ModelConfig.required = true;
} }
concatGroup.disabled = input1ModelConfig.readOnly;
if (isNotEmpty(this.firstPlaceholder)) { if (isNotEmpty(this.firstPlaceholder)) {
input1ModelConfig.placeholder = this.firstPlaceholder; input1ModelConfig.placeholder = this.firstPlaceholder;
} }

View File

@@ -1,3 +1,5 @@
import { SectionVisibility } from './../../../../submission/objects/section-visibility.model';
import { VisibilityType } from './../../../../submission/sections/visibility-type';
import { Inject, InjectionToken } from '@angular/core'; import { Inject, InjectionToken } from '@angular/core';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
@@ -22,6 +24,7 @@ import { RelationshipOptions } from '../models/relationship-options.model';
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
import { ParserType } from './parser-type'; import { ParserType } from './parser-type';
import { isNgbDateStruct } from '../../../date.util'; import { isNgbDateStruct } from '../../../date.util';
import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type';
export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId'); export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId');
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData'); export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
@@ -280,8 +283,8 @@ export abstract class FieldParser {
controlModel.id = (this.fieldId).replace(/\./g, '_'); controlModel.id = (this.fieldId).replace(/\./g, '_');
// Set read only option // Set read only option
controlModel.readOnly = this.parserOptions.readOnly; controlModel.readOnly = this.parserOptions.readOnly || this.isFieldReadOnly(this.configData.visibility, this.configData.scope, this.parserOptions.submissionScope);
controlModel.disabled = this.parserOptions.readOnly; controlModel.disabled = controlModel.readOnly;
if (hasValue(this.configData.selectableRelationship)) { if (hasValue(this.configData.selectableRelationship)) {
controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship); controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship);
} }
@@ -319,6 +322,28 @@ export abstract class FieldParser {
return controlModel; return controlModel;
} }
/**
* Checks if a field is read-only with the given scope.
* The field is readonly when submissionScope is WORKSPACE and the main visibility is READONLY
* or when submissionScope is WORKFLOW and the other visibility is READONLY
* @param visibility
* @param submissionScope
*/
private isFieldReadOnly(visibility: SectionVisibility, fieldScope: string, submissionScope: string) {
return isNotEmpty(submissionScope)
&& isNotEmpty(fieldScope)
&& isNotEmpty(visibility)
&& ((
submissionScope === SubmissionScopeType.WorkspaceItem
&& visibility.main === VisibilityType.READONLY
)
||
(visibility.other === VisibilityType.READONLY
&& submissionScope === SubmissionScopeType.WorkflowItem
)
);
}
/** /**
* Get the type bind values from the REST data for a specific field * Get the type bind values from the REST data for a specific field
* The return value is any[] in the method signature but in reality it's * The return value is any[] in the method signature but in reality it's

View File

@@ -59,19 +59,20 @@ export class OneboxFieldParser extends FieldParser {
this.setLabel(inputSelectGroup, label); this.setLabel(inputSelectGroup, label);
inputSelectGroup.required = isNotEmpty(this.configData.mandatory); inputSelectGroup.required = isNotEmpty(this.configData.mandatory);
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false);
inputModelConfig.hint = null;
this.setValues(inputModelConfig, fieldValue);
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false, false); const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false, false);
selectModelConfig.hint = null; selectModelConfig.hint = null;
this.setOptions(selectModelConfig); this.setOptions(selectModelConfig);
if (isNotEmpty(fieldValue)) { if (isNotEmpty(fieldValue)) {
selectModelConfig.value = fieldValue.metadata; selectModelConfig.value = fieldValue.metadata;
} }
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); selectModelConfig.disabled = inputModelConfig.readOnly;
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false);
inputModelConfig.hint = null;
this.setValues(inputModelConfig, fieldValue);
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput)); inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput));
return new DynamicQualdropModel(inputSelectGroup, clsGroup); return new DynamicQualdropModel(inputSelectGroup, clsGroup);

View File

@@ -1,9 +1,11 @@
import { SubmissionFieldScopeType } from './../../../../core/submission/submission-field-scope-type';
import { SectionVisibility } from './../../../../submission/objects/section-visibility.model';
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { isEmpty } from '../../../empty.util'; import { isEmpty, isNotEmpty } from '../../../empty.util';
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { FormFieldModel } from '../models/form-field.model'; import { FormFieldModel } from '../models/form-field.model';
import { CONFIG_DATA, FieldParser, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; import { CONFIG_DATA, FieldParser, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser';
@@ -12,6 +14,7 @@ import { ParserOptions } from './parser-options';
import { ParserType } from './parser-type'; import { ParserType } from './parser-type';
import { setLayout } from './parser.utils'; import { setLayout } from './parser.utils';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/ds-dynamic-form-constants'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/ds-dynamic-form-constants';
import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type';
export const ROW_ID_PREFIX = 'df-row-group-config-'; export const ROW_ID_PREFIX = 'df-row-group-config-';
@@ -118,15 +121,37 @@ export class RowParser {
return parsedResult; return parsedResult;
} }
checksFieldScope(fieldScope, submissionScope) { checksFieldScope(fieldScope, submissionScope, visibility: SectionVisibility) {
return (isEmpty(fieldScope) || isEmpty(submissionScope) || fieldScope === submissionScope); return (isEmpty(fieldScope) || !this.isHidden(visibility, fieldScope, submissionScope));
}
/**
* Check if the field is hidden or not.
* It is hidden when we do have the scope,
* but we do not have the visibility,
* also the field scope should be different from the submissionScope.
* @param visibility The visibility of the field
* @param scope the scope of the field
* @param submissionScope the scope of the submission
* @returns If the field is hidden or not
*/
private isHidden(visibility: SectionVisibility, scope: string, submissionScope: string): boolean {
return isNotEmpty(scope)
&& (
isEmpty(visibility)
&& (
submissionScope === SubmissionScopeType.WorkspaceItem && scope !== SubmissionFieldScopeType.WorkspaceItem
||
submissionScope === SubmissionScopeType.WorkflowItem && scope !== SubmissionFieldScopeType.WorkflowItem
)
);
} }
filterScopedFields(fields: FormFieldModel[], submissionScope): FormFieldModel[] { filterScopedFields(fields: FormFieldModel[], submissionScope): FormFieldModel[] {
const filteredFields: FormFieldModel[] = []; const filteredFields: FormFieldModel[] = [];
fields.forEach((field: FormFieldModel) => { fields.forEach((field: FormFieldModel) => {
// Whether field scope doesn't match the submission scope, skip it // Whether field scope doesn't match the submission scope, skip it
if (this.checksFieldScope(field.scope, submissionScope)) { if (this.checksFieldScope(field.scope, submissionScope, field.visibility)) {
filteredFields.push(field); filteredFields.push(field);
} }
}); });

View File

@@ -249,6 +249,8 @@ describe('SubmissionSectionFormComponent test suite', () => {
formConfigService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(testFormConfiguration)); formConfigService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(testFormConfiguration));
sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionData)); sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionData));
sectionsServiceStub.getSectionServerErrors.and.returnValue(observableOf([])); sectionsServiceStub.getSectionServerErrors.and.returnValue(observableOf([]));
sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false));
spyOn(comp, 'initForm'); spyOn(comp, 'initForm');
spyOn(comp, 'subscriptions'); spyOn(comp, 'subscriptions');

View File

@@ -119,6 +119,12 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
protected submissionObject: SubmissionObject; protected submissionObject: SubmissionObject;
/**
* A flag representing if this section is readonly
*/
protected isSectionReadonly = false;
/** /**
* The FormComponent reference * The FormComponent reference
*/ */
@@ -176,13 +182,15 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType), this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType),
this.submissionObjectService.findById(this.submissionId, true, false, followLink('item')).pipe( this.submissionObjectService.findById(this.submissionId, true, false, followLink('item')).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload()) getRemoteDataPayload()),
this.sectionService.isSectionReadOnly(this.submissionId, this.sectionData.id, this.submissionService.getSubmissionScope())
])), ])),
take(1)) take(1))
.subscribe(([sectionData, submissionObject]: [WorkspaceitemSectionFormObject, SubmissionObject]) => { .subscribe(([sectionData, submissionObject, isSectionReadOnly]: [WorkspaceitemSectionFormObject, SubmissionObject, boolean]) => {
if (isUndefined(this.formModel)) { if (isUndefined(this.formModel)) {
// this.sectionData.errorsToShow = []; // this.sectionData.errorsToShow = [];
this.submissionObject = submissionObject; this.submissionObject = submissionObject;
this.isSectionReadonly = isSectionReadOnly;
// Is the first loading so init form // Is the first loading so init form
this.initForm(sectionData); this.initForm(sectionData);
this.sectionData.data = sectionData; this.sectionData.data = sectionData;
@@ -295,11 +303,11 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
this.formConfig, this.formConfig,
this.collectionId, this.collectionId,
sectionData, sectionData,
this.submissionService.getSubmissionScope() this.submissionService.getSubmissionScope(),
this.isSectionReadonly
); );
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors, sectionMetadata); this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors, sectionMetadata);
} catch (e) { } catch (e) {
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
const sectionError: SubmissionSectionError = { const sectionError: SubmissionSectionError = {

View File

@@ -334,8 +334,9 @@ export class SectionsService {
filter((sectionObj) => hasValue(sectionObj)), filter((sectionObj) => hasValue(sectionObj)),
map((sectionObj: SubmissionSectionObject) => { map((sectionObj: SubmissionSectionObject) => {
return isNotEmpty(sectionObj.visibility) return isNotEmpty(sectionObj.visibility)
&& sectionObj.visibility.other === 'READONLY' && ((sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem)
&& submissionScope !== SubmissionScopeType.WorkspaceItem; || (sectionObj.visibility.main === 'READONLY' && submissionScope === SubmissionScopeType.WorkspaceItem)
);
}), }),
distinctUntilChanged()); distinctUntilChanged());
} }