From 11ae9cc0c7ee73babe1d920b0e363a2886319cc6 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 7 Jun 2024 02:49:42 +0000 Subject: [PATCH 01/30] Add web ui for internal lookup records intial commit --- assets/locales/en/translation.json | 4 +++- assets/locales/mri/translation.json | 4 +++- config/recordtype.js | 3 +++ config/workflow.js | 16 ++++++++++++++++ views/default/default/admin/home.ejs | 15 +++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 8f4dd6dbf2..9fd5a8c882 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -775,5 +775,7 @@ "lang-mri": "Māori", "system-message-enabled": "false", "system-message-title": "System Notification", - "system-message-description": "ReDBox will be unavailable from 9am to 12pm on October 20 for system upgrades. Please contact support for any help" + "system-message-description": "ReDBox will be unavailable from 9am to 12pm on October 20 for system upgrades. Please contact support for any help", + "system-lookup-records": "Lookup records", + "system-lookup-record-item1": "Parties" } \ No newline at end of file diff --git a/assets/locales/mri/translation.json b/assets/locales/mri/translation.json index 21bbf0cd74..0b6d051c83 100644 --- a/assets/locales/mri/translation.json +++ b/assets/locales/mri/translation.json @@ -761,5 +761,7 @@ "oidc-user-doesnt-exist-in-tenant": "User account from identity provider does not exist in tenant. The account needs to be added as an external user in the tenant first.", "oidc-user-doesnt-exist-in-tenant-detail": "For email {{-email}} and url {{-url}} and tenant {{-name}}", "lang-en": "English", - "lang-mri": "Māori" + "lang-mri": "Māori", + "system-lookup-records": "Lookup records", + "system-lookup-record-item1": "Parties" } diff --git a/config/recordtype.js b/config/recordtype.js index 135d89a9ff..76bee7a1e7 100644 --- a/config/recordtype.js +++ b/config/recordtype.js @@ -604,5 +604,8 @@ module.exports.recordtype = { "packageName": "consolidated", "searchFilters": [], hooks: { } + }, + "party": { + "packageType": "rdmp" } }; diff --git a/config/workflow.js b/config/workflow.js index 64cb2fb21c..f658e76b95 100644 --- a/config/workflow.js +++ b/config/workflow.js @@ -282,5 +282,21 @@ module.exports.workflow = { starting: false, consolidated: true } + }, + "party": { + "draft": { + config: { + workflow: { + stage: 'draft', + stageLabel: 'Draft', + }, + authorization: { + viewRoles: ['Admin', 'Librarians'], + editRoles: ['Admin', 'Librarians'] + }, + form: 'default-1.0-draft' + }, + starting: true + } } }; diff --git a/views/default/default/admin/home.ejs b/views/default/default/admin/home.ejs index 2d3248534c..047ddd9103 100644 --- a/views/default/default/admin/home.ejs +++ b/views/default/default/admin/home.ejs @@ -32,6 +32,21 @@ <% }%> + <% if ( hasRole(req,'Admin')) {%> +
+
+
+ <%= TranslationService.t('system-lookup-records')%> +
+ + + +
+ +
+ <% }%>
From 0b4e11d50cd2add14d245ebded4ea32bc7b5faa3 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 11 Jun 2024 07:34:11 +0000 Subject: [PATCH 02/30] Dashboard component intial refactor --- .../dashboard/src/app/dashboard.component.ts | 294 ++++++++++-------- .../src/lib/dashboard-models.ts | 36 +++ config/dashboardtype.js | 15 +- 3 files changed, 212 insertions(+), 133 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 6aeb1ba5b1..25460447f9 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, ElementRef } from '@angular/core'; import { PageChangedEvent } from 'ngx-bootstrap/pagination'; -import { BaseComponent, UtilityService, LoggerService, TranslationService, RecordService, PlanTable, UserService, ConfigService } from '@researchdatabox/portal-ng-common'; +import { BaseComponent, UtilityService, LoggerService, TranslationService, RecordService, PlanTable, UserService, ConfigService, FormatRules, SortGroupBy, QueryFilter } from '@researchdatabox/portal-ng-common'; import { get as _get, set as _set, isEmpty as _isEmpty, isUndefined as _isUndefined, trim as _trim, isNull as _isNull, orderBy as _orderBy, map as _map, find as _find, indexOf as _indexOf, isArray as _isArray, forEach as _forEach, join as _join, first as _first } from 'lodash-es'; import { LoDashTemplateUtilityService } from 'projects/researchdatabox/portal-ng-common/src/lib/lodash-template-utility.service'; @@ -28,6 +28,8 @@ export class DashboardComponent extends BaseComponent { rulesService: object; currentUser: object = {}; enableSort: boolean = true; + filterField: string = ''; + hideWorkflowStepTitle: boolean = false; defaultTableConfig = [ { @@ -90,14 +92,17 @@ export class DashboardComponent extends BaseComponent { sortFields = ['metaMetadata.lastSaveDate', 'metaMetadata.createdOn', 'metadata.title', 'metadata.contributor_ci.text_full_name', 'metadata.contributor_data_manager.text_full_name']; - defaultFormatRules: any = { - filterBy: [], //filterBase can only have two values user or record + defaultFormatRules: FormatRules = { + filterBy: {}, //filterBase can only have two values user or record filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] + recordTypeFilterBy: '', + queryFilters: [{ filterType: 'text', filterFields: ['metadata.title'] }], sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required? + hideWorkflowStepTitleForRecordType: [] }; - formatRules: any = {}; + formatRules: FormatRules = this.defaultFormatRules; defaultGroupRowConfig = {}; groupRowConfig = {}; @@ -157,14 +162,13 @@ export class DashboardComponent extends BaseComponent { public async initView(recordType: string) { - //console.log('----------------------- initView -------------------------- '+this.dashboardTypeSelected); this.formatRules = this.defaultFormatRules; this.rowLevelRules = this.defaultRowLevelRules; this.groupRowConfig = this.defaultGroupRowConfig; this.groupRowRules = this.defaultGroupRowRules; let dashboardType: any = await this.recordService.getDashboardType(this.dashboardTypeSelected); - let formatRules = _get(dashboardType, 'formatRules'); + let formatRules: FormatRules = _get(dashboardType, 'formatRules'); let startIndex = 1; if (!_isUndefined(formatRules) && !_isNull(formatRules) && !_isEmpty(formatRules)) { //global format rules from dashboardtype.js config @@ -176,6 +180,12 @@ export class DashboardComponent extends BaseComponent { recordType = recordTypeFilterBy; } + for(let recType of formatRules.hideWorkflowStepTitleForRecordType) { + if(recType == recordType) { + this.hideWorkflowStepTitle = true; + } + } + let beforeFilterSteps: any = await this.recordService.getWorkflowSteps(recordType); let filterWorkflowStepsBy = _get(this.formatRules, 'filterWorkflowStepsBy'); @@ -199,58 +209,10 @@ export class DashboardComponent extends BaseComponent { for (let step of steps) { this.workflowSteps.push(step); - // console.log('----------------------- step -------------------------- '+step.config.workflow.stageLabel); - let stepTableConfig = this.defaultTableConfig; - if (_isEmpty(this.defaultTableConfig[0].title)) { - this.defaultTableConfig[0].title = `${recordType}-title` || 'Title'; - // console.log('----------------------- title -------------------------- '+this.defaultTableConfig[0].title); - } - if (!_isUndefined(_get(step, 'config.dashboard')) - && !_isUndefined(_get(step, 'config.dashboard.table'))) { - - if (!_isUndefined(_get(step, 'config.dashboard.table.rowConfig'))) { - stepTableConfig = _get(step, 'config.dashboard.table.rowConfig'); - this.sortFields = _map(_get(step, 'config.dashboard.table.rowConfig'), (config) => { return config.variable }); - } - - if (!_isUndefined(_get(step, 'config.dashboard.table.rowRulesConfig'))) { - this.rowLevelRules = _get(step, 'config.dashboard.table.rowRulesConfig'); - console.log(JSON.stringify(this.rowLevelRules)); - } - - if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowConfig'))) { - this.groupRowConfig = _get(step, 'config.dashboard.table.groupRowConfig'); - } - - if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowRulesConfig'))) { - this.groupRowRules = _get(step, 'config.dashboard.table.groupRowRulesConfig'); - } - - //formtatRules override at step level from workflow.js config - if (!_isUndefined(_get(step, 'config.dashboard.table.formatRules'))) { - this.formatRules = _get(step, 'config.dashboard.table.formatRules'); - } - } - this.tableConfig[step.name] = stepTableConfig; - this.sortMap[step.name] = {}; - for (let rowConfig of stepTableConfig) { - this.sortMap[step.name][rowConfig.variable] = { - sort: rowConfig.initialSort - }; - } - - if(this.dashboardTypeSelected == 'consolidated') { - this.enableSort = false; - } else { - this.enableSort = true; - } + let stepTableConfig = this.initStepTableConfig(recordType, step); - if(this.dashboardTypeSelected == 'consolidated') { - this.enableSort = false; - } else { - this.enableSort = true; - } + this.initSortConfig(step, stepTableConfig); let packageType = ''; let stepName = ''; @@ -272,10 +234,56 @@ export class DashboardComponent extends BaseComponent { } await this.initStep(stepName, evaluateStepName, recordType, packageType, startIndex); + } + } + + private initStepTableConfig(recordType: string, step: any) { + let stepTableConfig = this.defaultTableConfig; + if (_isEmpty(this.defaultTableConfig[0].title)) { + this.defaultTableConfig[0].title = `${recordType}-title` || 'Title'; + } + if (!_isUndefined(_get(step, 'config.dashboard')) + && !_isUndefined(_get(step, 'config.dashboard.table'))) { + + if (!_isUndefined(_get(step, 'config.dashboard.table.rowConfig'))) { + stepTableConfig = _get(step, 'config.dashboard.table.rowConfig'); + this.sortFields = _map(_get(step, 'config.dashboard.table.rowConfig'), (config) => { return config.variable; }); + } - // console.log('-------------------------------------------------'); - // console.log(JSON.stringify(this.records)); - // console.log('-------------------------------------------------'); + if (!_isUndefined(_get(step, 'config.dashboard.table.rowRulesConfig'))) { + this.rowLevelRules = _get(step, 'config.dashboard.table.rowRulesConfig'); + } + + if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowConfig'))) { + this.groupRowConfig = _get(step, 'config.dashboard.table.groupRowConfig'); + } + + if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowRulesConfig'))) { + this.groupRowRules = _get(step, 'config.dashboard.table.groupRowRulesConfig'); + } + + //formtatRules override at step level from workflow.js config + if (!_isUndefined(_get(step, 'config.dashboard.table.formatRules'))) { + this.formatRules = _get(step, 'config.dashboard.table.formatRules'); + } + } + return stepTableConfig; + } + + private initSortConfig(step: any, stepTableConfig: any[]) { + + this.tableConfig[step.name] = stepTableConfig; + this.sortMap[step.name] = {}; + for (let rowConfig of stepTableConfig) { + this.sortMap[step.name][rowConfig.variable] = { + sort: rowConfig.initialSort + }; + } + + if (this.dashboardTypeSelected == 'consolidated') { + this.enableSort = false; + } else { + this.enableSort = true; } } @@ -303,7 +311,6 @@ export class DashboardComponent extends BaseComponent { sortByString = sortBy; } - //TODO getRecords defaults to 10 perhaps add another param to set? let stagedRecords = await this.recordService.getRecords(recordType, stepName, startIndex, packageType, sortByString, filterFileds, filterString, filterMode); let planTable: PlanTable; @@ -318,68 +325,12 @@ export class DashboardComponent extends BaseComponent { let sortGroupBy = _get(this.formatRules, 'sortGroupBy'); if (groupBy == 'groupedByRelationships' && !_isUndefined(sortGroupBy) && !_isEmpty(sortGroupBy)) { - for (let item of items) { - let oid = _get(item, 'oid'); - let itemsAfterApplyInnerGroupFormatRules = []; - - let itemsGroupRelated: any = await this.recordService.getRelatedRecords(oid); - let sortItems = _get(itemsGroupRelated, 'items'); - let totalSortItems = sortItems.length; - let countHerarchyLevels = sortGroupBy.length; - - for (let j = 0; j < totalSortItems; j++) { - let parentTreeNodeOid = oid; - for (let i = 0; i < countHerarchyLevels; i++) { - let rule = _find(sortGroupBy, function (o) { - if (_get(o, 'rowLevel') == i) { - return o; - } - }); - let compareField = _get(rule, 'compareField'); - let compareFieldValue = _get(rule, 'compareFieldValue'); - let relatedTo = _get(rule, 'relatedTo'); - - for (let sortItem of sortItems) { - let relatedToOid = _get(sortItem, relatedTo); - let foundParent = relatedToOid == parentTreeNodeOid; - let foundRecord = _get(sortItem, compareField) == compareFieldValue; - let foundTopLevelParent = relatedTo == ''; - if (foundRecord && (foundParent || foundTopLevelParent)) { - let currentOid = _get(sortItem, 'oid'); - let rowExists = _find(itemsAfterApplyInnerGroupFormatRules, ['oid', currentOid]); - if (_isUndefined(rowExists)) { - itemsAfterApplyInnerGroupFormatRules.push(sortItem); - if ((i + 1) < countHerarchyLevels) { - parentTreeNodeOid = currentOid; - break; - } - } - } - } - } - } - if (!_isEmpty(itemsAfterApplyInnerGroupFormatRules)) { - _set(itemsGroupRelated, 'items', itemsAfterApplyInnerGroupFormatRules); - } + allItemsByGroup = await this.getAllItemsGroupedByRelationships(items, sortGroupBy); - allItemsByGroup.push(itemsGroupRelated); - } } else if (groupBy == 'groupedByRecordType' && !_isUndefined(sortGroupBy) && !_isEmpty(sortGroupBy)) { - let countHerarchyLevels = sortGroupBy.length; - for (let i = 0; i < countHerarchyLevels; i++) { - - let rule = _find(sortGroupBy, function (o) { - if (_get(o, 'rowLevel') == i) { - return o; - } - }); - let compareFieldValue = _get(rule, 'compareFieldValue'); - let itemsGroupRelated: any = await this.recordService.getRecords(compareFieldValue, stepName, startIndex, packageType, sortByString, filterFileds, filterString, filterMode); - - allItemsByGroup.push(itemsGroupRelated); - } + allItemsByGroup = await this.getAllItemsGroupedByRecordType(sortGroupBy, stepName, startIndex, packageType, sortByString, filterFileds, filterString, filterMode); } let pageNumber = _get(stagedRecords, 'currentPage'); @@ -401,6 +352,79 @@ export class DashboardComponent extends BaseComponent { this.records[evaluateStepName] = planTable; } + private async getAllItemsGroupedByRecordType(sortGroupBy: SortGroupBy[], stepName: string, startIndex: number, packageType: string, sortByString: string, filterFileds: any, filterString: any, filterMode: any) { + let allItemsByGroup: any[] = []; + let countHerarchyLevels = sortGroupBy.length; + for (let i = 0; i < countHerarchyLevels; i++) { + + let rule = _find(sortGroupBy, function (o) { + if (_get(o, 'rowLevel') == i) { + return true; + } + return false; + }); + let compareFieldValue = _get(rule, 'compareFieldValue', ''); + let itemsGroupRelated: any = await this.recordService.getRecords(compareFieldValue, stepName, startIndex, packageType, sortByString, filterFileds, filterString, filterMode); + + allItemsByGroup.push(itemsGroupRelated); + } + + return allItemsByGroup; + } + + private async getAllItemsGroupedByRelationships(items: any, sortGroupBy: SortGroupBy[]) { + let allItemsByGroup: any[] = []; + for (let item of items) { + let oid = _get(item, 'oid'); + let itemsAfterApplyInnerGroupFormatRules = []; + + let itemsGroupRelated: any = await this.recordService.getRelatedRecords(oid); + let sortItems = _get(itemsGroupRelated, 'items'); + let totalSortItems = sortItems.length; + let countHerarchyLevels = sortGroupBy.length; + + for (let j = 0; j < totalSortItems; j++) { + let parentTreeNodeOid = oid; + for (let i = 0; i < countHerarchyLevels; i++) { + let rule = _find(sortGroupBy, function (o) { + if (_get(o, 'rowLevel') == i) { + return true; + } + return false; + }); + let compareField = _get(rule, 'compareField', ''); + let compareFieldValue = _get(rule, 'compareFieldValue', ''); + let relatedTo = _get(rule, 'relatedTo', ''); + + for (let sortItem of sortItems) { + let relatedToOid = _get(sortItem, relatedTo); + let foundParent = relatedToOid == parentTreeNodeOid; + let foundRecord = _get(sortItem, compareField) == compareFieldValue; + let foundTopLevelParent = relatedTo == ''; + if (foundRecord && (foundParent || foundTopLevelParent)) { + let currentOid = _get(sortItem, 'oid'); + let rowExists = _find(itemsAfterApplyInnerGroupFormatRules, ['oid', currentOid]); + if (_isUndefined(rowExists)) { + itemsAfterApplyInnerGroupFormatRules.push(sortItem); + if ((i + 1) < countHerarchyLevels) { + parentTreeNodeOid = currentOid; + break; + } + } + } + } + } + } + + if (!_isEmpty(itemsAfterApplyInnerGroupFormatRules)) { + _set(itemsGroupRelated, 'items', itemsAfterApplyInnerGroupFormatRules); + } + + allItemsByGroup.push(itemsGroupRelated); + } + return allItemsByGroup; + } + public evaluatePlanTableColumns(groupRowConfig: any, groupRowRules: any, rowLevelRulesConfig: any, stepName: string, stagedOrGroupedRecords: any): PlanTable { let recordRows: any = []; @@ -442,7 +466,6 @@ export class DashboardComponent extends BaseComponent { let record: any = {}; - let stepTableConfig = _isEmpty(this.tableConfig[stepName]) ? this.defaultTableConfig : this.tableConfig[stepName]; for (let rowConfig of stepTableConfig) { @@ -485,8 +508,6 @@ export class DashboardComponent extends BaseComponent { _set(imports, 'portal', this.portal); _set(imports, 'translationService', this.translationService); - - let record: any = {}; let stepTableCOnfig = _isEmpty(this.tableConfig[stepName]) ? this.defaultTableConfig : this.tableConfig[stepName]; @@ -505,9 +526,6 @@ export class DashboardComponent extends BaseComponent { return planTable; } - - - public evaluateRowLevelRules(rulesConfig: any, metadata: any, metaMetadata: any, workflow: any, oid: string, ruleSetName: string) { let res: any; @@ -521,7 +539,6 @@ export class DashboardComponent extends BaseComponent { _set(imports, 'portal', this.portal); _set(imports, 'translationService', this.translationService); - let ruleSetConfig = _find(rulesConfig, ['ruleSetName', ruleSetName]); if (!_isUndefined(ruleSetConfig)) { @@ -538,7 +555,6 @@ export class DashboardComponent extends BaseComponent { let evaluateRulesTemplate = _get(rule, 'evaluateRulesTemplate'); _set(imports, 'name', name); - let evaluatedAction = ''; let action = _get(rule, 'action'); @@ -604,9 +620,7 @@ export class DashboardComponent extends BaseComponent { _set(imports, 'oid', oid); _set(imports, 'name', name); - - - const result = this.runTemplate(evaluateRulesTemplate, imports) + const result = this.runTemplate(evaluateRulesTemplate, imports); if (result == 'true') { results.push(result); } else if (mode == 'all') { @@ -719,6 +733,28 @@ export class DashboardComponent extends BaseComponent { return 'metaMetadata.lastSaveDate:-1'; } + public getFilters() { + let filterFields: string[] = []; + let queryFilters: QueryFilter[] = this.formatRules.queryFilters; + for(let queryFilter of queryFilters) { + for(let filterField of queryFilter.filterFields) { + filterFields.push(filterField); + } + } + return filterFields; + } + + public filterChanged() { + + } + + public resetSearch() { + + } + + public setFilterField(filterField:string, e: any) { + this.filterField = filterField; + } } diff --git a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts index 641432ca27..e58b155dfc 100644 --- a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts +++ b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts @@ -21,3 +21,39 @@ export class Plan { metadata: object = {}; dashboardTitle: string = ''; } + +// export declare class FilterBy { +// filterBase: string; +// filterBaseFieldOrValue: string; +// filterField: string; +// filterMode: string; +// } + +export declare class QueryFilter { + filterType: string; + filterFields: string[]; +} + +export declare class SortGroupBy { + rowLevel: number; + compareFieldValue: string; + compareField: string; + relatedTo: string; +} + +export declare class FormatRules { + filterBy: any; + filterWorkflowStepsBy: string[]; + recordTypeFilterBy: string; + queryFilters: QueryFilter[]; + sortBy: string; + groupBy: string; + sortGroupBy: SortGroupBy[]; + hideWorkflowStepTitleForRecordType: string[]; +} + +export declare class DashboardConfig { + [key: string]: { + formatRules: FormatRules; + } +} \ No newline at end of file diff --git a/config/dashboardtype.js b/config/dashboardtype.js index a7cf6d0c63..a7e7f8a397 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -1,16 +1,23 @@ module.exports.dashboardtype = { "standard": { formatRules: { - filterBy: [], //filterBase can only have two values user or record - filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] + filterBy: {}, //filterBase can only have two values user or record + filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] + queryFilters: [ + { + filterType: 'text', + filterFields: ['metadata.title', 'metadata.description'] + } + ], sortBy: 'metaMetadata.lastSaveDate:-1', - groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships + groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required? + hideWorkflowStepTitleForRecordType: ['party'] } }, "workspace": { formatRules: { - filterBy: [], //filterBase can only have two values user or record + filterBy: {}, //filterBase can only have two values user or record recordTypeFilterBy: 'existing-locations', filterWorkflowStepsBy: ['existing-locations-draft'], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised'] sortBy: 'metaMetadata.lastSaveDate:-1', From 1a3a75fe5fca1a34a823660eb628fc23a1069e8d Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 11 Jun 2024 07:35:33 +0000 Subject: [PATCH 03/30] Dashboard component template inital changes --- .../src/app/dashboard.component.html | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 23e9beed87..e0462bc83a 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -1,9 +1,29 @@
-

+

+ {{ 'dashboard-heading' | i18next: {stage: workflowStep.config.workflow.stageLabel, recordTypeName: typeLabel} }}

-

+

+ {{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }} +

+

+
+
{{ 'record-search-basic-search' | i18next }}
+ +
+ + + +
+ +

From cfb875dce16232f49153ca022811ac93ef954596 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Thu, 13 Jun 2024 02:46:17 +0000 Subject: [PATCH 04/30] Add filter method initial implementation to dashboard component --- .../src/app/dashboard.component.html | 39 ++++++++++-------- .../dashboard/src/app/dashboard.component.ts | 41 ++++++++++++++----- .../src/lib/dashboard-models.ts | 7 +++- config/dashboardtype.js | 8 +++- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index e0462bc83a..fdee756651 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -8,25 +8,32 @@

-
-
{{ 'record-search-basic-search' | i18next }}
- -
- - - -
- +
+
+
+
+
{{ 'record-search-basic-search' | i18next }}
+ +
+ + + +
+

+ +
+

-
+
+ {{ 'no-records' | i18next: {stage: workflowStep.config.workflow.stageLabel.toLowerCase(), recordTypeName: typeLabel.toLowerCase()} }}
diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 25460447f9..488c793f8d 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, ElementRef } from '@angular/core'; import { PageChangedEvent } from 'ngx-bootstrap/pagination'; -import { BaseComponent, UtilityService, LoggerService, TranslationService, RecordService, PlanTable, UserService, ConfigService, FormatRules, SortGroupBy, QueryFilter } from '@researchdatabox/portal-ng-common'; +import { BaseComponent, UtilityService, LoggerService, TranslationService, RecordService, PlanTable, UserService, ConfigService, FormatRules, SortGroupBy, QueryFilter, FilterField } from '@researchdatabox/portal-ng-common'; import { get as _get, set as _set, isEmpty as _isEmpty, isUndefined as _isUndefined, trim as _trim, isNull as _isNull, orderBy as _orderBy, map as _map, find as _find, indexOf as _indexOf, isArray as _isArray, forEach as _forEach, join as _join, first as _first } from 'lodash-es'; import { LoDashTemplateUtilityService } from 'projects/researchdatabox/portal-ng-common/src/lib/lodash-template-utility.service'; @@ -28,7 +28,9 @@ export class DashboardComponent extends BaseComponent { rulesService: object; currentUser: object = {}; enableSort: boolean = true; - filterField: string = ''; + filterField: string = 'Title'; + filterFieldPath: string = 'metadata.title'; + filterSearchString: string = ''; hideWorkflowStepTitle: boolean = false; defaultTableConfig = [ @@ -96,7 +98,7 @@ export class DashboardComponent extends BaseComponent { filterBy: {}, //filterBase can only have two values user or record filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] recordTypeFilterBy: '', - queryFilters: [{ filterType: 'text', filterFields: ['metadata.title'] }], + queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required? @@ -664,7 +666,7 @@ export class DashboardComponent extends BaseComponent { if (this.dashboardTypeSelected == 'workspace') { stagedRecords = await this.recordService.getRecords('', '', 1, this.dashboardTypeSelected, sortString); } else { - stagedRecords = await this.recordService.getRecords(this.recordType, data.step, 1, '', sortString); + stagedRecords = await this.recordService.getRecords(this.recordType, data.step, 1, '', sortString,this.filterFieldPath,this.filterSearchString); } let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, data.step, stagedRecords); @@ -689,7 +691,7 @@ export class DashboardComponent extends BaseComponent { let sortDetails = this.sortMap[step]; if (this.dashboardTypeSelected == 'standard') { - let stagedRecords = await this.recordService.getRecords(this.recordType, step, event.page, '', this.getSortString(sortDetails)); + let stagedRecords = await this.recordService.getRecords(this.recordType, step, event.page, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; } else if (this.dashboardTypeSelected == 'workspace') { @@ -734,7 +736,7 @@ export class DashboardComponent extends BaseComponent { } public getFilters() { - let filterFields: string[] = []; + let filterFields: FilterField[] = []; let queryFilters: QueryFilter[] = this.formatRules.queryFilters; for(let queryFilter of queryFilters) { for(let filterField of queryFilter.filterFields) { @@ -744,16 +746,35 @@ export class DashboardComponent extends BaseComponent { return filterFields; } - public filterChanged() { + public async filterChanged(step: string) { + if (this.dashboardTypeSelected == 'standard') { + let sortDetails = this.sortMap[step]; + let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); + let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); + this.records[step] = planTable; + } } - public resetSearch() { + public async resetFilterAndSearch(step: string) { + if (this.dashboardTypeSelected == 'standard') { + let sortDetails = this.sortMap[step]; + this.filterField = 'Title'; + this.filterFieldPath = 'metadata.title'; + this.filterSearchString = ''; + let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); + let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); + this.records[step] = planTable; + } } - public setFilterField(filterField:string, e: any) { - this.filterField = filterField; + public setFilterField(filterField:FilterField, e: any) { + if (e) { + e.preventDefault(); + } + this.filterField = filterField.name; + this.filterFieldPath = filterField.path; } } diff --git a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts index e58b155dfc..6657c3bee8 100644 --- a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts +++ b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts @@ -29,9 +29,14 @@ export class Plan { // filterMode: string; // } +export declare class FilterField { + name: string; + path: string; +} + export declare class QueryFilter { filterType: string; - filterFields: string[]; + filterFields: FilterField[]; } export declare class SortGroupBy { diff --git a/config/dashboardtype.js b/config/dashboardtype.js index a7e7f8a397..b9d02ce980 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -6,7 +6,13 @@ module.exports.dashboardtype = { queryFilters: [ { filterType: 'text', - filterFields: ['metadata.title', 'metadata.description'] + filterFields: [ + { name: 'Title', + path: 'metadata.title' }, + { name: 'Description', + path: 'metadata.description' + } + ] } ], sortBy: 'metaMetadata.lastSaveDate:-1', From 1e91fee81cf2176bbddf1a248f80073849f79fcb Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Thu, 13 Jun 2024 05:19:08 +0000 Subject: [PATCH 05/30] Update angular tests for dashboard component --- .../src/app/dashboard.component.spec.ts | 40 +++++++++++-------- config/dashboardtype.js | 12 +++--- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts index e8d666c5d3..a0464fb781 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts @@ -13,11 +13,13 @@ let recordDataStandard = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: [], + filterBy: [], + filterWorkflowStepsBy: [], + queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], sortBy: 'metaMetadata.lastSaveDate:-1', - groupBy: '', - sortGroupBy: [], + groupBy: '', + sortGroupBy: [], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ @@ -152,12 +154,14 @@ let recordDataWorkspace = { dashboardType: { formatRules: { - filterBy: [], + filterBy: [], recordTypeFilterBy: 'existing-locations', - filterWorkflowStepsBy: [ 'existing-locations-draft'], + filterWorkflowStepsBy: [ 'existing-locations-draft'], + queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], sortBy: 'metaMetadata.lastSaveDate:-1', - groupBy: '', - sortGroupBy: [], + groupBy: '', + sortGroupBy: [], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ @@ -295,11 +299,13 @@ let recordDataConsolidated = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: ['consolidated'], + filterBy: [], + filterWorkflowStepsBy: ['consolidated'], + queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], sortBy: '', - groupBy: 'groupedByRecordType', - sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], + groupBy: 'groupedByRecordType', + sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ @@ -505,11 +511,13 @@ let recordDataConsolidatedRelationships = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: ['consolidated'], + filterBy: [], + filterWorkflowStepsBy: ['consolidated'], + queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], sortBy: '', - groupBy: 'groupedByRelationships', - sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], + groupBy: 'groupedByRelationships', + sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ diff --git a/config/dashboardtype.js b/config/dashboardtype.js index b9d02ce980..f3544df291 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -9,15 +9,15 @@ module.exports.dashboardtype = { filterFields: [ { name: 'Title', path: 'metadata.title' }, - { name: 'Description', - path: 'metadata.description' + { name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' } ] } ], sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships - sortGroupBy: [], //values: as many levels as required? + sortGroupBy: [], //values: as many levels as required hideWorkflowStepTitleForRecordType: ['party'] } }, @@ -28,7 +28,8 @@ module.exports.dashboardtype = { filterWorkflowStepsBy: ['existing-locations-draft'], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised'] sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships - sortGroupBy: [] //values: as many levels as required? + sortGroupBy: [], //values: as many levels as required + hideWorkflowStepTitleForRecordType: ['party'] } }, "consolidated": { @@ -39,7 +40,8 @@ module.exports.dashboardtype = { groupBy: 'groupedByRelationships', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp', compareField: 'metadata.metaMetadata.type', relatedTo: '' }, { rowLevel: 1, compareFieldValue: 'dataRecord', compareField: 'metadata.metaMetadata.type', relatedTo: 'metadata.metadata.rdmp.oid' }, - { rowLevel: 2, compareFieldValue: 'dataPublication', compareField: 'metadata.metaMetadata.type', relatedTo: 'metadata.metadata.dataRecord.oid' }] //values: as many levels as required + { rowLevel: 2, compareFieldValue: 'dataPublication', compareField: 'metadata.metaMetadata.type', relatedTo: 'metadata.metadata.dataRecord.oid' }], //values: as many levels as required + hideWorkflowStepTitleForRecordType: ['party'] } } }; From 86e44f267d778615fba14b3b00575458122b87cc Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Thu, 13 Jun 2024 07:13:25 +0000 Subject: [PATCH 06/30] Add intial implementation of date range filters to dashboard component --- .../src/app/dashboard.component.html | 48 ++++++++++++++-- .../src/app/dashboard.component.spec.ts | 56 ++++++++++++++++-- .../dashboard/src/app/dashboard.component.ts | 42 ++++++++++++-- .../dashboard/src/app/dashboard.module.ts | 9 ++- .../src/lib/dashboard-models.ts | 2 +- config/dashboardtype.js | 57 +++++++++++++++---- 6 files changed, 181 insertions(+), 33 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index fdee756651..155a8abe87 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -7,8 +7,42 @@

{{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }}

-

-
+ +
+
+
+
+
+ +
+
+ +
+ + + + +
+
+
+ +
+ + +
+
+
+
+ +
@@ -29,8 +63,10 @@

{{ 'record-search-reset' | i18next }}

-
-

+ + + +
{{ 'no-records' | i18next: {stage: workflowStep.config.workflow.stageLabel.toLowerCase(), recordTypeName: typeLabel.toLowerCase()} }} diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts index a0464fb781..51b39c6f89 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts @@ -15,7 +15,19 @@ let recordDataStandard = { formatRules: { filterBy: [], filterWorkflowStepsBy: [], - queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', sortGroupBy: [], @@ -157,7 +169,19 @@ let recordDataWorkspace = { filterBy: [], recordTypeFilterBy: 'existing-locations', filterWorkflowStepsBy: [ 'existing-locations-draft'], - queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', sortGroupBy: [], @@ -301,7 +325,19 @@ let recordDataConsolidated = { formatRules: { filterBy: [], filterWorkflowStepsBy: ['consolidated'], - queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: '', groupBy: 'groupedByRecordType', sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], @@ -513,7 +549,19 @@ let recordDataConsolidatedRelationships = { formatRules: { filterBy: [], filterWorkflowStepsBy: ['consolidated'], - queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: '', groupBy: 'groupedByRelationships', sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp' }], diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 488c793f8d..6bce46bdf6 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -32,6 +32,10 @@ export class DashboardComponent extends BaseComponent { filterFieldPath: string = 'metadata.title'; filterSearchString: string = ''; hideWorkflowStepTitle: boolean = false; + dateRangeFilterParams: any = {}; + datePickerOpts = { dateInputFormat: 'DD/MM/YYYY', containerClass: 'theme-dark-blue' }; + datePickerPlaceHolder: string = ''; + appName:string = 'dashboard'; defaultTableConfig = [ { @@ -98,7 +102,19 @@ export class DashboardComponent extends BaseComponent { filterBy: {}, //filterBase can only have two values user or record filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] recordTypeFilterBy: '', - queryFilters: [{ filterType: 'text', filterFields: [ { name: 'Title', path: 'metadata.title' } ] } ], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required? @@ -147,6 +163,11 @@ export class DashboardComponent extends BaseComponent { protected override async initComponent(): Promise { if (_indexOf(this.dashboardTypeOptions, this.dashboardTypeSelected) >= 0) { + const sysConfig = await this.configService.getConfig(); + const defaultDatePickerOpts = { dateInputFormat: 'DD/MM/YYYY', containerClass: 'theme-dark-blue' }; + const defaultDatePickerPlaceHolder = 'dd/mm/yyyy'; + this.datePickerOpts = ConfigService._getAppConfigProperty(sysConfig, this.appName, 'datePickerOpts', defaultDatePickerOpts); + this.datePickerPlaceHolder = ConfigService._getAppConfigProperty(sysConfig, this.appName, 'datePickerPlaceHolder', defaultDatePickerPlaceHolder); this.loggerService.debug(`Dashboard waiting for deps to init...`); this.loggerService.debug(`Dashboard initialised.`); this.config = this.recordService.getConfig(); @@ -735,17 +756,28 @@ export class DashboardComponent extends BaseComponent { return 'metaMetadata.lastSaveDate:-1'; } - public getFilters() { + private getFilters(type:string) { let filterFields: FilterField[] = []; - let queryFilters: QueryFilter[] = this.formatRules.queryFilters; + let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; for(let queryFilter of queryFilters) { - for(let filterField of queryFilter.filterFields) { - filterFields.push(filterField); + if(queryFilter.filterType == type) { + for(let filterField of queryFilter.filterFields) { + filterFields.push(filterField); + } } } return filterFields; } + + public getTextFilters() { + return this.getFilters('text'); + } + + public getDateRangeFilters() { + return this.getFilters('date-range'); + } + public async filterChanged(step: string) { if (this.dashboardTypeSelected == 'standard') { diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts index dc3be92f0c..a2097d8589 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts @@ -1,13 +1,12 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { ReactiveFormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { RedboxPortalCoreModule, trimLastSlashFromUrl } from '@researchdatabox/portal-ng-common'; import { APP_BASE_HREF, PlatformLocation } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { PaginationModule } from 'ngx-bootstrap/pagination'; - import { DashboardComponent } from './dashboard.component'; import { SortComponent } from './sort/sort.component'; @@ -18,10 +17,10 @@ import { SortComponent } from './sort/sort.component'; ], imports: [ BrowserModule, - ReactiveFormsModule, FormsModule, - RouterModule, + BrowserAnimationsModule, RedboxPortalCoreModule, + BsDatepickerModule.forRoot(), PaginationModule.forRoot(), TooltipModule.forRoot() ], diff --git a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts index 6657c3bee8..232a931417 100644 --- a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts +++ b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts @@ -50,7 +50,7 @@ export declare class FormatRules { filterBy: any; filterWorkflowStepsBy: string[]; recordTypeFilterBy: string; - queryFilters: QueryFilter[]; + queryFilters: { [key: string]: QueryFilter[] }; sortBy: string; groupBy: string; sortGroupBy: SortGroupBy[]; diff --git a/config/dashboardtype.js b/config/dashboardtype.js index f3544df291..c1c8d86c90 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -3,18 +3,51 @@ module.exports.dashboardtype = { formatRules: { filterBy: {}, //filterBase can only have two values user or record filterWorkflowStepsBy: [], //values: empty array (all) or a list with particular types i.e. [ 'draft', 'finalised' ] - queryFilters: [ - { - filterType: 'text', - filterFields: [ - { name: 'Title', - path: 'metadata.title' }, - { name: 'Contributor', - path: 'metadata.contributor_ci.text_full_name' - } - ] - } - ], + queryFilters: { + party: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + }, + { + filterType: 'date-range', + filterFields: [ + { + name: 'Filter by date modified', + path: 'lastSaveDate' + }, + { + name: 'Filter by date created', + path: 'dateCreated' + } + ] + } + ], + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required From 02e7b2e722d9ea9bb7b49e59864de449b2f12e22 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 14 Jun 2024 07:06:58 +0000 Subject: [PATCH 07/30] Improve text filter implementation in dashboard component and remove date range filters --- .../src/app/dashboard.component.html | 80 +++++-------------- .../dashboard/src/app/dashboard.component.ts | 53 +++++++----- .../dashboard/src/app/dashboard.module.ts | 6 +- .../src/lib/dashboard-models.ts | 7 -- config/dashboardtype.js | 31 +++++-- 5 files changed, 77 insertions(+), 100 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 155a8abe87..1092ed3fcd 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -1,72 +1,30 @@
-

- {{ 'dashboard-heading' | i18next: {stage: workflowStep.config.workflow.stageLabel, recordTypeName: typeLabel} }} -

-

- {{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }} -

-
-
-
-
-
- -
-
- -
- - - - -
-
-
- -
- - -
-
-
+
+
+

{{ 'dashboard-heading' | i18next: {stage: workflowStep.config.workflow.stageLabel, recordTypeName: typeLabel} }}

- -
-
-
-
-
{{ 'record-search-basic-search' | i18next }}
- -
- - - -
-

- +
+

{{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }}

+
+ +
+
+ + + +
-
-
+
+
-
{{ 'no-records' | i18next: {stage: workflowStep.config.workflow.stageLabel.toLowerCase(), recordTypeName: typeLabel.toLowerCase()} }} diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 6bce46bdf6..76ad4979e4 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -4,6 +4,7 @@ import { BaseComponent, UtilityService, LoggerService, TranslationService, Recor import { get as _get, set as _set, isEmpty as _isEmpty, isUndefined as _isUndefined, trim as _trim, isNull as _isNull, orderBy as _orderBy, map as _map, find as _find, indexOf as _indexOf, isArray as _isArray, forEach as _forEach, join as _join, first as _first } from 'lodash-es'; import { LoDashTemplateUtilityService } from 'projects/researchdatabox/portal-ng-common/src/lib/lodash-template-utility.service'; +import * as _ from 'lodash'; @Component({ selector: 'dashboard', @@ -28,14 +29,13 @@ export class DashboardComponent extends BaseComponent { rulesService: object; currentUser: object = {}; enableSort: boolean = true; - filterField: string = 'Title'; + filterFieldName: string = 'Title'; filterFieldPath: string = 'metadata.title'; + defaultFilterField: FilterField = { name: this.filterFieldName, path: this.filterFieldPath }; filterSearchString: string = ''; hideWorkflowStepTitle: boolean = false; - dateRangeFilterParams: any = {}; - datePickerOpts = { dateInputFormat: 'DD/MM/YYYY', containerClass: 'theme-dark-blue' }; - datePickerPlaceHolder: string = ''; - appName:string = 'dashboard'; + isFilterSearchDisplayed: boolean = false; + isSearching: boolean = false; defaultTableConfig = [ { @@ -163,11 +163,6 @@ export class DashboardComponent extends BaseComponent { protected override async initComponent(): Promise { if (_indexOf(this.dashboardTypeOptions, this.dashboardTypeSelected) >= 0) { - const sysConfig = await this.configService.getConfig(); - const defaultDatePickerOpts = { dateInputFormat: 'DD/MM/YYYY', containerClass: 'theme-dark-blue' }; - const defaultDatePickerPlaceHolder = 'dd/mm/yyyy'; - this.datePickerOpts = ConfigService._getAppConfigProperty(sysConfig, this.appName, 'datePickerOpts', defaultDatePickerOpts); - this.datePickerPlaceHolder = ConfigService._getAppConfigProperty(sysConfig, this.appName, 'datePickerPlaceHolder', defaultDatePickerPlaceHolder); this.loggerService.debug(`Dashboard waiting for deps to init...`); this.loggerService.debug(`Dashboard initialised.`); this.config = this.recordService.getConfig(); @@ -370,6 +365,10 @@ export class DashboardComponent extends BaseComponent { } else { planTable = this.evaluatePlanTableColumns({}, {}, {}, evaluateStepName, stagedRecords); + + let filter: FilterField = this.getFirstTextFilter(); + this.filterFieldName = filter.name; + this.filterFieldPath = filter.path; } this.records[evaluateStepName] = planTable; @@ -756,6 +755,18 @@ export class DashboardComponent extends BaseComponent { return 'metaMetadata.lastSaveDate:-1'; } + private getFirstFilter(type:string): FilterField { + let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; + for(let queryFilter of queryFilters) { + if(queryFilter.filterType == type) { + for(let filterField of queryFilter.filterFields) { + return filterField; + } + } + } + return this.defaultFilterField; + } + private getFilters(type:string) { let filterFields: FilterField[] = []; let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; @@ -769,35 +780,37 @@ export class DashboardComponent extends BaseComponent { return filterFields; } - - public getTextFilters() { - return this.getFilters('text'); + private getFirstTextFilter(): FilterField { + return this.getFirstFilter('text'); } - public getDateRangeFilters() { - return this.getFilters('date-range'); + public getTextFilters() { + return this.getFilters('text'); } public async filterChanged(step: string) { if (this.dashboardTypeSelected == 'standard') { + this.isSearching = true; + this.isFilterSearchDisplayed = true; let sortDetails = this.sortMap[step]; let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; + this.isSearching = false; } } - public async resetFilterAndSearch(step: string) { - + public async resetFilterAndSearch(step: string, e: any) { if (this.dashboardTypeSelected == 'standard') { + this.setFilterField(this.getFirstTextFilter(), e); + this.isSearching = true; let sortDetails = this.sortMap[step]; - this.filterField = 'Title'; - this.filterFieldPath = 'metadata.title'; this.filterSearchString = ''; let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; + this.isSearching = false; } } @@ -805,7 +818,7 @@ export class DashboardComponent extends BaseComponent { if (e) { e.preventDefault(); } - this.filterField = filterField.name; + this.filterFieldName = filterField.name; this.filterFieldPath = filterField.path; } diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts index a2097d8589..a189561a46 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts @@ -1,12 +1,10 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; -import { RedboxPortalCoreModule, trimLastSlashFromUrl } from '@researchdatabox/portal-ng-common'; import { APP_BASE_HREF, PlatformLocation } from '@angular/common'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { PaginationModule } from 'ngx-bootstrap/pagination'; +import { RedboxPortalCoreModule, trimLastSlashFromUrl } from '@researchdatabox/portal-ng-common'; import { DashboardComponent } from './dashboard.component'; import { SortComponent } from './sort/sort.component'; @@ -18,9 +16,7 @@ import { SortComponent } from './sort/sort.component'; imports: [ BrowserModule, FormsModule, - BrowserAnimationsModule, RedboxPortalCoreModule, - BsDatepickerModule.forRoot(), PaginationModule.forRoot(), TooltipModule.forRoot() ], diff --git a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts index 232a931417..4158ab51e5 100644 --- a/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts +++ b/angular/projects/researchdatabox/portal-ng-common/src/lib/dashboard-models.ts @@ -22,13 +22,6 @@ export class Plan { dashboardTitle: string = ''; } -// export declare class FilterBy { -// filterBase: string; -// filterBaseFieldOrValue: string; -// filterField: string; -// filterMode: string; -// } - export declare class FilterField { name: string; path: string; diff --git a/config/dashboardtype.js b/config/dashboardtype.js index c1c8d86c90..0b2080e368 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -17,22 +17,39 @@ module.exports.dashboardtype = { path: 'metadata.contributor_ci.text_full_name' } ] - }, + } + ], + rdmp: [ { - filterType: 'date-range', + filterType: 'text', filterFields: [ { - name: 'Filter by date modified', - path: 'lastSaveDate' + name: 'Title', + path: 'metadata.title' }, { - name: 'Filter by date created', - path: 'dateCreated' + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' } ] } ], - rdmp: [ + dataRecord: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + } + ], + dataPublication: [ { filterType: 'text', filterFields: [ From c1b429db388dce8bba1c2fcabd89ff10ccc524dc Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 14 Jun 2024 07:24:45 +0000 Subject: [PATCH 08/30] Change initialise text filters only for standard dashboard --- .../dashboard/src/app/dashboard.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 76ad4979e4..6d4c5d1dab 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -366,9 +366,11 @@ export class DashboardComponent extends BaseComponent { planTable = this.evaluatePlanTableColumns({}, {}, {}, evaluateStepName, stagedRecords); - let filter: FilterField = this.getFirstTextFilter(); - this.filterFieldName = filter.name; - this.filterFieldPath = filter.path; + if (this.dashboardTypeSelected == 'standard') { + let filter: FilterField = this.getFirstTextFilter(); + this.filterFieldName = filter.name; + this.filterFieldPath = filter.path; + } } this.records[evaluateStepName] = planTable; From cf9641429064b027d5a32d001a2deb50bfcdc763 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 14 Jun 2024 07:50:16 +0000 Subject: [PATCH 09/30] Make init first text filter more robust in dashboard component --- .../dashboard/src/app/dashboard.component.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 6d4c5d1dab..8092eeb3cf 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -758,15 +758,19 @@ export class DashboardComponent extends BaseComponent { } private getFirstFilter(type:string): FilterField { - let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; - for(let queryFilter of queryFilters) { - if(queryFilter.filterType == type) { - for(let filterField of queryFilter.filterFields) { - return filterField; + try { + let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; + for(let queryFilter of queryFilters) { + if(queryFilter.filterType == type) { + for(let filterField of queryFilter.filterFields) { + return filterField; + } } } + return this.defaultFilterField; + } catch(error) { + return this.defaultFilterField; } - return this.defaultFilterField; } private getFilters(type:string) { From 1eacd9985a0f01ea5d628f8085db817af995f671 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 18 Jun 2024 06:15:06 +0000 Subject: [PATCH 10/30] Initial implementation of infer schema and generate form methods in form service --- .../src/app/dashboard.component.spec.ts | 8 +- .../dashboard/src/app/dashboard.component.ts | 111 ++++++------ config/dashboardtype.js | 4 +- config/workflow.js | 38 ++++- package-lock.json | 6 + package.json | 1 + .../api/controllers/RecordController.ts | 96 +++++++++++ typescript/api/services/FormsService.ts | 159 +++++++++++++++++- 8 files changed, 364 insertions(+), 59 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts index 51b39c6f89..e95cbbb25f 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts @@ -149,7 +149,7 @@ describe('DashboardComponent standard', () => { const dashboardComponent = fixture.componentInstance; await dashboardComponent.initView('rdmp'); expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); - expect(dashboardComponent.defaultTableConfig.length).toBeGreaterThan(0); + expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('standard'); await dashboardComponent.initStep('draft','draft','rdmp','',1); let planTable = dashboardComponent.evaluatePlanTableColumns({}, {}, {}, 'draft', recordDataStandard['records']); @@ -306,7 +306,7 @@ describe('DashboardComponent workspace', () => { dashboardComponent.dashboardTypeSelected = 'workspace'; await dashboardComponent.initView('workspace'); expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); - expect(dashboardComponent.defaultTableConfig.length).toBeGreaterThan(0); + expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('workspace'); await dashboardComponent.initStep('','existing-locations-draft','','workspace',1); let planTable = dashboardComponent.evaluatePlanTableColumns({}, {}, {}, 'existing-locations-draft', recordDataWorkspace['records']); @@ -520,7 +520,7 @@ describe('DashboardComponent consolidated group by record type', () => { dashboardComponent.dashboardTypeSelected = 'consolidated'; await dashboardComponent.initView('consolidated'); expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); - expect(dashboardComponent.defaultTableConfig.length).toBeGreaterThan(0); + expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('consolidated'); await dashboardComponent.initStep('','consolidated','rdmp','',1); let groupedRecords = recordDataConsolidated['groupedRecords']; @@ -761,7 +761,7 @@ describe('DashboardComponent consolidated group by relationships', () => { dashboardComponent.dashboardTypeSelected = 'consolidated'; await dashboardComponent.initView('consolidated'); expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); - expect(dashboardComponent.defaultTableConfig.length).toBeGreaterThan(0); + expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('consolidated'); await dashboardComponent.initStep('','consolidated','rdmp','',1); let groupedRecords = recordDataConsolidatedRelationships['groupedRecords']; diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 8092eeb3cf..9e42dee2b7 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -37,7 +37,7 @@ export class DashboardComponent extends BaseComponent { isFilterSearchDisplayed: boolean = false; isSearching: boolean = false; - defaultTableConfig = [ + defaultRowConfig = [ { title: 'Record Title', variable: 'metadata.title', @@ -185,9 +185,8 @@ export class DashboardComponent extends BaseComponent { this.groupRowConfig = this.defaultGroupRowConfig; this.groupRowRules = this.defaultGroupRowRules; - let dashboardType: any = await this.recordService.getDashboardType(this.dashboardTypeSelected); - let formatRules: FormatRules = _get(dashboardType, 'formatRules'); - let startIndex = 1; + let dashboardTypeConfig: any = await this.recordService.getDashboardType(this.dashboardTypeSelected); + let formatRules: FormatRules = _get(dashboardTypeConfig, 'formatRules'); if (!_isUndefined(formatRules) && !_isNull(formatRules) && !_isEmpty(formatRules)) { //global format rules from dashboardtype.js config this.formatRules = formatRules; @@ -204,38 +203,20 @@ export class DashboardComponent extends BaseComponent { } } - let beforeFilterSteps: any = await this.recordService.getWorkflowSteps(recordType); - - let filterWorkflowStepsBy = _get(this.formatRules, 'filterWorkflowStepsBy'); - let steps = []; - - if (!_isUndefined(filterWorkflowStepsBy) && _isArray(filterWorkflowStepsBy) && !_isEmpty(filterWorkflowStepsBy)) { - for (let bfStep of beforeFilterSteps) { - let filterByStage = _get(bfStep, 'config.workflow.stage'); - if (!_isUndefined(filterByStage)) { - let indexFilterByStage = _indexOf(filterWorkflowStepsBy, filterByStage); - if (indexFilterByStage >= 0) { - steps.push(bfStep); - } - } - } - } else { - steps = beforeFilterSteps; - } - steps = _orderBy(steps, ['config.displayIndex'], ['asc']); + let steps = await this.initWorkflowSteps(recordType); + let startIndex = 1; for (let step of steps) { - this.workflowSteps.push(step); + this.initStepTableConfig(recordType, step); - let stepTableConfig = this.initStepTableConfig(recordType, step); - - this.initSortConfig(step, stepTableConfig); + this.initSortConfig(step); let packageType = ''; let stepName = ''; let evaluateStepName = ''; if (this.dashboardTypeSelected == 'consolidated') { + this.workflowSteps.push(step); packageType = ''; stepName = ''; evaluateStepName = _get(step, 'name'); @@ -255,16 +236,43 @@ export class DashboardComponent extends BaseComponent { } } + private async initWorkflowSteps(recordType: string) { + + let beforeFilterSteps: any = await this.recordService.getWorkflowSteps(recordType); + + let filterWorkflowStepsBy = _get(this.formatRules, 'filterWorkflowStepsBy'); + let steps = []; + + if (!_isUndefined(filterWorkflowStepsBy) && _isArray(filterWorkflowStepsBy) && !_isEmpty(filterWorkflowStepsBy)) { + for (let bfStep of beforeFilterSteps) { + let filterByStage = _get(bfStep, 'config.workflow.stage'); + if (!_isUndefined(filterByStage)) { + let indexFilterByStage = _indexOf(filterWorkflowStepsBy, filterByStage); + if (indexFilterByStage >= 0) { + steps.push(bfStep); + } + } + } + } else { + steps = beforeFilterSteps; + } + steps = _orderBy(steps, ['config.displayIndex'], ['asc']); + return steps; + } + private initStepTableConfig(recordType: string, step: any) { - let stepTableConfig = this.defaultTableConfig; - if (_isEmpty(this.defaultTableConfig[0].title)) { - this.defaultTableConfig[0].title = `${recordType}-title` || 'Title'; + + let stepRowConfig = this.defaultRowConfig; + + if (_isEmpty(this.defaultRowConfig[0].title)) { + this.defaultRowConfig[0].title = `${recordType}-title` || 'Title'; } + if (!_isUndefined(_get(step, 'config.dashboard')) && !_isUndefined(_get(step, 'config.dashboard.table'))) { if (!_isUndefined(_get(step, 'config.dashboard.table.rowConfig'))) { - stepTableConfig = _get(step, 'config.dashboard.table.rowConfig'); + stepRowConfig = _get(step, 'config.dashboard.table.rowConfig'); this.sortFields = _map(_get(step, 'config.dashboard.table.rowConfig'), (config) => { return config.variable; }); } @@ -285,16 +293,19 @@ export class DashboardComponent extends BaseComponent { this.formatRules = _get(step, 'config.dashboard.table.formatRules'); } } - return stepTableConfig; + + this.tableConfig[step.name] = stepRowConfig; } - private initSortConfig(step: any, stepTableConfig: any[]) { + private initSortConfig(step: any) { + + let stepRowConfig: any[] = this.tableConfig[step.name]; - this.tableConfig[step.name] = stepTableConfig; this.sortMap[step.name] = {}; - for (let rowConfig of stepTableConfig) { - this.sortMap[step.name][rowConfig.variable] = { - sort: rowConfig.initialSort + + for (let columnConfig of stepRowConfig) { + this.sortMap[step.name][columnConfig.variable] = { + sort: columnConfig.initialSort }; } @@ -490,13 +501,11 @@ export class DashboardComponent extends BaseComponent { let record: any = {}; - let stepTableConfig = _isEmpty(this.tableConfig[stepName]) ? this.defaultTableConfig : this.tableConfig[stepName]; - - for (let rowConfig of stepTableConfig) { + let stepRowConfig: any[] = this.tableConfig[stepName]; - - const templateRes = this.runTemplate(rowConfig.template, imports) - record[rowConfig.variable] = templateRes; + for (let columnConfig of stepRowConfig) { + const templateRes = this.runTemplate(columnConfig.template, imports) + record[columnConfig.variable] = templateRes; } recordRows.push(record); } @@ -533,12 +542,11 @@ export class DashboardComponent extends BaseComponent { _set(imports, 'translationService', this.translationService); let record: any = {}; - let stepTableCOnfig = _isEmpty(this.tableConfig[stepName]) ? this.defaultTableConfig : this.tableConfig[stepName]; - - for (let rowConfig of stepTableCOnfig) { + let stepRowConfig = this.tableConfig[stepName]; - const templateRes = this.runTemplate(rowConfig.template, imports); - record[rowConfig.variable] = templateRes; + for (let columnConfig of stepRowConfig) { + const templateRes = this.runTemplate(columnConfig.template, imports); + record[columnConfig.variable] = templateRes; } recordRows.push(record); } @@ -700,9 +708,10 @@ export class DashboardComponent extends BaseComponent { } private updateSortMap(sortData: any) { - let stepTableConfig = this.tableConfig[sortData.step]; - for (let rowConfig of stepTableConfig) { - this.sortMap[sortData.step][rowConfig.variable] = { sort: rowConfig.noSort }; + + let stepRowConfig: any[] = this.tableConfig[sortData.step]; + for (let columnConfig of stepRowConfig) { + this.sortMap[sortData.step][columnConfig.variable] = { sort: columnConfig.noSort }; } this.sortMap[sortData.step][sortData.variable] = { sort: sortData.sort }; diff --git a/config/dashboardtype.js b/config/dashboardtype.js index 0b2080e368..24952313d4 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -79,7 +79,7 @@ module.exports.dashboardtype = { sortBy: 'metaMetadata.lastSaveDate:-1', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships sortGroupBy: [], //values: as many levels as required - hideWorkflowStepTitleForRecordType: ['party'] + hideWorkflowStepTitleForRecordType: [] } }, "consolidated": { @@ -91,7 +91,7 @@ module.exports.dashboardtype = { sortGroupBy: [{ rowLevel: 0, compareFieldValue: 'rdmp', compareField: 'metadata.metaMetadata.type', relatedTo: '' }, { rowLevel: 1, compareFieldValue: 'dataRecord', compareField: 'metadata.metaMetadata.type', relatedTo: 'metadata.metadata.rdmp.oid' }, { rowLevel: 2, compareFieldValue: 'dataPublication', compareField: 'metadata.metaMetadata.type', relatedTo: 'metadata.metadata.dataRecord.oid' }], //values: as many levels as required - hideWorkflowStepTitleForRecordType: ['party'] + hideWorkflowStepTitleForRecordType: [] } } }; diff --git a/config/workflow.js b/config/workflow.js index f658e76b95..fc2257f5e9 100644 --- a/config/workflow.js +++ b/config/workflow.js @@ -294,7 +294,43 @@ module.exports.workflow = { viewRoles: ['Admin', 'Librarians'], editRoles: ['Admin', 'Librarians'] }, - form: 'default-1.0-draft' + form: 'generated-view-only', + dashboard: { + table: { + rowConfig: [ + { + title: 'Record Title', + variable: 'metadata.title', + template: `<%= metadata.title %>`, + initialSort: 'desc' + }, + { + title: 'header-ci', + variable: 'metadata.contributor_ci.text_full_name', + template: '<%= metadata.contributor_ci != undefined ? metadata.contributor_ci.text_full_name : "" %>', + initialSort: 'desc' + }, + { + title: 'header-data-manager', + variable: 'metadata.contributor_data_manager.text_full_name', + template: '<%= metadata.contributor_data_manager != undefined ? metadata.contributor_data_manager.text_full_name : "" %>', + initialSort: 'desc' + }, + { + title: 'header-created', + variable: 'metaMetadata.createdOn', + template: '<%= util.formatDateLocale(util.parseDateString(dateCreated), "DATETIME_MED") %>', + initialSort: 'desc' + }, + { + title: 'header-modified', + variable: 'metaMetadata.lastSaveDate', + template: '<%= util.formatDateLocale(util.parseDateString(dateModified),"DATETIME_MED") %>', + initialSort: 'desc' + } + ] + } + } }, starting: true } diff --git a/package-lock.json b/package-lock.json index 6ca1e28b27..2b1750b51a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "flat": "^6.0.1", "font-awesome-sass": "4.7.0", "fs-extra": "^11.2.0", + "genson-js": "^0.0.8", "glob": "^10.4.1", "har-validator": "5.1.5", "i18next": "^23.11.5", @@ -3525,6 +3526,11 @@ "node": ">=12" } }, + "node_modules/genson-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/genson-js/-/genson-js-0.0.8.tgz", + "integrity": "sha512-4NUusDTwF+lzYh72uKV+Uvpky9iPO+YDIMpGImA5pbHfLV9HwgRCA4hYjGu78V4J4Cx2IZRTFfRERn9aUs74mw==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "license": "ISC", diff --git a/package.json b/package.json index f28521a93d..d037523321 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "flat": "^6.0.1", "font-awesome-sass": "4.7.0", "fs-extra": "^11.2.0", + "genson-js": "^0.0.8", "glob": "^10.4.1", "har-validator": "5.1.5", "i18next": "^23.11.5", diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 203e790ad1..3ebe3527d4 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -228,6 +228,102 @@ export module Controllers { } public getForm(req, res) { + const brand:BrandingModel = BrandingService.getBrand(req.session.branding); + const name = req.param('name'); + const oid = req.param('oid'); + const editMode = req.query.edit == "true"; + const formParam = req.param('formName'); + let obs = null; + if (_.isEmpty(oid)) { + obs = FormsService.getForm(brand.id, name, editMode, true).flatMap(form => { + let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}).then(fields => { + form.fields = fields; + return form; + }); + return mergedForm; + }); + } else { + // defaults to retrive the form of the current workflow state... + obs = Observable.fromPromise(this.recordsService.getMeta(oid)).flatMap(currentRec => { + if (_.isEmpty(currentRec)) { + return Observable.throw(new Error(`Error, empty metadata for OID: ${oid}`)); + } + // allow client to set the form name to use + const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; + + if(formName == 'generated-view-only') { + let schema = FormsService.inferSchemaFromMetadata(currentRec); + _.set(currentRec,'schema',schema); + const user = req.user; + this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); + } + + if (editMode) { + return this.hasEditAccess(brand, req.user, currentRec) + .flatMap(hasEditAccess => { + if (!hasEditAccess) { + return Observable.throw(new Error(TranslationService.t('edit-error-no-permissions'))); + } + return FormsService.getFormByName(formName, editMode).flatMap(form => { + if (_.isEmpty(form)) { + return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); + } + let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { + form.fields = fields; + + return form; + }); + return mergedForm; + }); + }); + } else { + return this.hasViewAccess(brand, req.user, currentRec) + .flatMap(hasViewAccess => { + if (!hasViewAccess) { + return Observable.throw(new Error(TranslationService.t('view-error-no-permissions'))); + } + return this.hasEditAccess(brand, req.user, currentRec); + }) + .flatMap(hasEditAccess => { + return FormsService.getFormByName('default-1.0-draft', editMode).flatMap(form => { + if (_.isEmpty(form)) { + return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); + } + if(formName == 'generated-view-only') { + form = FormsService.generateFormFromSchema(currentRec); + } + FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); + return this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { + form.fields = fields; + + return form; + }); + }); + }); + } + }); + } + obs.subscribe(form => { + if (!_.isEmpty(form)) { + this.ajaxOk(req, res, null, form); + } else { + this.ajaxFail(req, res, null, { + message: `Failed to get form with name:${name}` + }); + } + }, error => { + sails.log.error("Error getting form definition:"); + sails.log.error(error); + let message = error.message; + if (error.error && error.error.code == 500) { + message = TranslationService.t('missing-record'); + } + this.ajaxFail(req, res, message); + }); + + } + + public getFormByName(req, res) { const brand:BrandingModel = BrandingService.getBrand(req.session.branding); const name = req.param('name'); const oid = req.param('oid'); diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 7505a8a5b7..50c47ed746 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -25,6 +25,7 @@ import { Sails, Model } from "sails"; +import { createSchema } from 'genson-js'; declare var sails: Sails; declare var Form: Model; @@ -48,7 +49,9 @@ export module Services { 'flattenFields', 'getFormByName', 'filterFieldsHasEditAccess', - 'listForms' + 'listForms', + 'inferSchemaFromMetadata', + 'generateFormFromSchema' ]; public async bootstrap(workflowStep): Promise { @@ -188,6 +191,160 @@ export module Services { }).filter(result => result !== null).last(); } + public inferSchemaFromMetadata(record: any): any { + const schema = createSchema(record.metadata); + // sails.log.verbose(schema); + return schema; + } + + public generateFormFromSchema(record: any): FormModel { + + let form: FormModel; + + let schema = this.inferSchemaFromMetadata(record); + + let fieldKeys = _.keys(schema.properties); + + let buttonsList = [ + { + class: "AnchorOrButton", + viewOnly: true, + definition: { + label: '@dmp-edit-record-link', + value: '/@branding/@portal/record/edit/@oid', + cssClasses: 'btn btn-large btn-info', + showPencil: true, + controlType: 'anchor' + }, + variableSubstitutionFields: ['value'] + }, + { + class: "AnchorOrButton", + roles: ['Admin', 'Librarians'], + viewOnly: true, + definition: { + label: '@view-record-audit-link', + value: '/@branding/@portal/record/viewAudit/@oid', + cssClasses: 'btn btn-large btn-info margin-15', + controlType: 'anchor' + }, + variableSubstitutionFields: ['value'] + } + ]; + + let textFieldTemplate = { + class: 'TextField', + viewOnly: true, + definition: { + name: '', + label: '', + help: '', + type: 'text' + } + }; + + let objectFieldHeadingTemplate = { + class: 'Container', + compClass: 'TextBlockComponent', + definition: { + value: '', + type: 'h3' + } + }; + + let fieldList = [ + { + class: 'Container', + compClass: 'TextBlockComponent', + viewOnly: true, + definition: { + name: 'title', + type: 'h1' + } + }, + { + class: 'Container', + compClass: 'GenericGroupComponent', + definition: { + cssClasses: "form-inline", + fields: buttonsList + } + } + + // { + // class: 'TextArea', + // viewOnly: true, + // definition: { + // name: 'description', + // label: '@dmpt-description' + // } + // }, + + + // { + // class: "Container", + // definition: { + // id: "project", + // label: "project-tab", + // fields: [ + // ] + // } + // } + + + // { + // class: "TabOrAccordionContainer", + // compClass: "TabOrAccordionContainerComponent", + // definition: { + // id: "mainTab", + // accContainerClass: "view-accordion", + // expandAccordionsOnOpen: true, + // fields: [ + // ] + // } + // } + ]; + + for(let fieldKey of fieldKeys) { + if(schema.properties[fieldKey].type == 'string') { + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey); + _.set(textField.definition,'label',fieldKey); + fieldList.push(textField); + } else if(schema.properties[fieldKey].type == 'object') { + + let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); + _.set(objectFieldHeading.definition, 'value', fieldKey); + fieldList.push(objectFieldHeading); + + let objectFieldKeys = _.keys(schema.properties[fieldKey].properties); + for(let objectFieldKey of objectFieldKeys) { + if(schema.properties[fieldKey].properties[objectFieldKey].type == 'string') { + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',objectFieldKey); + _.set(textField.definition,'label',objectFieldKey); + fieldList.push(textField); + } + } + } + } + + let formObject = { + name: 'generated-view-only', + type: record.metaMetadata.type, + skipValidationOnSave: false, + editCssClasses: 'row col-md-12', + viewCssClasses: 'row col-md-offset-1 col-md-10', + messages: {}, + attachmentFields: [], + fields: fieldList + }; + + form = formObject as any; + + return form; + } + protected setFormEditMode(fields, editMode): void{ _.remove(fields, field => { if (editMode) { From 23fa3ec565a703fc223c41089d1128125055a2fe Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 18 Jun 2024 06:52:40 +0000 Subject: [PATCH 11/30] Fix dashboard component angular test --- .../dashboard/src/app/dashboard.component.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts index e95cbbb25f..0fc1119280 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts @@ -148,7 +148,6 @@ describe('DashboardComponent standard', () => { const fixture = TestBed.createComponent(DashboardComponent); const dashboardComponent = fixture.componentInstance; await dashboardComponent.initView('rdmp'); - expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('standard'); await dashboardComponent.initStep('draft','draft','rdmp','',1); @@ -305,7 +304,6 @@ describe('DashboardComponent workspace', () => { const dashboardComponent = fixture.componentInstance; dashboardComponent.dashboardTypeSelected = 'workspace'; await dashboardComponent.initView('workspace'); - expect(dashboardComponent.workflowSteps.length).toBeGreaterThan(0); expect(dashboardComponent.defaultRowConfig.length).toBeGreaterThan(0); expect(dashboardComponent.dashboardTypeSelected).toEqual('workspace'); await dashboardComponent.initStep('','existing-locations-draft','','workspace',1); From 349e3725eb77bd70b1d34087c95b9bdac981d47b Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Wed, 19 Jun 2024 04:37:24 +0000 Subject: [PATCH 12/30] Refactor workflow steps service and intial refactor of forms service --- .../api/controllers/RecordController.ts | 22 ++---- typescript/api/services/FormsService.ts | 73 +++++++++++++++---- .../api/services/WorkflowStepsService.ts | 6 +- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 3ebe3527d4..d88d488a19 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -235,7 +235,7 @@ export module Controllers { const formParam = req.param('formName'); let obs = null; if (_.isEmpty(oid)) { - obs = FormsService.getForm(brand.id, name, editMode, true).flatMap(form => { + obs = FormsService.getFormByStartingWorkflowStep(brand.id, name, editMode).flatMap(form => { let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}).then(fields => { form.fields = fields; return form; @@ -248,15 +248,6 @@ export module Controllers { if (_.isEmpty(currentRec)) { return Observable.throw(new Error(`Error, empty metadata for OID: ${oid}`)); } - // allow client to set the form name to use - const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; - - if(formName == 'generated-view-only') { - let schema = FormsService.inferSchemaFromMetadata(currentRec); - _.set(currentRec,'schema',schema); - const user = req.user; - this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); - } if (editMode) { return this.hasEditAccess(brand, req.user, currentRec) @@ -264,9 +255,9 @@ export module Controllers { if (!hasEditAccess) { return Observable.throw(new Error(TranslationService.t('edit-error-no-permissions'))); } - return FormsService.getFormByName(formName, editMode).flatMap(form => { + return FormsService.getForm(formParam, editMode, currentRec).flatMap(form => { if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); + return Observable.throw(new Error(`Error, getting form ${formParam} for OID: ${oid}`)); } let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { form.fields = fields; @@ -285,12 +276,9 @@ export module Controllers { return this.hasEditAccess(brand, req.user, currentRec); }) .flatMap(hasEditAccess => { - return FormsService.getFormByName('default-1.0-draft', editMode).flatMap(form => { + return FormsService.getForm('default-1.0-draft', editMode, currentRec).flatMap(form => { if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); - } - if(formName == 'generated-view-only') { - form = FormsService.generateFormFromSchema(currentRec); + return Observable.throw(new Error(`Error, getting form ${formParam} for OID: ${oid}`)); } FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); return this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 50c47ed746..4e86f742e6 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -160,7 +160,31 @@ export module Services { }); } - public getForm = (branding, recordType, editMode, starting: boolean): Observable => { + public getForm = (formParam: string, editMode: boolean, currentRec: any): Observable => { + + // allow client to set the form name to use + const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; + + if(formName == 'generated-view-only') { + return super.getObservable(this.generateFormFromSchema(currentRec)); + } else { + + return this.getFormByName(formName, editMode); + } + + + // if(formName == 'generated-view-only') { + // let schema = this.inferSchemaFromMetadata(currentRec); + // _.set(currentRec,'schema',schema); + // const user = req.user; + // this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); + // } + + } + + public getFormByStartingWorkflowStep = (branding, recordType, editMode): Observable => { + + let starting = true; return super.getObservable(RecordType.findOne({ key: branding + "_" + recordType @@ -199,6 +223,8 @@ export module Services { public generateFormFromSchema(record: any): FormModel { + let recordType = record.metaMetadata.type; + let form: FormModel; let schema = this.inferSchemaFromMetadata(record); @@ -206,29 +232,48 @@ export module Services { let fieldKeys = _.keys(schema.properties); let buttonsList = [ + // { + // class: "AnchorOrButton", + // viewOnly: true, + // definition: { + // label: '@dmp-edit-record-link', + // value: '/@branding/@portal/record/edit/@oid', + // cssClasses: 'btn btn-large btn-info', + // showPencil: true, + // controlType: 'anchor' + // }, + // variableSubstitutionFields: ['value'] + // }, { - class: "AnchorOrButton", + class: 'AnchorOrButton', + roles: ['Admin', 'Librarians'], viewOnly: true, definition: { - label: '@dmp-edit-record-link', - value: '/@branding/@portal/record/edit/@oid', - cssClasses: 'btn btn-large btn-info', - showPencil: true, + label: '@view-record-audit-link', + value: '/@branding/@portal/record/viewAudit/@oid', + cssClasses: 'btn btn-large btn-info margin-15', controlType: 'anchor' }, variableSubstitutionFields: ['value'] }, { - class: "AnchorOrButton", - roles: ['Admin', 'Librarians'], + class: 'SaveButton', viewOnly: true, + roles: ['Admin', 'Librarians'], definition: { - label: '@view-record-audit-link', - value: '/@branding/@portal/record/viewAudit/@oid', - cssClasses: 'btn btn-large btn-info margin-15', - controlType: 'anchor' + name: 'confirmDelete', + label: 'Delete this record', + closeOnSave: true, + redirectLocation: '/@branding/@portal/dashboard/'+recordType, + cssClasses: 'btn-danger', + confirmationMessage: '@dataPublication-confirmDelete', + confirmationTitle: '@dataPublication-confirmDeleteTitle', + cancelButtonMessage: '@dataPublication-cancelButtonMessage', + confirmButtonMessage: '@dataPublication-confirmButtonMessage', + isDelete: true, + isSubmissionButton: true }, - variableSubstitutionFields: ['value'] + variableSubstitutionFields: ['redirectLocation'] } ]; @@ -331,7 +376,7 @@ export module Services { let formObject = { name: 'generated-view-only', - type: record.metaMetadata.type, + type: recordType, skipValidationOnSave: false, editCssClasses: 'row col-md-12', viewCssClasses: 'row col-md-offset-1 col-md-10', diff --git a/typescript/api/services/WorkflowStepsService.ts b/typescript/api/services/WorkflowStepsService.ts index 0ee7b84921..bf54cc403d 100644 --- a/typescript/api/services/WorkflowStepsService.ts +++ b/typescript/api/services/WorkflowStepsService.ts @@ -78,7 +78,11 @@ export module Services { for(let recordTypeName in wfSteps) { let workflowStepsObject = wfSteps[recordTypeName]; for (let workflowStep of workflowStepsObject){ - const workflowConf = sails.config.workflow[recordTypeName][workflowStep["workflow"]]; + let workflowConf = sails.config.workflow[recordTypeName][workflowStep["workflow"]]; + let form = _.get(workflowConf,'config.form',''); + if(form == '') { + _.set(workflowConf.config,'form','generated-view-only'); + } var obs = await this.create(workflowStep["recordType"], workflowStep["workflow"], workflowConf.config, workflowConf.starting == true, workflowConf['hidden']).toPromise(); workflowSteps.push(obs); }; From beb36d7b983fe9aaf60af264b4fe358f00745602 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Thu, 20 Jun 2024 07:34:41 +0000 Subject: [PATCH 13/30] Refactor get form method in record controller to promisify logic flow --- .../api/controllers/RecordController.ts | 197 +++++------------- typescript/api/services/FormsService.ts | 2 +- 2 files changed, 51 insertions(+), 148 deletions(-) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index d88d488a19..7ad4687c7f 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -227,173 +227,76 @@ export module Controllers { return Observable.of(this.recordsService.hasViewAccess(brand, user, user.roles, currentRec)); } - public getForm(req, res) { + public async getForm(req, res) { const brand:BrandingModel = BrandingService.getBrand(req.session.branding); const name = req.param('name'); const oid = req.param('oid'); const editMode = req.query.edit == "true"; const formParam = req.param('formName'); - let obs = null; - if (_.isEmpty(oid)) { - obs = FormsService.getFormByStartingWorkflowStep(brand.id, name, editMode).flatMap(form => { - let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}).then(fields => { - form.fields = fields; - return form; - }); - return mergedForm; - }); - } else { - // defaults to retrive the form of the current workflow state... - obs = Observable.fromPromise(this.recordsService.getMeta(oid)).flatMap(currentRec => { - if (_.isEmpty(currentRec)) { - return Observable.throw(new Error(`Error, empty metadata for OID: ${oid}`)); - } - - if (editMode) { - return this.hasEditAccess(brand, req.user, currentRec) - .flatMap(hasEditAccess => { - if (!hasEditAccess) { - return Observable.throw(new Error(TranslationService.t('edit-error-no-permissions'))); - } - return FormsService.getForm(formParam, editMode, currentRec).flatMap(form => { - if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formParam} for OID: ${oid}`)); - } - let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { - form.fields = fields; - - return form; - }); - return mergedForm; - }); - }); + try { + if (_.isEmpty(oid)) { + let form = await FormsService.getFormByStartingWorkflowStep(brand.id, name, editMode); + let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}); + form.fields = fields; + let mergedForm = form; + if (!_.isEmpty(mergedForm)) { + return this.ajaxOk(req, res, null, mergedForm); } else { - return this.hasViewAccess(brand, req.user, currentRec) - .flatMap(hasViewAccess => { - if (!hasViewAccess) { - return Observable.throw(new Error(TranslationService.t('view-error-no-permissions'))); - } - return this.hasEditAccess(brand, req.user, currentRec); - }) - .flatMap(hasEditAccess => { - return FormsService.getForm('default-1.0-draft', editMode, currentRec).flatMap(form => { - if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formParam} for OID: ${oid}`)); - } - FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); - return this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { - form.fields = fields; - - return form; - }); - }); - }); + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); } - }); - } - obs.subscribe(form => { - if (!_.isEmpty(form)) { - this.ajaxOk(req, res, null, form); } else { - this.ajaxFail(req, res, null, { - message: `Failed to get form with name:${name}` - }); - } - }, error => { - sails.log.error("Error getting form definition:"); - sails.log.error(error); - let message = error.message; - if (error.error && error.error.code == 500) { - message = TranslationService.t('missing-record'); - } - this.ajaxFail(req, res, message); - }); - - } - - public getFormByName(req, res) { - const brand:BrandingModel = BrandingService.getBrand(req.session.branding); - const name = req.param('name'); - const oid = req.param('oid'); - const editMode = req.query.edit == "true"; - const formParam = req.param('formName'); - let obs = null; - if (_.isEmpty(oid)) { - obs = FormsService.getForm(brand.id, name, editMode, true).flatMap(form => { - let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}).then(fields => { - form.fields = fields; - return form; - }); - return mergedForm; - }); - } else { - // defaults to retrive the form of the current workflow state... - obs = Observable.fromPromise(this.recordsService.getMeta(oid)).flatMap(currentRec => { + // defaults to retrive the form of the current workflow state... + let currentRec = await this.recordsService.getMeta(oid); if (_.isEmpty(currentRec)) { - return Observable.throw(new Error(`Error, empty metadata for OID: ${oid}`)); + return this.ajaxFail(req, res, null, {message: `Error, empty metadata for OID: ${oid}`}); } - // allow client to set the form name to use - const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; if (editMode) { - return this.hasEditAccess(brand, req.user, currentRec) - .flatMap(hasEditAccess => { - if (!hasEditAccess) { - return Observable.throw(new Error(TranslationService.t('edit-error-no-permissions'))); - } - return FormsService.getFormByName(formName, editMode).flatMap(form => { - if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); - } - let mergedForm = this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { - form.fields = fields; - - return form; - }); - return mergedForm; - }); - }); + let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); + if (!hasEditAccess) { + return this.ajaxFail(req, res, null, {message: TranslationService.t('edit-error-no-permissions')}); + } + let form = await FormsService.getForm(formParam, editMode, currentRec).toPromise(); + if (_.isEmpty(form)) { + return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); + } + let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); + form.fields = fields; + let mergedForm = form; + if (!_.isEmpty(mergedForm)) { + return this.ajaxOk(req, res, null, mergedForm); + } else { + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); + } } else { - return this.hasViewAccess(brand, req.user, currentRec) - .flatMap(hasViewAccess => { - if (!hasViewAccess) { - return Observable.throw(new Error(TranslationService.t('view-error-no-permissions'))); - } - return this.hasEditAccess(brand, req.user, currentRec) - }) - .flatMap(hasEditAccess => { - return FormsService.getFormByName(formName, editMode).flatMap(form => { - if (_.isEmpty(form)) { - return Observable.throw(new Error(`Error, getting form ${formName} for OID: ${oid}`)); - } - FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); - return this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec).then(fields => { - form.fields = fields; - - return form; - }); - }); - }); + let hasViewAccess = await this.hasViewAccess(brand, req.user, currentRec).toPromise(); + + if (!hasViewAccess) { + return this.ajaxFail(req, res, null, {message: TranslationService.t('view-error-no-permissions')}); + } + let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); + let form = await FormsService.getForm(formParam, editMode, currentRec).toPromise(); + if (_.isEmpty(form)) { + return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); + } + FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); + let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); + form.fields = fields; + if (!_.isEmpty(form)) { + return this.ajaxOk(req, res, null, form); + } else { + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); + } } - }); - } - obs.subscribe(form => { - if (!_.isEmpty(form)) { - this.ajaxOk(req, res, null, form); - } else { - this.ajaxFail(req, res, null, { - message: `Failed to get form with name:${name}` - }); } - }, error => { + } catch(error) { sails.log.error("Error getting form definition:"); sails.log.error(error); let message = error.message; if (error.error && error.error.code == 500) { message = TranslationService.t('missing-record'); } - this.ajaxFail(req, res, message); - }); - + return this.ajaxFail(req, res, message); + } } public create(req, res) { diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 4e86f742e6..dada8516c4 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -166,7 +166,7 @@ export module Services { const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; if(formName == 'generated-view-only') { - return super.getObservable(this.generateFormFromSchema(currentRec)); + return Observable.of(this.generateFormFromSchema(currentRec)); } else { return this.getFormByName(formName, editMode); From 88f52121cfcba731fd749fbf2eba2f548aa84878 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 21 Jun 2024 00:54:25 +0000 Subject: [PATCH 14/30] More refactoring to promisify getForm method in record controller --- typescript/api/controllers/RecordController.ts | 18 ++++++++++-------- typescript/api/services/FormsService.ts | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 7ad4687c7f..60668312d4 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -150,7 +150,7 @@ export module Controllers { sails.log.debug('RECORD::APP: ' + appName); sails.log.debug('RECORD::APP formName: ' + extFormName); if (recordType != '' && extFormName == '') { - FormsService.getForm(brand.id, recordType, true, true).subscribe(form => { + FormsService.getFormByStartingWorkflowStep(brand.id, recordType, true).subscribe(form => { if (form['customAngularApp'] != null) { appSelector = form['customAngularApp']['appSelector']; appName = form['customAngularApp']['appName']; @@ -229,20 +229,23 @@ export module Controllers { public async getForm(req, res) { const brand:BrandingModel = BrandingService.getBrand(req.session.branding); - const name = req.param('name'); + const recordType = req.param('name'); const oid = req.param('oid'); const editMode = req.query.edit == "true"; const formParam = req.param('formName'); try { if (_.isEmpty(oid)) { - let form = await FormsService.getFormByStartingWorkflowStep(brand.id, name, editMode); - let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, name, {}); + let form = await FormsService.getFormByStartingWorkflowStep(brand.id, recordType, editMode).toPromise(); + if (_.isEmpty(form)) { + return this.ajaxFail(req, res, null, {message: `Error, getting form for record type: ${recordType}`}); + } + let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, recordType, {}); form.fields = fields; let mergedForm = form; if (!_.isEmpty(mergedForm)) { return this.ajaxOk(req, res, null, mergedForm); } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); } } else { // defaults to retrive the form of the current workflow state... @@ -265,11 +268,10 @@ export module Controllers { if (!_.isEmpty(mergedForm)) { return this.ajaxOk(req, res, null, mergedForm); } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); } } else { let hasViewAccess = await this.hasViewAccess(brand, req.user, currentRec).toPromise(); - if (!hasViewAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('view-error-no-permissions')}); } @@ -284,7 +286,7 @@ export module Controllers { if (!_.isEmpty(form)) { return this.ajaxOk(req, res, null, form); } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${name}`}); + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); } } } diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index dada8516c4..7f591a82e2 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -51,7 +51,8 @@ export module Services { 'filterFieldsHasEditAccess', 'listForms', 'inferSchemaFromMetadata', - 'generateFormFromSchema' + 'generateFormFromSchema', + 'getFormByStartingWorkflowStep' ]; public async bootstrap(workflowStep): Promise { From acd14a7659aae5871409eb1b110d0c526bca883e Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 21 Jun 2024 01:01:35 +0000 Subject: [PATCH 15/30] Refactor to simplify redundant code in getForm method in record controller --- .../api/controllers/RecordController.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 60668312d4..63bcacfbd5 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -233,6 +233,7 @@ export module Controllers { const oid = req.param('oid'); const editMode = req.query.edit == "true"; const formParam = req.param('formName'); + let mergedForm: any = {}; try { if (_.isEmpty(oid)) { let form = await FormsService.getFormByStartingWorkflowStep(brand.id, recordType, editMode).toPromise(); @@ -241,12 +242,7 @@ export module Controllers { } let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, recordType, {}); form.fields = fields; - let mergedForm = form; - if (!_.isEmpty(mergedForm)) { - return this.ajaxOk(req, res, null, mergedForm); - } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); - } + mergedForm = form; } else { // defaults to retrive the form of the current workflow state... let currentRec = await this.recordsService.getMeta(oid); @@ -264,12 +260,7 @@ export module Controllers { } let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); form.fields = fields; - let mergedForm = form; - if (!_.isEmpty(mergedForm)) { - return this.ajaxOk(req, res, null, mergedForm); - } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); - } + mergedForm = form; } else { let hasViewAccess = await this.hasViewAccess(brand, req.user, currentRec).toPromise(); if (!hasViewAccess) { @@ -283,13 +274,14 @@ export module Controllers { FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); form.fields = fields; - if (!_.isEmpty(form)) { - return this.ajaxOk(req, res, null, form); - } else { - return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); - } + mergedForm = form; } } + if (!_.isEmpty(mergedForm)) { + return this.ajaxOk(req, res, null, mergedForm); + } else { + return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); + } } catch(error) { sails.log.error("Error getting form definition:"); sails.log.error(error); From 0d1352496a7fd940e789071c837de33159649608 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 21 Jun 2024 02:34:10 +0000 Subject: [PATCH 16/30] Update forms service unit test --- test/unit/services/FormsService.test.js | 49 ++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index b39c73a4e8..8454ffa886 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -9,9 +9,12 @@ describe('The FormsService', function () { var brand = BrandingService.getDefault(); var recordType = 'rdmp'; var formName = 'default-1.0-draft'; - RecordType.find().then(forms => {sails.log.verbose(`going to look for ${brand.id}_${formName}`);sails.log.verbose(forms);}); + RecordType.find().then(forms => { + sails.log.verbose(`going to look for ${brand.id}_${formName}`); + sails.log.verbose(forms); + }); console.log('brand.id '+brand.id+' recordType '+recordType); - FormsService.getForm(brand.id, recordType, true, true).subscribe(function(form) { + FormsService.getFormByStartingWorkflowStep(brand.id, recordType, true).subscribe(function(form) { expect(form).to.have.property('name', formName); done(); }) @@ -29,4 +32,46 @@ describe('The FormsService', function () { }) }); + it('should return the form based of a given record', function (done) { + let formName = 'default-1.0-draft'; + let record = { + metaMetadata: { + form: formName + } + }; + FormsService.getForm('', record, true).subscribe(form => { + expect(form).to.have.property('name', formName); + done(); + }) + }); + + it('should return the autogenerated form based of a given record', function (done) { + let formName = 'generated-view-only'; + let record = { + metaMetadata: { + form: formName + } + }; + FormsService.getForm('', record, true).subscribe(form => { + expect(form).to.have.property('name', formName); + done(); + }) + }); + + it('should return the form', function (done) { + let formName = 'default-1.0-draft'; + FormsService.getForm(formName, {}, true).subscribe(form => { + expect(form).to.have.property('name', formName); + done(); + }) + }); + + it('should return the autogenerated form', function (done) { + let formName = 'generated-view-only'; + FormsService.getForm(formName, {}, true).subscribe(form => { + expect(form).to.have.property('name', formName); + done(); + }) + }); + }); From 4f9a7b1087ae83e0a33947e522041f7752090f34 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Fri, 21 Jun 2024 07:08:37 +0000 Subject: [PATCH 17/30] Update references of renamed method getFormByStartingWorkflowStep and more promsify refactoring --- config/recordtype.js | 2 +- config/workflow.js | 19 +- test/unit/services/FormsService.test.js | 12 +- .../api/controllers/RecordController.ts | 36 +- typescript/api/services/FormsService.ts | 315 ++++++++---------- 5 files changed, 186 insertions(+), 198 deletions(-) diff --git a/config/recordtype.js b/config/recordtype.js index 76bee7a1e7..3f51f2190f 100644 --- a/config/recordtype.js +++ b/config/recordtype.js @@ -606,6 +606,6 @@ module.exports.recordtype = { hooks: { } }, "party": { - "packageType": "rdmp" + packageType: "party" } }; diff --git a/config/workflow.js b/config/workflow.js index fc2257f5e9..e8fbb48a53 100644 --- a/config/workflow.js +++ b/config/workflow.js @@ -295,25 +295,26 @@ module.exports.workflow = { editRoles: ['Admin', 'Librarians'] }, form: 'generated-view-only', + generatedView: { mainField: 'JOB_TITLE' }, dashboard: { table: { rowConfig: [ { - title: 'Record Title', - variable: 'metadata.title', - template: `<%= metadata.title %>`, + title: 'Party Name', + variable: 'metadata.GIVEN_NAME', + template: `<%= metadata.GIVEN_NAME %> <%= metadata.FAMILY_NAME %>`, initialSort: 'desc' }, { - title: 'header-ci', - variable: 'metadata.contributor_ci.text_full_name', - template: '<%= metadata.contributor_ci != undefined ? metadata.contributor_ci.text_full_name : "" %>', + title: 'Party Title', + variable: 'metadata.JOB_TITLE', + template: '<%= metadata.JOB_TITLE %>', initialSort: 'desc' }, { - title: 'header-data-manager', - variable: 'metadata.contributor_data_manager.text_full_name', - template: '<%= metadata.contributor_data_manager != undefined ? metadata.contributor_data_manager.text_full_name : "" %>', + title: 'Party Email', + variable: 'metadata.EMAIL', + template: '<%= metadata.EMAIL %>', initialSort: 'desc' }, { diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index 8454ffa886..96ab65c6ff 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -33,42 +33,46 @@ describe('The FormsService', function () { }); it('should return the form based of a given record', function (done) { + let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; let record = { metaMetadata: { form: formName } }; - FormsService.getForm('', record, true).subscribe(form => { + FormsService.getForm(brand, '', record, true).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) }); it('should return the autogenerated form based of a given record', function (done) { + let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; let record = { metaMetadata: { form: formName } }; - FormsService.getForm('', record, true).subscribe(form => { + FormsService.getForm(brand, '', record, true).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) }); it('should return the form', function (done) { + let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; - FormsService.getForm(formName, {}, true).subscribe(form => { + FormsService.getForm(brand, formName, {}, true).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) }); it('should return the autogenerated form', function (done) { + let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; - FormsService.getForm(formName, {}, true).subscribe(form => { + FormsService.getForm(brand, formName, {}, true).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 63bcacfbd5..ffd9b96ed5 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -150,7 +150,7 @@ export module Controllers { sails.log.debug('RECORD::APP: ' + appName); sails.log.debug('RECORD::APP formName: ' + extFormName); if (recordType != '' && extFormName == '') { - FormsService.getFormByStartingWorkflowStep(brand.id, recordType, true).subscribe(form => { + FormsService.getFormByStartingWorkflowStep(brand, recordType, true).subscribe(form => { if (form['customAngularApp'] != null) { appSelector = form['customAngularApp']['appSelector']; appName = form['customAngularApp']['appName']; @@ -236,52 +236,66 @@ export module Controllers { let mergedForm: any = {}; try { if (_.isEmpty(oid)) { - let form = await FormsService.getFormByStartingWorkflowStep(brand.id, recordType, editMode).toPromise(); + //find form to create a record + let form = await FormsService.getFormByStartingWorkflowStep(brand, recordType, editMode).toPromise(); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form for record type: ${recordType}`}); } let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, recordType, {}); form.fields = fields; mergedForm = form; + } else { + // defaults to retrive the form of the current workflow state... let currentRec = await this.recordsService.getMeta(oid); if (_.isEmpty(currentRec)) { return this.ajaxFail(req, res, null, {message: `Error, empty metadata for OID: ${oid}`}); } + + let form: any = {}; + if (editMode) { + //find form to edit a record let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); if (!hasEditAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('edit-error-no-permissions')}); } - let form = await FormsService.getForm(formParam, editMode, currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, currentRec).toPromise(); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } - let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); - form.fields = fields; - mergedForm = form; } else { + //find form to view a record let hasViewAccess = await this.hasViewAccess(brand, req.user, currentRec).toPromise(); if (!hasViewAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('view-error-no-permissions')}); } let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); - let form = await FormsService.getForm(formParam, editMode, currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, currentRec).toPromise(); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } - FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); - let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); - form.fields = fields; - mergedForm = form; + if(form.name == 'generated-view-only') { + //if autogenerated view only form then save json schema on the record + let schema = FormsService.inferSchemaFromMetadata(currentRec); + _.set(currentRec,'system.recordSchema',schema); + const user = req.user; + this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); + } } + + let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); + form.fields = fields; + mergedForm = form; } + if (!_.isEmpty(mergedForm)) { return this.ajaxOk(req, res, null, mergedForm); } else { return this.ajaxFail(req, res, null, {message: `Failed to get form with name:${recordType}`}); } + } catch(error) { sails.log.error("Error getting form definition:"); sails.log.error(error); diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 7f591a82e2..96f62205bd 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -20,12 +20,13 @@ import { Observable } from 'rxjs/Rx'; -import { FormModel, Services as services } from '@researchdatabox/redbox-core-types'; +import { BrandingModel, FormModel, Services as services } from '@researchdatabox/redbox-core-types'; import { Sails, Model } from "sails"; import { createSchema } from 'genson-js'; +import { config } from 'process'; declare var sails: Sails; declare var Form: Model; @@ -161,34 +162,25 @@ export module Services { }); } - public getForm = (formParam: string, editMode: boolean, currentRec: any): Observable => { + public getForm(branding: BrandingModel, formParam: string, editMode: boolean, currentRec: any): Observable { // allow client to set the form name to use const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; if(formName == 'generated-view-only') { - return Observable.of(this.generateFormFromSchema(currentRec)); + return this.generateFormFromSchema(branding, currentRec); } else { return this.getFormByName(formName, editMode); } - - - // if(formName == 'generated-view-only') { - // let schema = this.inferSchemaFromMetadata(currentRec); - // _.set(currentRec,'schema',schema); - // const user = req.user; - // this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); - // } - } - public getFormByStartingWorkflowStep = (branding, recordType, editMode): Observable => { + public getFormByStartingWorkflowStep(branding: BrandingModel, recordType: string, editMode: boolean): Observable { let starting = true; return super.getObservable(RecordType.findOne({ - key: branding + "_" + recordType + key: branding.id + "_" + recordType })) .flatMap(recordType => { @@ -218,178 +210,155 @@ export module Services { public inferSchemaFromMetadata(record: any): any { const schema = createSchema(record.metadata); - // sails.log.verbose(schema); return schema; } - public generateFormFromSchema(record: any): FormModel { + public generateFormFromSchema(branding: BrandingModel, record: any): Observable { let recordType = record.metaMetadata.type; - let form: FormModel; - - let schema = this.inferSchemaFromMetadata(record); - - let fieldKeys = _.keys(schema.properties); - - let buttonsList = [ - // { - // class: "AnchorOrButton", - // viewOnly: true, - // definition: { - // label: '@dmp-edit-record-link', - // value: '/@branding/@portal/record/edit/@oid', - // cssClasses: 'btn btn-large btn-info', - // showPencil: true, - // controlType: 'anchor' - // }, - // variableSubstitutionFields: ['value'] - // }, - { - class: 'AnchorOrButton', - roles: ['Admin', 'Librarians'], - viewOnly: true, - definition: { - label: '@view-record-audit-link', - value: '/@branding/@portal/record/viewAudit/@oid', - cssClasses: 'btn btn-large btn-info margin-15', - controlType: 'anchor' - }, - variableSubstitutionFields: ['value'] - }, - { - class: 'SaveButton', - viewOnly: true, - roles: ['Admin', 'Librarians'], - definition: { - name: 'confirmDelete', - label: 'Delete this record', - closeOnSave: true, - redirectLocation: '/@branding/@portal/dashboard/'+recordType, - cssClasses: 'btn-danger', - confirmationMessage: '@dataPublication-confirmDelete', - confirmationTitle: '@dataPublication-confirmDeleteTitle', - cancelButtonMessage: '@dataPublication-cancelButtonMessage', - confirmButtonMessage: '@dataPublication-confirmButtonMessage', - isDelete: true, - isSubmissionButton: true - }, - variableSubstitutionFields: ['redirectLocation'] - } - ]; - - let textFieldTemplate = { - class: 'TextField', - viewOnly: true, - definition: { - name: '', - label: '', - help: '', - type: 'text' - } - }; + let starting = true; - let objectFieldHeadingTemplate = { - class: 'Container', - compClass: 'TextBlockComponent', - definition: { - value: '', - type: 'h3' - } - }; + return super.getObservable(RecordType.findOne({ + key: branding.id + "_" + recordType + })) + .flatMap(recordType => { - let fieldList = [ - { - class: 'Container', - compClass: 'TextBlockComponent', - viewOnly: true, - definition: { - name: 'title', - type: 'h1' - } - }, - { - class: 'Container', - compClass: 'GenericGroupComponent', - definition: { - cssClasses: "form-inline", - fields: buttonsList - } - } + return super.getObservable(WorkflowStep.findOne({ + recordType: recordType.id, + starting: starting + })); + }).flatMap(workflowStep => { - // { - // class: 'TextArea', - // viewOnly: true, - // definition: { - // name: 'description', - // label: '@dmpt-description' - // } - // }, - - - // { - // class: "Container", - // definition: { - // id: "project", - // label: "project-tab", - // fields: [ - // ] - // } - // } - - - // { - // class: "TabOrAccordionContainer", - // compClass: "TabOrAccordionContainerComponent", - // definition: { - // id: "mainTab", - // accContainerClass: "view-accordion", - // expandAccordionsOnOpen: true, - // fields: [ - // ] - // } - // } - ]; - - for(let fieldKey of fieldKeys) { - if(schema.properties[fieldKey].type == 'string') { - let textField = _.cloneDeep(textFieldTemplate); - _.set(textField.definition,'name',fieldKey); - _.set(textField.definition,'label',fieldKey); - fieldList.push(textField); - } else if(schema.properties[fieldKey].type == 'object') { - - let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); - _.set(objectFieldHeading.definition, 'value', fieldKey); - fieldList.push(objectFieldHeading); - - let objectFieldKeys = _.keys(schema.properties[fieldKey].properties); - for(let objectFieldKey of objectFieldKeys) { - if(schema.properties[fieldKey].properties[objectFieldKey].type == 'string') { - let textField = _.cloneDeep(textFieldTemplate); - _.set(textField.definition,'name',objectFieldKey); - _.set(textField.definition,'label',objectFieldKey); - fieldList.push(textField); + if (workflowStep.starting == true) { + + let form: FormModel; + + let schema = this.inferSchemaFromMetadata(record); + + let fieldKeys = _.keys(schema.properties); + + let buttonsList = [ + { + class: 'AnchorOrButton', + roles: ['Admin', 'Librarians'], + viewOnly: true, + definition: { + label: '@view-record-audit-link', + value: '/@branding/@portal/record/viewAudit/@oid', + cssClasses: 'btn btn-large btn-info margin-15', + controlType: 'anchor' + }, + variableSubstitutionFields: ['value'] + }, + { + class: 'SaveButton', + viewOnly: true, + roles: ['Admin', 'Librarians'], + definition: { + name: 'confirmDelete', + label: 'Delete this record', + closeOnSave: true, + redirectLocation: '/@branding/@portal/dashboard/'+recordType, + cssClasses: 'btn-danger', + confirmationMessage: '@dataPublication-confirmDelete', + confirmationTitle: '@dataPublication-confirmDeleteTitle', + cancelButtonMessage: '@dataPublication-cancelButtonMessage', + confirmButtonMessage: '@dataPublication-confirmButtonMessage', + isDelete: true, + isSubmissionButton: true + }, + variableSubstitutionFields: ['redirectLocation'] + } + ]; + + let textFieldTemplate = { + class: 'TextField', + viewOnly: true, + definition: { + name: '', + label: '', + help: '', + type: 'text' + } + }; + + let objectFieldHeadingTemplate = { + class: 'Container', + compClass: 'TextBlockComponent', + definition: { + value: '', + type: 'h3' + } + }; + + let mainTitleFieldName = _.get(workflowStep,'config.generatedView.mainField','title'); + + let fieldList = [ + { + class: 'Container', + compClass: 'TextBlockComponent', + viewOnly: true, + definition: { + name: mainTitleFieldName, + type: 'h1' + } + }, + { + class: 'Container', + compClass: 'GenericGroupComponent', + definition: { + cssClasses: "form-inline", + fields: buttonsList + } + } + ]; + + for(let fieldKey of fieldKeys) { + if(schema.properties[fieldKey].type == 'string') { + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey); + _.set(textField.definition,'label',fieldKey); + fieldList.push(textField); + } else if(schema.properties[fieldKey].type == 'object') { + + let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); + _.set(objectFieldHeading.definition, 'value', fieldKey); + fieldList.push(objectFieldHeading); + + let objectFieldKeys = _.keys(schema.properties[fieldKey].properties); + for(let objectFieldKey of objectFieldKeys) { + if(schema.properties[fieldKey].properties[objectFieldKey].type == 'string') { + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey+'.'+objectFieldKey); + _.set(textField.definition,'label',objectFieldKey); + fieldList.push(textField); + } + } + } } + + let formObject = { + name: 'generated-view-only', + type: recordType, + skipValidationOnSave: false, + editCssClasses: 'row col-md-12', + viewCssClasses: 'row col-md-offset-1 col-md-10', + messages: {}, + attachmentFields: [], + fields: fieldList + }; + + form = formObject as any; + + return Observable.of(form); } - } - } - - let formObject = { - name: 'generated-view-only', - type: recordType, - skipValidationOnSave: false, - editCssClasses: 'row col-md-12', - viewCssClasses: 'row col-md-offset-1 col-md-10', - messages: {}, - attachmentFields: [], - fields: fieldList - }; - - form = formObject as any; - return form; + return Observable.of(null); + }).filter(result => result !== null).last(); } + + protected setFormEditMode(fields, editMode): void{ _.remove(fields, field => { From 7f01055cd00d393abeac733a39c4bfd76e876dae Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Mon, 24 Jun 2024 06:31:44 +0000 Subject: [PATCH 18/30] Add support for array fields in autogenerated forms --- config/recordtype.js | 36 +++++- config/workflow.js | 1 - test/unit/services/FormsService.test.js | 8 +- .../api/controllers/RecordController.ts | 14 +-- typescript/api/services/FormsService.ts | 107 ++++++++++++++++-- typescript/api/services/RecordsService.ts | 3 +- 6 files changed, 140 insertions(+), 29 deletions(-) diff --git a/config/recordtype.js b/config/recordtype.js index 3f51f2190f..e39480306c 100644 --- a/config/recordtype.js +++ b/config/recordtype.js @@ -606,6 +606,40 @@ module.exports.recordtype = { hooks: { } }, "party": { - packageType: "party" + packageType: "party", + hooks: { + onCreate: { + pre: [ + { + function: 'sails.services.rdmpservice.runTemplates', + options: { + parseObject: false, + templates: [ + { + field: "metadata.title", + template: "<%= _.get(record, 'metadata.JOB_TITLE') %>" + } + ] + } + } + ] + }, + onUpdate: { + pre: [ + { + function: 'sails.services.rdmpservice.runTemplates', + options: { + parseObject: false, + templates: [ + { + field: "metadata.title", + template: "<%= _.get(record, 'metadata.JOB_TITLE') %>" + } + ] + } + } + ] + } + } } }; diff --git a/config/workflow.js b/config/workflow.js index e8fbb48a53..b4f9dbae79 100644 --- a/config/workflow.js +++ b/config/workflow.js @@ -295,7 +295,6 @@ module.exports.workflow = { editRoles: ['Admin', 'Librarians'] }, form: 'generated-view-only', - generatedView: { mainField: 'JOB_TITLE' }, dashboard: { table: { rowConfig: [ diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index 96ab65c6ff..22f8ccd704 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -40,7 +40,7 @@ describe('The FormsService', function () { form: formName } }; - FormsService.getForm(brand, '', record, true).subscribe(form => { + FormsService.getForm(brand, '', true, '', record).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) @@ -54,7 +54,7 @@ describe('The FormsService', function () { form: formName } }; - FormsService.getForm(brand, '', record, true).subscribe(form => { + FormsService.getForm(brand, '', true, '', record).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) @@ -63,7 +63,7 @@ describe('The FormsService', function () { it('should return the form', function (done) { let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; - FormsService.getForm(brand, formName, {}, true).subscribe(form => { + FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) @@ -72,7 +72,7 @@ describe('The FormsService', function () { it('should return the autogenerated form', function (done) { let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; - FormsService.getForm(brand, formName, {}, true).subscribe(form => { + FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { expect(form).to.have.property('name', formName); done(); }) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index ffd9b96ed5..45b1d4c2bf 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -261,7 +261,7 @@ export module Controllers { if (!hasEditAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('edit-error-no-permissions')}); } - form = await FormsService.getForm(brand, formParam, editMode, currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, '', currentRec).toPromise(); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } @@ -271,18 +271,12 @@ export module Controllers { if (!hasViewAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('view-error-no-permissions')}); } - let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); - form = await FormsService.getForm(brand, formParam, editMode, currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, '', currentRec).toPromise(); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } - if(form.name == 'generated-view-only') { - //if autogenerated view only form then save json schema on the record - let schema = FormsService.inferSchemaFromMetadata(currentRec); - _.set(currentRec,'system.recordSchema',schema); - const user = req.user; - this.recordsService.updateMeta(brand, oid, currentRec, user, false, false); - } + let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); + FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); } let fields = await this.mergeFields(req, res, form.fields, form.requiredFieldIndicator, currentRec.metaMetadata.type, currentRec); diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 96f62205bd..c8ed03ef51 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -162,13 +162,13 @@ export module Services { }); } - public getForm(branding: BrandingModel, formParam: string, editMode: boolean, currentRec: any): Observable { + public getForm(branding: BrandingModel, formParam: string, editMode: boolean, recordType: string, currentRec: any): Observable { // allow client to set the form name to use const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; if(formName == 'generated-view-only') { - return this.generateFormFromSchema(branding, currentRec); + return this.generateFormFromSchema(branding, recordType, currentRec); } else { return this.getFormByName(formName, editMode); @@ -213,9 +213,14 @@ export module Services { return schema; } - public generateFormFromSchema(branding: BrandingModel, record: any): Observable { + public generateFormFromSchema(branding: BrandingModel, recordType: string, record: any): Observable { - let recordType = record.metaMetadata.type; + if(recordType == '') { + recordType = _.get(record,'metaMetadata.type',''); + if(recordType == '') { + return Observable.of(null); + } + } let starting = true; @@ -282,6 +287,40 @@ export module Services { type: 'text' } }; + + let groupComponentTemplate = { + class: 'Container', + compClass: 'GenericGroupComponent', + definition: { + name: '', + cssClasses: 'form-inline', + fields: [] + } + }; + + let groupTextFieldTemplate = { + class: 'TextField', + definition: { + name: '', + label: '', + type: 'text', + groupName: '', + groupClasses: 'width-30', + cssClasses : "width-80 form-control" + } + }; + + let repeatableGroupComponentTemplate = { + class: 'RepeatableContainer', + compClass: 'RepeatableGroupComponent', + definition: { + name: '', + label: '', + help: '', + forceClone: ['fields'], + fields: [] + } + }; let objectFieldHeadingTemplate = { class: 'Container', @@ -292,7 +331,7 @@ export module Services { } }; - let mainTitleFieldName = _.get(workflowStep,'config.generatedView.mainField','title'); + let mainTitleFieldName = 'title'; let fieldList = [ { @@ -315,26 +354,69 @@ export module Services { ]; for(let fieldKey of fieldKeys) { + if(schema.properties[fieldKey].type == 'string') { + let textField = _.cloneDeep(textFieldTemplate); _.set(textField.definition,'name',fieldKey); _.set(textField.definition,'label',fieldKey); fieldList.push(textField); + + } if(schema.properties[fieldKey].type == 'array') { + + if(schema.properties[fieldKey].items.type == 'string') { + + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey); + _.set(textField.definition,'label',fieldKey); + fieldList.push(textField); + + } else if(schema.properties[fieldKey].items.type == 'object') { + + let objectFieldKeys = _.keys(schema.properties[fieldKey].items.properties); + let repeatableGroupField = _.cloneDeep(repeatableGroupComponentTemplate); + let groupField = _.cloneDeep(groupComponentTemplate); + let groupFieldList = []; + for(let objectFieldKey of objectFieldKeys) { + if(schema.properties[fieldKey].items.properties[objectFieldKey].type == 'string') { + let textField = _.cloneDeep(groupTextFieldTemplate); + _.set(textField.definition,'name',objectFieldKey); + _.set(textField.definition,'label',objectFieldKey); + _.set(textField.definition,'groupName','item'); + groupFieldList.push(textField); + } + } + + _.set(groupField.definition,'name','item'); + _.set(groupField.definition,'fields',groupFieldList); + _.set(repeatableGroupField.definition,'name',fieldKey); + _.set(repeatableGroupField.definition,'label',fieldKey); + _.set(repeatableGroupField.definition,'fields',[groupField]); + fieldList.push(repeatableGroupField); + } + } else if(schema.properties[fieldKey].type == 'object') { - let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); - _.set(objectFieldHeading.definition, 'value', fieldKey); - fieldList.push(objectFieldHeading); - let objectFieldKeys = _.keys(schema.properties[fieldKey].properties); + let groupField = _.cloneDeep(groupComponentTemplate); + let groupFieldList = []; for(let objectFieldKey of objectFieldKeys) { if(schema.properties[fieldKey].properties[objectFieldKey].type == 'string') { - let textField = _.cloneDeep(textFieldTemplate); - _.set(textField.definition,'name',fieldKey+'.'+objectFieldKey); + let textField = _.cloneDeep(groupTextFieldTemplate); + _.set(textField.definition,'name',objectFieldKey); _.set(textField.definition,'label',objectFieldKey); - fieldList.push(textField); + _.set(textField.definition,'groupName',fieldKey); + groupFieldList.push(textField); } } + + let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); + _.set(objectFieldHeading.definition, 'value', fieldKey); + fieldList.push(objectFieldHeading); + + _.set(groupField.definition,'name',fieldKey); + _.set(groupField.definition,'fields',groupFieldList); + fieldList.push(groupField); } } @@ -355,6 +437,7 @@ export module Services { } return Observable.of(null); + }).filter(result => result !== null).last(); } diff --git a/typescript/api/services/RecordsService.ts b/typescript/api/services/RecordsService.ts index a2d950b569..8a82d44855 100644 --- a/typescript/api/services/RecordsService.ts +++ b/typescript/api/services/RecordsService.ts @@ -175,7 +175,8 @@ export module Services { let wfStep = await WorkflowStepsService.getFirst(recordType).toPromise(); let formName = _.get(wfStep,'config.form'); - let form = await FormsService.getFormByName(formName, true).toPromise(); + + let form = await FormsService.getForm(brand, formName, true, recordType.name, record).toPromise(); let metaMetadata = this.initRecordMetaMetadata(brand.id, user.username, recordType, wfStep, form, moment().format()); _.set(record,'metaMetadata',metaMetadata); From 290704b865833780918007e3ad3ccf9f7253b385 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Mon, 24 Jun 2024 07:17:33 +0000 Subject: [PATCH 19/30] Fix dashboard component filter input box showing in empty workflow steps --- .../dashboard/src/app/dashboard.component.html | 2 +- .../dashboard/src/app/dashboard.component.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 1092ed3fcd..b4895f8d68 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -9,7 +9,7 @@

{{ 'dashboard-heading' | i18next: {stage: workflowStep.config.workflow.stage

{{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }}

- +
diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 9e42dee2b7..409c6c9f06 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -34,7 +34,7 @@ export class DashboardComponent extends BaseComponent { defaultFilterField: FilterField = { name: this.filterFieldName, path: this.filterFieldPath }; filterSearchString: string = ''; hideWorkflowStepTitle: boolean = false; - isFilterSearchDisplayed: boolean = false; + isFilterSearchDisplayed: any = {}; isSearching: boolean = false; defaultRowConfig = [ @@ -803,11 +803,20 @@ export class DashboardComponent extends BaseComponent { return this.getFilters('text'); } + public getFilterSearchDisplayed(step: string): boolean { + let filterDisplayed = _.get(this.isFilterSearchDisplayed,step,''); + if(filterDisplayed == 'filterDisplayed') { + return true; + } else { + return false; + } + } + public async filterChanged(step: string) { if (this.dashboardTypeSelected == 'standard') { this.isSearching = true; - this.isFilterSearchDisplayed = true; + this.isFilterSearchDisplayed[step] = 'filterDisplayed'; let sortDetails = this.sortMap[step]; let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); From 7db7efc035de8b670024be0bae6fdab2fe6cb82d Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Mon, 24 Jun 2024 08:45:01 +0000 Subject: [PATCH 20/30] Reinstate workflow steps populated for all dashboard types --- .../dashboard/src/app/dashboard.component.html | 1 - .../researchdatabox/dashboard/src/app/dashboard.component.ts | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index b4895f8d68..6d4f224f35 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -1,7 +1,6 @@
-

{{ 'dashboard-heading' | i18next: {stage: workflowStep.config.workflow.stageLabel, recordTypeName: typeLabel} }}

diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 409c6c9f06..5c3620821d 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -212,11 +212,12 @@ export class DashboardComponent extends BaseComponent { this.initSortConfig(step); + this.workflowSteps.push(step); + let packageType = ''; let stepName = ''; let evaluateStepName = ''; if (this.dashboardTypeSelected == 'consolidated') { - this.workflowSteps.push(step); packageType = ''; stepName = ''; evaluateStepName = _get(step, 'name'); @@ -803,7 +804,7 @@ export class DashboardComponent extends BaseComponent { return this.getFilters('text'); } - public getFilterSearchDisplayed(step: string): boolean { + public getFilterSearchDisplayed(step: any): boolean { let filterDisplayed = _.get(this.isFilterSearchDisplayed,step,''); if(filterDisplayed == 'filterDisplayed') { return true; From 97e16d1fd2ff254a0563b1561bb80450d80d6a52 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 25 Jun 2024 00:17:57 +0000 Subject: [PATCH 21/30] Improve dashboard app habdling of filter input box --- .../src/app/dashboard.component.html | 8 ++--- .../dashboard/src/app/dashboard.component.ts | 36 +++++++++++++------ test/unit/services/FormsService.test.js | 16 +++++---- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 6d4f224f35..0b6f77ab9b 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -13,12 +13,12 @@

{{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }} - - + +

diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts index 5c3620821d..e89d20d664 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -32,10 +32,10 @@ export class DashboardComponent extends BaseComponent { filterFieldName: string = 'Title'; filterFieldPath: string = 'metadata.title'; defaultFilterField: FilterField = { name: this.filterFieldName, path: this.filterFieldPath }; - filterSearchString: string = ''; + filterSearchString: any = {}; hideWorkflowStepTitle: boolean = false; isFilterSearchDisplayed: any = {}; - isSearching: boolean = false; + isSearching: any = {}; defaultRowConfig = [ { @@ -697,7 +697,7 @@ export class DashboardComponent extends BaseComponent { if (this.dashboardTypeSelected == 'workspace') { stagedRecords = await this.recordService.getRecords('', '', 1, this.dashboardTypeSelected, sortString); } else { - stagedRecords = await this.recordService.getRecords(this.recordType, data.step, 1, '', sortString,this.filterFieldPath,this.filterSearchString); + stagedRecords = await this.recordService.getRecords(this.recordType, data.step, 1, '', sortString,this.filterFieldPath,this.getFilterSearchString(data.step)); } let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, data.step, stagedRecords); @@ -723,7 +723,7 @@ export class DashboardComponent extends BaseComponent { let sortDetails = this.sortMap[step]; if (this.dashboardTypeSelected == 'standard') { - let stagedRecords = await this.recordService.getRecords(this.recordType, step, event.page, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); + let stagedRecords = await this.recordService.getRecords(this.recordType, step, event.page, '', this.getSortString(sortDetails),this.filterFieldPath,this.getFilterSearchString(step)); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; } else if (this.dashboardTypeSelected == 'workspace') { @@ -813,29 +813,43 @@ export class DashboardComponent extends BaseComponent { } } + public getIsSearching(step: any): boolean { + let searching = _.get(this.isSearching,step,''); + if(searching == 'searching') { + return true; + } else { + return false; + } + } + + public getFilterSearchString(step: any): string { + let filterString = _.get(this.filterSearchString,step,''); + return filterString; + } + public async filterChanged(step: string) { if (this.dashboardTypeSelected == 'standard') { - this.isSearching = true; + this.isSearching[step] = 'searching'; this.isFilterSearchDisplayed[step] = 'filterDisplayed'; let sortDetails = this.sortMap[step]; - let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); + let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.getFilterSearchString(step)); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; - this.isSearching = false; + this.isSearching[step] = ''; } } public async resetFilterAndSearch(step: string, e: any) { if (this.dashboardTypeSelected == 'standard') { this.setFilterField(this.getFirstTextFilter(), e); - this.isSearching = true; + this.isSearching[step] = 'searching'; let sortDetails = this.sortMap[step]; - this.filterSearchString = ''; - let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.filterSearchString); + this.filterSearchString[step] = ''; + let stagedRecords = await this.recordService.getRecords(this.recordType, step, 1, '', this.getSortString(sortDetails),this.filterFieldPath,this.getFilterSearchString(step)); let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); this.records[step] = planTable; - this.isSearching = false; + this.isSearching[step] = ''; } } diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index 22f8ccd704..a0bfeb1800 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -10,11 +10,11 @@ describe('The FormsService', function () { var recordType = 'rdmp'; var formName = 'default-1.0-draft'; RecordType.find().then(forms => { - sails.log.verbose(`going to look for ${brand.id}_${formName}`); + sails.log.verbose(`going to look for ${brand.id}_${recordType}`); sails.log.verbose(forms); }); console.log('brand.id '+brand.id+' recordType '+recordType); - FormsService.getFormByStartingWorkflowStep(brand.id, recordType, true).subscribe(function(form) { + FormsService.getFormByStartingWorkflowStep(brand, recordType, true).subscribe(function(form) { expect(form).to.have.property('name', formName); done(); }) @@ -41,7 +41,8 @@ describe('The FormsService', function () { } }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { - expect(form).to.have.property('name', formName); + // expect(form).to.have.property('name', formName); + expect(true).to.eq(true); done(); }) }); @@ -55,7 +56,8 @@ describe('The FormsService', function () { } }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { - expect(form).to.have.property('name', formName); + // expect(form).to.have.property('name', formName); + expect(true).to.eq(true); done(); }) }); @@ -64,7 +66,8 @@ describe('The FormsService', function () { let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { - expect(form).to.have.property('name', formName); + // expect(form).to.have.property('name', formName); + expect(true).to.eq(true); done(); }) }); @@ -73,7 +76,8 @@ describe('The FormsService', function () { let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { - expect(form).to.have.property('name', formName); + // expect(form).to.have.property('name', formName); + expect(true).to.eq(true); done(); }) }); From 91aca99c2cc2c0072c177d5bcffbfcadd90f9e49 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 25 Jun 2024 02:10:35 +0000 Subject: [PATCH 22/30] Fix filter input not setting ngmodel properly --- .../researchdatabox/dashboard/src/app/dashboard.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 0b6f77ab9b..12fde4e599 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -17,7 +17,7 @@

{{ 'dashboard-heading-one-step' | i18next: {recordTypeName: typeLabel} }}
  • {{ 'record-search-reset' | i18next }}
  • - +

    From 0e55a36b110d5b93807b36a4033c6d924497eeba Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 25 Jun 2024 02:23:38 +0000 Subject: [PATCH 23/30] Fix mocha unit tests for new form service method getform --- test/unit/services/FormsService.test.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index a0bfeb1800..f98cf52ba4 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -37,12 +37,13 @@ describe('The FormsService', function () { let formName = 'default-1.0-draft'; let record = { metaMetadata: { - form: formName + form: formName, + type: 'rdmp' } }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { - // expect(form).to.have.property('name', formName); - expect(true).to.eq(true); + expect(form).to.have.property('name', formName); + // expect(true).to.eq(true); done(); }) }); @@ -52,7 +53,8 @@ describe('The FormsService', function () { let formName = 'generated-view-only'; let record = { metaMetadata: { - form: formName + form: formName, + type: 'rdmp' } }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { @@ -65,9 +67,9 @@ describe('The FormsService', function () { it('should return the form', function (done) { let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; - FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { - // expect(form).to.have.property('name', formName); - expect(true).to.eq(true); + FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { + expect(form).to.have.property('name', formName); + // expect(true).to.eq(true); done(); }) }); @@ -75,9 +77,9 @@ describe('The FormsService', function () { it('should return the autogenerated form', function (done) { let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; - FormsService.getForm(brand, formName, true, '', {}).subscribe(form => { - // expect(form).to.have.property('name', formName); - expect(true).to.eq(true); + FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { + expect(form).to.have.property('name', formName); + //expect(true).to.eq(true); done(); }) }); From 028f0837468b9bfa5a767dc44014526d140a84a2 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 25 Jun 2024 03:41:57 +0000 Subject: [PATCH 24/30] Fix one more mocha unit test in forms service --- test/unit/services/FormsService.test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index f98cf52ba4..9c3d27b7a5 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -43,7 +43,6 @@ describe('The FormsService', function () { }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { expect(form).to.have.property('name', formName); - // expect(true).to.eq(true); done(); }) }); @@ -58,8 +57,7 @@ describe('The FormsService', function () { } }; FormsService.getForm(brand, '', true, '', record).subscribe(form => { - // expect(form).to.have.property('name', formName); - expect(true).to.eq(true); + expect(form).to.have.property('name', formName); done(); }) }); @@ -69,7 +67,6 @@ describe('The FormsService', function () { let formName = 'default-1.0-draft'; FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { expect(form).to.have.property('name', formName); - // expect(true).to.eq(true); done(); }) }); @@ -79,7 +76,6 @@ describe('The FormsService', function () { let formName = 'generated-view-only'; FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { expect(form).to.have.property('name', formName); - //expect(true).to.eq(true); done(); }) }); From 3ddae5a01e17bef23a7d0d91f4341eb22a58f41c Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Tue, 25 Jun 2024 07:22:55 +0000 Subject: [PATCH 25/30] Make generate form from schema method more robust in forms service --- typescript/api/services/FormsService.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index c8ed03ef51..d47fdb7517 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -355,30 +355,34 @@ export module Services { for(let fieldKey of fieldKeys) { - if(schema.properties[fieldKey].type == 'string') { + let schemaProperty = schema.properties[fieldKey]; + + if(_.get(schemaProperty,'type','') == 'string') { let textField = _.cloneDeep(textFieldTemplate); _.set(textField.definition,'name',fieldKey); _.set(textField.definition,'label',fieldKey); fieldList.push(textField); - } if(schema.properties[fieldKey].type == 'array') { + } if(_.get(schemaProperty,'type','') == 'array') { - if(schema.properties[fieldKey].items.type == 'string') { + if(_.get(schemaProperty,'items.type','') == 'string') { let textField = _.cloneDeep(textFieldTemplate); _.set(textField.definition,'name',fieldKey); _.set(textField.definition,'label',fieldKey); fieldList.push(textField); - } else if(schema.properties[fieldKey].items.type == 'object') { + } else if(_.get(schemaProperty,'items.type','') == 'object') { - let objectFieldKeys = _.keys(schema.properties[fieldKey].items.properties); + let objectFieldKeys = _.keys(schemaProperty.items.properties); let repeatableGroupField = _.cloneDeep(repeatableGroupComponentTemplate); let groupField = _.cloneDeep(groupComponentTemplate); let groupFieldList = []; + for(let objectFieldKey of objectFieldKeys) { - if(schema.properties[fieldKey].items.properties[objectFieldKey].type == 'string') { + let innerProperty = schemaProperty.items.properties[objectFieldKey]; + if(_.get(innerProperty,'type','') == 'string') { let textField = _.cloneDeep(groupTextFieldTemplate); _.set(textField.definition,'name',objectFieldKey); _.set(textField.definition,'label',objectFieldKey); @@ -395,13 +399,15 @@ export module Services { fieldList.push(repeatableGroupField); } - } else if(schema.properties[fieldKey].type == 'object') { + } else if(_.get(schemaProperty,'type','') == 'object') { - let objectFieldKeys = _.keys(schema.properties[fieldKey].properties); + let objectFieldKeys = _.keys(schemaProperty.properties); let groupField = _.cloneDeep(groupComponentTemplate); let groupFieldList = []; + for(let objectFieldKey of objectFieldKeys) { - if(schema.properties[fieldKey].properties[objectFieldKey].type == 'string') { + let innerProperty = schemaProperty.properties[objectFieldKey]; + if(_.get(innerProperty,'type','') == 'string') { let textField = _.cloneDeep(groupTextFieldTemplate); _.set(textField.definition,'name',objectFieldKey); _.set(textField.definition,'label',objectFieldKey); From f5b3150d9c42dccb548697289ac3035a722a9f38 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Wed, 26 Jun 2024 05:32:51 +0000 Subject: [PATCH 26/30] Add basic metadata comparison method before update to legacy harvest method in api record controller --- .../webservice/RecordController.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/typescript/api/controllers/webservice/RecordController.ts b/typescript/api/controllers/webservice/RecordController.ts index 0263ff28d7..1775d7778f 100644 --- a/typescript/api/controllers/webservice/RecordController.ts +++ b/typescript/api/controllers/webservice/RecordController.ts @@ -1080,6 +1080,19 @@ export module Controllers { return this.apiFailWrapper(req, res, 400, null, null, "Invalid request"); } + private isMetadataEqual(meta1:any, meta2:any): boolean { + + let keys = _.keys(meta1); + + for(let key of keys) { + if(_.get(meta1,key,'') != _.get(meta2,key,'')) { + return false; + } + } + + return true; + } + public async legacyHarvest(req, res) { const brand:BrandingModel = BrandingService.getBrand(req.session.branding); @@ -1108,7 +1121,21 @@ export module Controllers { recordResponses.push(await this.createHarvestRecord(brand, recordTypeModel, record['metadata']['data'], harvestId, 'update', user)); } else { let oid = existingRecord[0].redboxOid; - recordResponses.push(await this.updateHarvestRecord(brand, recordTypeModel, 'update', record['metadata']['data'], oid, harvestId, user)); + let oldMetadata = existingRecord[0].metadata; + let newMetadata = record['metadata']['data']; + let response = { + details: '', + message: `skip update of harvestId ${harvestId} oid ${oid} metadata sent is equal to metadata in existing record`, + harvestId: harvestId, + oid: oid, + status: true + }; + if(this.isMetadataEqual(newMetadata,oldMetadata)) { + recordResponses.push(response); + } else { + response = await this.updateHarvestRecord(brand, recordTypeModel, 'update', newMetadata, oid, harvestId, user); + recordResponses.push(response); + } } } } From b088fddbfc5d636d046ec5def1d1d82fb3073403 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Wed, 26 Jun 2024 05:49:42 +0000 Subject: [PATCH 27/30] Add agenda queue indexes on init --- typescript/api/services/AgendaQueueService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/typescript/api/services/AgendaQueueService.ts b/typescript/api/services/AgendaQueueService.ts index ec7efb7b6b..4ad82b483e 100644 --- a/typescript/api/services/AgendaQueueService.ts +++ b/typescript/api/services/AgendaQueueService.ts @@ -87,8 +87,12 @@ export module Services { _.forOwn(sails.config.agendaQueue.options, (optionVal:any, optionName:string) => { this.setOptionIfDefined(agendaOpts, optionName, optionVal); }); + const dbManager = User.getDatastore().manager; + const collectionName = _.get(agendaOpts, 'collection', 'agendaJobs'); + await dbManager.collection(collectionName).createIndex({ name: 1, disabled: 1, lockedAt: 1, nextRunAt: 1 }) + await dbManager.collection(collectionName).createIndex({ name: -1, disabled: -1, lockedAt: -1, nextRunAt: -1}) if (_.isEmpty(_.get(agendaOpts, 'db.address'))) { - agendaOpts['mongo'] = User.getDatastore().manager; + agendaOpts['mongo'] = dbManager; } this.agenda = new Agenda(agendaOpts); this.defineJobs(sails.config.agendaQueue.jobs); From 7a8aa43fc8ccca6176bd21e1a462dabf7823b59d Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Wed, 26 Jun 2024 07:09:28 +0000 Subject: [PATCH 28/30] Move indexes creation after agenda start on init method in agenda queue service --- typescript/api/services/AgendaQueueService.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typescript/api/services/AgendaQueueService.ts b/typescript/api/services/AgendaQueueService.ts index 4ad82b483e..241f53a120 100644 --- a/typescript/api/services/AgendaQueueService.ts +++ b/typescript/api/services/AgendaQueueService.ts @@ -88,9 +88,6 @@ export module Services { this.setOptionIfDefined(agendaOpts, optionName, optionVal); }); const dbManager = User.getDatastore().manager; - const collectionName = _.get(agendaOpts, 'collection', 'agendaJobs'); - await dbManager.collection(collectionName).createIndex({ name: 1, disabled: 1, lockedAt: 1, nextRunAt: 1 }) - await dbManager.collection(collectionName).createIndex({ name: -1, disabled: -1, lockedAt: -1, nextRunAt: -1}) if (_.isEmpty(_.get(agendaOpts, 'db.address'))) { agendaOpts['mongo'] = dbManager; } @@ -115,6 +112,12 @@ export module Services { sails.log.error(err); }); await this.agenda.start(); + + //Create indexes after agenda start + const collectionName = _.get(agendaOpts, 'collection', 'agendaJobs'); + await dbManager.collection(collectionName).createIndex({ name: 1, disabled: 1, lockedAt: 1, nextRunAt: 1 }); + await dbManager.collection(collectionName).createIndex({ name: -1, disabled: -1, lockedAt: -1, nextRunAt: -1}); + // check for in-line job schedule _.each(sails.config.agendaQueue.jobs, (job) => { if (!_.isEmpty(job.schedule)) { From 8e31a5f4a36e2dbd3a9f565e202c8a9cef48ee1b Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Wed, 26 Jun 2024 07:20:54 +0000 Subject: [PATCH 29/30] Improve forms service unit tests --- test/unit/services/FormsService.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index 9c3d27b7a5..e00fb85a4a 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -20,14 +20,13 @@ describe('The FormsService', function () { }) }); - it('should get default-1.0-draft form', function (done) { + it('should get dataPublication-1.0-embargoed form', function (done) { var formName = 'dataPublication-1.0-embargoed'; - FormsService.getFormByName( formName, true).subscribe(function(form) { + FormsService.getFormByName(formName, true).subscribe(function(form) { console.log(form) - // expect(form).to.have.property('name', formName); - expect(true).to.eq(true) + expect(form).to.have.property('name', formName); done(); }) }); From 88812dc9b056d4f538e92240fcf4b8c3b862ffb7 Mon Sep 17 00:00:00 2001 From: "alejandro.bulgaris@qcif.edu.au" Date: Thu, 27 Jun 2024 03:05:43 +0000 Subject: [PATCH 30/30] Refactor new get form methond in forms service to be promise based and fix all references --- test/unit/services/FormsService.test.js | 8 +- .../api/controllers/RecordController.ts | 4 +- .../webservice/RecordController.ts | 2 +- typescript/api/services/FormsService.ts | 422 +++++++++--------- typescript/api/services/RecordsService.ts | 2 +- 5 files changed, 208 insertions(+), 230 deletions(-) diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index e00fb85a4a..c821c64f4a 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -40,7 +40,7 @@ describe('The FormsService', function () { type: 'rdmp' } }; - FormsService.getForm(brand, '', true, '', record).subscribe(form => { + FormsService.getForm(brand, '', true, '', record).then(form => { expect(form).to.have.property('name', formName); done(); }) @@ -55,7 +55,7 @@ describe('The FormsService', function () { type: 'rdmp' } }; - FormsService.getForm(brand, '', true, '', record).subscribe(form => { + FormsService.getForm(brand, '', true, '', record).then(form => { expect(form).to.have.property('name', formName); done(); }) @@ -64,7 +64,7 @@ describe('The FormsService', function () { it('should return the form', function (done) { let brand = BrandingService.getDefault(); let formName = 'default-1.0-draft'; - FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { + FormsService.getForm(brand, formName, true, 'rdmp', {}).then(form => { expect(form).to.have.property('name', formName); done(); }) @@ -73,7 +73,7 @@ describe('The FormsService', function () { it('should return the autogenerated form', function (done) { let brand = BrandingService.getDefault(); let formName = 'generated-view-only'; - FormsService.getForm(brand, formName, true, 'rdmp', {}).subscribe(form => { + FormsService.getForm(brand, formName, true, 'rdmp', {}).then(form => { expect(form).to.have.property('name', formName); done(); }) diff --git a/typescript/api/controllers/RecordController.ts b/typescript/api/controllers/RecordController.ts index 45b1d4c2bf..ae787d36a4 100644 --- a/typescript/api/controllers/RecordController.ts +++ b/typescript/api/controllers/RecordController.ts @@ -261,7 +261,7 @@ export module Controllers { if (!hasEditAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('edit-error-no-permissions')}); } - form = await FormsService.getForm(brand, formParam, editMode, '', currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, '', currentRec); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } @@ -271,7 +271,7 @@ export module Controllers { if (!hasViewAccess) { return this.ajaxFail(req, res, null, {message: TranslationService.t('view-error-no-permissions')}); } - form = await FormsService.getForm(brand, formParam, editMode, '', currentRec).toPromise(); + form = await FormsService.getForm(brand, formParam, editMode, '', currentRec); if (_.isEmpty(form)) { return this.ajaxFail(req, res, null, {message: `Error, getting form ${formParam} for OID: ${oid}`}); } diff --git a/typescript/api/controllers/webservice/RecordController.ts b/typescript/api/controllers/webservice/RecordController.ts index 1775d7778f..65c66bad64 100644 --- a/typescript/api/controllers/webservice/RecordController.ts +++ b/typescript/api/controllers/webservice/RecordController.ts @@ -1085,7 +1085,7 @@ export module Controllers { let keys = _.keys(meta1); for(let key of keys) { - if(_.get(meta1,key,'') != _.get(meta2,key,'')) { + if(!_.isEqual(meta1?.[key],meta2?.[key])) { return false; } } diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index d47fdb7517..dc1623c91b 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -162,16 +162,16 @@ export module Services { }); } - public getForm(branding: BrandingModel, formParam: string, editMode: boolean, recordType: string, currentRec: any): Observable { + public async getForm(branding: BrandingModel, formParam: string, editMode: boolean, recordType: string, currentRec: any) { // allow client to set the form name to use const formName = _.isUndefined(formParam) || _.isEmpty(formParam) ? currentRec.metaMetadata.form : formParam; if(formName == 'generated-view-only') { - return this.generateFormFromSchema(branding, recordType, currentRec); + return await this.generateFormFromSchema(branding, recordType, currentRec); } else { - return this.getFormByName(formName, editMode); + return await this.getFormByName(formName, editMode).toPromise(); } } @@ -213,241 +213,219 @@ export module Services { return schema; } - public generateFormFromSchema(branding: BrandingModel, recordType: string, record: any): Observable { + public async generateFormFromSchema(branding: BrandingModel, recordType: string, record: any) { if(recordType == '') { recordType = _.get(record,'metaMetadata.type',''); if(recordType == '') { - return Observable.of(null); + return {}; } } - let starting = true; + let form: FormModel; + + let schema = this.inferSchemaFromMetadata(record); + + let fieldKeys = _.keys(schema.properties); + + let buttonsList = [ + { + class: 'AnchorOrButton', + roles: ['Admin', 'Librarians'], + viewOnly: true, + definition: { + label: '@view-record-audit-link', + value: '/@branding/@portal/record/viewAudit/@oid', + cssClasses: 'btn btn-large btn-info margin-15', + controlType: 'anchor' + }, + variableSubstitutionFields: ['value'] + }, + { + class: 'SaveButton', + viewOnly: true, + roles: ['Admin', 'Librarians'], + definition: { + name: 'confirmDelete', + label: 'Delete this record', + closeOnSave: true, + redirectLocation: '/@branding/@portal/dashboard/'+recordType, + cssClasses: 'btn-danger', + confirmationMessage: '@dataPublication-confirmDelete', + confirmationTitle: '@dataPublication-confirmDeleteTitle', + cancelButtonMessage: '@dataPublication-cancelButtonMessage', + confirmButtonMessage: '@dataPublication-confirmButtonMessage', + isDelete: true, + isSubmissionButton: true + }, + variableSubstitutionFields: ['redirectLocation'] + } + ]; + + let textFieldTemplate = { + class: 'TextField', + viewOnly: true, + definition: { + name: '', + label: '', + help: '', + type: 'text' + } + }; - return super.getObservable(RecordType.findOne({ - key: branding.id + "_" + recordType - })) - .flatMap(recordType => { + let groupComponentTemplate = { + class: 'Container', + compClass: 'GenericGroupComponent', + definition: { + name: '', + cssClasses: 'form-inline', + fields: [] + } + }; - return super.getObservable(WorkflowStep.findOne({ - recordType: recordType.id, - starting: starting - })); - }).flatMap(workflowStep => { + let groupTextFieldTemplate = { + class: 'TextField', + definition: { + name: '', + label: '', + type: 'text', + groupName: '', + groupClasses: 'width-30', + cssClasses : "width-80 form-control" + } + }; - if (workflowStep.starting == true) { - - let form: FormModel; - - let schema = this.inferSchemaFromMetadata(record); - - let fieldKeys = _.keys(schema.properties); - - let buttonsList = [ - { - class: 'AnchorOrButton', - roles: ['Admin', 'Librarians'], - viewOnly: true, - definition: { - label: '@view-record-audit-link', - value: '/@branding/@portal/record/viewAudit/@oid', - cssClasses: 'btn btn-large btn-info margin-15', - controlType: 'anchor' - }, - variableSubstitutionFields: ['value'] - }, - { - class: 'SaveButton', - viewOnly: true, - roles: ['Admin', 'Librarians'], - definition: { - name: 'confirmDelete', - label: 'Delete this record', - closeOnSave: true, - redirectLocation: '/@branding/@portal/dashboard/'+recordType, - cssClasses: 'btn-danger', - confirmationMessage: '@dataPublication-confirmDelete', - confirmationTitle: '@dataPublication-confirmDeleteTitle', - cancelButtonMessage: '@dataPublication-cancelButtonMessage', - confirmButtonMessage: '@dataPublication-confirmButtonMessage', - isDelete: true, - isSubmissionButton: true - }, - variableSubstitutionFields: ['redirectLocation'] - } - ]; - - let textFieldTemplate = { - class: 'TextField', - viewOnly: true, - definition: { - name: '', - label: '', - help: '', - type: 'text' - } - }; - - let groupComponentTemplate = { - class: 'Container', - compClass: 'GenericGroupComponent', - definition: { - name: '', - cssClasses: 'form-inline', - fields: [] - } - }; - - let groupTextFieldTemplate = { - class: 'TextField', - definition: { - name: '', - label: '', - type: 'text', - groupName: '', - groupClasses: 'width-30', - cssClasses : "width-80 form-control" - } - }; - - let repeatableGroupComponentTemplate = { - class: 'RepeatableContainer', - compClass: 'RepeatableGroupComponent', - definition: { - name: '', - label: '', - help: '', - forceClone: ['fields'], - fields: [] - } - }; - - let objectFieldHeadingTemplate = { - class: 'Container', - compClass: 'TextBlockComponent', - definition: { - value: '', - type: 'h3' - } - }; - - let mainTitleFieldName = 'title'; - - let fieldList = [ - { - class: 'Container', - compClass: 'TextBlockComponent', - viewOnly: true, - definition: { - name: mainTitleFieldName, - type: 'h1' - } - }, - { - class: 'Container', - compClass: 'GenericGroupComponent', - definition: { - cssClasses: "form-inline", - fields: buttonsList - } - } - ]; - - for(let fieldKey of fieldKeys) { - - let schemaProperty = schema.properties[fieldKey]; - - if(_.get(schemaProperty,'type','') == 'string') { - - let textField = _.cloneDeep(textFieldTemplate); - _.set(textField.definition,'name',fieldKey); - _.set(textField.definition,'label',fieldKey); - fieldList.push(textField); - - } if(_.get(schemaProperty,'type','') == 'array') { - - if(_.get(schemaProperty,'items.type','') == 'string') { - - let textField = _.cloneDeep(textFieldTemplate); - _.set(textField.definition,'name',fieldKey); - _.set(textField.definition,'label',fieldKey); - fieldList.push(textField); - - } else if(_.get(schemaProperty,'items.type','') == 'object') { - - let objectFieldKeys = _.keys(schemaProperty.items.properties); - let repeatableGroupField = _.cloneDeep(repeatableGroupComponentTemplate); - let groupField = _.cloneDeep(groupComponentTemplate); - let groupFieldList = []; - - for(let objectFieldKey of objectFieldKeys) { - let innerProperty = schemaProperty.items.properties[objectFieldKey]; - if(_.get(innerProperty,'type','') == 'string') { - let textField = _.cloneDeep(groupTextFieldTemplate); - _.set(textField.definition,'name',objectFieldKey); - _.set(textField.definition,'label',objectFieldKey); - _.set(textField.definition,'groupName','item'); - groupFieldList.push(textField); - } - } - - _.set(groupField.definition,'name','item'); - _.set(groupField.definition,'fields',groupFieldList); - _.set(repeatableGroupField.definition,'name',fieldKey); - _.set(repeatableGroupField.definition,'label',fieldKey); - _.set(repeatableGroupField.definition,'fields',[groupField]); - fieldList.push(repeatableGroupField); - } - - } else if(_.get(schemaProperty,'type','') == 'object') { - - let objectFieldKeys = _.keys(schemaProperty.properties); - let groupField = _.cloneDeep(groupComponentTemplate); - let groupFieldList = []; - - for(let objectFieldKey of objectFieldKeys) { - let innerProperty = schemaProperty.properties[objectFieldKey]; - if(_.get(innerProperty,'type','') == 'string') { - let textField = _.cloneDeep(groupTextFieldTemplate); - _.set(textField.definition,'name',objectFieldKey); - _.set(textField.definition,'label',objectFieldKey); - _.set(textField.definition,'groupName',fieldKey); - groupFieldList.push(textField); - } - } - - let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); - _.set(objectFieldHeading.definition, 'value', fieldKey); - fieldList.push(objectFieldHeading); - - _.set(groupField.definition,'name',fieldKey); - _.set(groupField.definition,'fields',groupFieldList); - fieldList.push(groupField); + let repeatableGroupComponentTemplate = { + class: 'RepeatableContainer', + compClass: 'RepeatableGroupComponent', + definition: { + name: '', + label: '', + help: '', + forceClone: ['fields'], + fields: [] + } + }; + + let objectFieldHeadingTemplate = { + class: 'Container', + compClass: 'TextBlockComponent', + definition: { + value: '', + type: 'h3' + } + }; + + let mainTitleFieldName = 'title'; + + let fieldList = [ + { + class: 'Container', + compClass: 'TextBlockComponent', + viewOnly: true, + definition: { + name: mainTitleFieldName, + type: 'h1' + } + }, + { + class: 'Container', + compClass: 'GenericGroupComponent', + definition: { + cssClasses: "form-inline", + fields: buttonsList + } + } + ]; + + for(let fieldKey of fieldKeys) { + + let schemaProperty = schema.properties[fieldKey]; + + if(_.get(schemaProperty,'type','') == 'string') { + + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey); + _.set(textField.definition,'label',fieldKey); + fieldList.push(textField); + + } if(_.get(schemaProperty,'type','') == 'array') { + + if(_.get(schemaProperty,'items.type','') == 'string') { + + let textField = _.cloneDeep(textFieldTemplate); + _.set(textField.definition,'name',fieldKey); + _.set(textField.definition,'label',fieldKey); + fieldList.push(textField); + + } else if(_.get(schemaProperty,'items.type','') == 'object') { + + let objectFieldKeys = _.keys(schemaProperty.items.properties); + let repeatableGroupField = _.cloneDeep(repeatableGroupComponentTemplate); + let groupField = _.cloneDeep(groupComponentTemplate); + let groupFieldList = []; + + for(let objectFieldKey of objectFieldKeys) { + let innerProperty = schemaProperty.items.properties[objectFieldKey]; + if(_.get(innerProperty,'type','') == 'string') { + let textField = _.cloneDeep(groupTextFieldTemplate); + _.set(textField.definition,'name',objectFieldKey); + _.set(textField.definition,'label',objectFieldKey); + _.set(textField.definition,'groupName','item'); + groupFieldList.push(textField); } } - - let formObject = { - name: 'generated-view-only', - type: recordType, - skipValidationOnSave: false, - editCssClasses: 'row col-md-12', - viewCssClasses: 'row col-md-offset-1 col-md-10', - messages: {}, - attachmentFields: [], - fields: fieldList - }; - - form = formObject as any; - - return Observable.of(form); + + _.set(groupField.definition,'name','item'); + _.set(groupField.definition,'fields',groupFieldList); + _.set(repeatableGroupField.definition,'name',fieldKey); + _.set(repeatableGroupField.definition,'label',fieldKey); + _.set(repeatableGroupField.definition,'fields',[groupField]); + fieldList.push(repeatableGroupField); } - return Observable.of(null); + } else if(_.get(schemaProperty,'type','') == 'object') { + + let objectFieldKeys = _.keys(schemaProperty.properties); + let groupField = _.cloneDeep(groupComponentTemplate); + let groupFieldList = []; + + for(let objectFieldKey of objectFieldKeys) { + let innerProperty = schemaProperty.properties[objectFieldKey]; + if(_.get(innerProperty,'type','') == 'string') { + let textField = _.cloneDeep(groupTextFieldTemplate); + _.set(textField.definition,'name',objectFieldKey); + _.set(textField.definition,'label',objectFieldKey); + _.set(textField.definition,'groupName',fieldKey); + groupFieldList.push(textField); + } + } - }).filter(result => result !== null).last(); - } - + let objectFieldHeading = _.cloneDeep(objectFieldHeadingTemplate); + _.set(objectFieldHeading.definition, 'value', fieldKey); + fieldList.push(objectFieldHeading); + + _.set(groupField.definition,'name',fieldKey); + _.set(groupField.definition,'fields',groupFieldList); + fieldList.push(groupField); + } + } + + let formObject = { + name: 'generated-view-only', + type: recordType, + skipValidationOnSave: false, + editCssClasses: 'row col-md-12', + viewCssClasses: 'row col-md-offset-1 col-md-10', + messages: {}, + attachmentFields: [], + fields: fieldList + }; + + form = formObject as any; + return form; + } protected setFormEditMode(fields, editMode): void{ _.remove(fields, field => { diff --git a/typescript/api/services/RecordsService.ts b/typescript/api/services/RecordsService.ts index 8a82d44855..cdc815473c 100644 --- a/typescript/api/services/RecordsService.ts +++ b/typescript/api/services/RecordsService.ts @@ -176,7 +176,7 @@ export module Services { let wfStep = await WorkflowStepsService.getFirst(recordType).toPromise(); let formName = _.get(wfStep,'config.form'); - let form = await FormsService.getForm(brand, formName, true, recordType.name, record).toPromise(); + let form = await FormsService.getForm(brand, formName, true, recordType.name, record); let metaMetadata = this.initRecordMetaMetadata(brand.id, user.username, recordType, wfStep, form, moment().format()); _.set(record,'metaMetadata',metaMetadata);