diff --git a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html index 23e9beed87..12fde4e599 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.html @@ -1,12 +1,32 @@
-

-

-

-

+
+
+

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

+
+
+

{{ '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.spec.ts b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.spec.ts index e8d666c5d3..0fc1119280 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,25 @@ let recordDataStandard = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: [], + filterBy: [], + filterWorkflowStepsBy: [], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', - groupBy: '', - sortGroupBy: [], + groupBy: '', + sortGroupBy: [], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ @@ -134,8 +148,7 @@ describe('DashboardComponent standard', () => { const fixture = TestBed.createComponent(DashboardComponent); 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']); @@ -152,12 +165,26 @@ let recordDataWorkspace = { dashboardType: { formatRules: { - filterBy: [], + filterBy: [], recordTypeFilterBy: 'existing-locations', - filterWorkflowStepsBy: [ 'existing-locations-draft'], + filterWorkflowStepsBy: [ 'existing-locations-draft'], + queryFilters: { + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + } + ] + } + ] + }, sortBy: 'metaMetadata.lastSaveDate:-1', - groupBy: '', - sortGroupBy: [], + groupBy: '', + sortGroupBy: [], + hideWorkflowStepTitleForRecordType: [] } }, step: [{ @@ -277,8 +304,7 @@ describe('DashboardComponent workspace', () => { const dashboardComponent = fixture.componentInstance; 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']); @@ -295,11 +321,25 @@ let recordDataConsolidated = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: ['consolidated'], + filterBy: [], + filterWorkflowStepsBy: ['consolidated'], + queryFilters: { + rdmp: [ + { + 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: [{ @@ -478,7 +518,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']; @@ -505,11 +545,25 @@ let recordDataConsolidatedRelationships = { dashboardType: { formatRules: { - filterBy: [], - filterWorkflowStepsBy: ['consolidated'], + filterBy: [], + filterWorkflowStepsBy: ['consolidated'], + queryFilters: { + rdmp: [ + { + 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: [{ @@ -705,7 +759,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 6aeb1ba5b1..e89d20d664 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.component.ts @@ -1,9 +1,10 @@ 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, 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'; +import * as _ from 'lodash'; @Component({ selector: 'dashboard', @@ -28,8 +29,15 @@ export class DashboardComponent extends BaseComponent { rulesService: object; currentUser: object = {}; enableSort: boolean = true; - - defaultTableConfig = [ + filterFieldName: string = 'Title'; + filterFieldPath: string = 'metadata.title'; + defaultFilterField: FilterField = { name: this.filterFieldName, path: this.filterFieldPath }; + filterSearchString: any = {}; + hideWorkflowStepTitle: boolean = false; + isFilterSearchDisplayed: any = {}; + isSearching: any = {}; + + defaultRowConfig = [ { title: 'Record Title', variable: 'metadata.title', @@ -90,14 +98,29 @@ 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: { + 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? + hideWorkflowStepTitleForRecordType: [] }; - formatRules: any = {}; + formatRules: FormatRules = this.defaultFormatRules; defaultGroupRowConfig = {}; groupRowConfig = {}; @@ -157,15 +180,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 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; @@ -176,6 +197,48 @@ export class DashboardComponent extends BaseComponent { recordType = recordTypeFilterBy; } + for(let recType of formatRules.hideWorkflowStepTitleForRecordType) { + if(recType == recordType) { + this.hideWorkflowStepTitle = true; + } + } + + let steps = await this.initWorkflowSteps(recordType); + + let startIndex = 1; + for (let step of steps) { + + this.initStepTableConfig(recordType, step); + + this.initSortConfig(step); + + this.workflowSteps.push(step); + + let packageType = ''; + let stepName = ''; + let evaluateStepName = ''; + if (this.dashboardTypeSelected == 'consolidated') { + packageType = ''; + stepName = ''; + evaluateStepName = _get(step, 'name'); + recordType = _get(step, 'config.baseRecordType'); + } else if (this.dashboardTypeSelected == 'workspace') { + stepName = ''; + packageType = this.packageType; + evaluateStepName = _get(step, 'name'); + recordType = ''; + } else { + packageType = ''; + stepName = _get(step, 'name'); + evaluateStepName = stepName; + } + + await this.initStep(stepName, evaluateStepName, recordType, packageType, startIndex); + } + } + + private async initWorkflowSteps(recordType: string) { + let beforeFilterSteps: any = await this.recordService.getWorkflowSteps(recordType); let filterWorkflowStepsBy = _get(this.formatRules, 'filterWorkflowStepsBy'); @@ -195,87 +258,62 @@ export class DashboardComponent extends BaseComponent { steps = beforeFilterSteps; } steps = _orderBy(steps, ['config.displayIndex'], ['asc']); + return steps; + } - 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'))) { + private initStepTableConfig(recordType: string, step: any) { - 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 }); - } + let stepRowConfig = this.defaultRowConfig; - 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 (_isEmpty(this.defaultRowConfig[0].title)) { + this.defaultRowConfig[0].title = `${recordType}-title` || 'Title'; + } - if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowRulesConfig'))) { - this.groupRowRules = _get(step, 'config.dashboard.table.groupRowRulesConfig'); - } + if (!_isUndefined(_get(step, 'config.dashboard')) + && !_isUndefined(_get(step, 'config.dashboard.table'))) { - //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'); - } + if (!_isUndefined(_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; }); } - this.tableConfig[step.name] = stepTableConfig; - this.sortMap[step.name] = {}; - for (let rowConfig of stepTableConfig) { - this.sortMap[step.name][rowConfig.variable] = { - sort: rowConfig.initialSort - }; + if (!_isUndefined(_get(step, 'config.dashboard.table.rowRulesConfig'))) { + this.rowLevelRules = _get(step, 'config.dashboard.table.rowRulesConfig'); } - - if(this.dashboardTypeSelected == 'consolidated') { - this.enableSort = false; - } else { - this.enableSort = true; + + if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowConfig'))) { + this.groupRowConfig = _get(step, 'config.dashboard.table.groupRowConfig'); } - if(this.dashboardTypeSelected == 'consolidated') { - this.enableSort = false; - } else { - this.enableSort = true; + if (!_isUndefined(_get(step, 'config.dashboard.table.groupRowRulesConfig'))) { + this.groupRowRules = _get(step, 'config.dashboard.table.groupRowRulesConfig'); } - let packageType = ''; - let stepName = ''; - let evaluateStepName = ''; - if (this.dashboardTypeSelected == 'consolidated') { - packageType = ''; - stepName = ''; - evaluateStepName = _get(step, 'name'); - recordType = _get(step, 'config.baseRecordType'); - } else if (this.dashboardTypeSelected == 'workspace') { - stepName = ''; - packageType = this.packageType; - evaluateStepName = _get(step, 'name'); - recordType = ''; - } else { - packageType = ''; - stepName = _get(step, 'name'); - evaluateStepName = stepName; + //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'); } + } - await this.initStep(stepName, evaluateStepName, recordType, packageType, startIndex); + this.tableConfig[step.name] = stepRowConfig; + } + + private initSortConfig(step: any) { + + let stepRowConfig: any[] = this.tableConfig[step.name]; + + this.sortMap[step.name] = {}; + + for (let columnConfig of stepRowConfig) { + this.sortMap[step.name][columnConfig.variable] = { + sort: columnConfig.initialSort + }; + } - // console.log('-------------------------------------------------'); - // console.log(JSON.stringify(this.records)); - // console.log('-------------------------------------------------'); + if (this.dashboardTypeSelected == 'consolidated') { + this.enableSort = false; + } else { + this.enableSort = true; } } @@ -303,7 +341,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 +355,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'); @@ -396,11 +377,90 @@ export class DashboardComponent extends BaseComponent { } else { planTable = this.evaluatePlanTableColumns({}, {}, {}, evaluateStepName, stagedRecords); + + if (this.dashboardTypeSelected == 'standard') { + let filter: FilterField = this.getFirstTextFilter(); + this.filterFieldName = filter.name; + this.filterFieldPath = filter.path; + } } 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,14 +502,11 @@ export class DashboardComponent extends BaseComponent { let record: any = {}; + let stepRowConfig: any[] = this.tableConfig[stepName]; - let stepTableConfig = _isEmpty(this.tableConfig[stepName]) ? this.defaultTableConfig : this.tableConfig[stepName]; - - for (let rowConfig of stepTableConfig) { - - - 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); } @@ -485,15 +542,12 @@ 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]; + let stepRowConfig = this.tableConfig[stepName]; - for (let rowConfig of stepTableCOnfig) { - - 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); } @@ -505,9 +559,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 +572,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 +588,6 @@ export class DashboardComponent extends BaseComponent { let evaluateRulesTemplate = _get(rule, 'evaluateRulesTemplate'); _set(imports, 'name', name); - let evaluatedAction = ''; let action = _get(rule, 'action'); @@ -604,9 +653,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') { @@ -650,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); + 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); @@ -662,9 +709,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 }; @@ -675,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)); + 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') { @@ -719,6 +767,99 @@ export class DashboardComponent extends BaseComponent { return 'metaMetadata.lastSaveDate:-1'; } + private getFirstFilter(type:string): 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; + } + } + + private getFilters(type:string) { + let filterFields: FilterField[] = []; + let queryFilters: QueryFilter[] = this.formatRules.queryFilters[this.recordType]; + for(let queryFilter of queryFilters) { + if(queryFilter.filterType == type) { + for(let filterField of queryFilter.filterFields) { + filterFields.push(filterField); + } + } + } + return filterFields; + } + + private getFirstTextFilter(): FilterField { + return this.getFirstFilter('text'); + } + + public getTextFilters() { + return this.getFilters('text'); + } + + public getFilterSearchDisplayed(step: any): boolean { + let filterDisplayed = _.get(this.isFilterSearchDisplayed,step,''); + if(filterDisplayed == 'filterDisplayed') { + return true; + } else { + return false; + } + } + + 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[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.getFilterSearchString(step)); + let planTable: PlanTable = this.evaluatePlanTableColumns({}, {}, {}, step, stagedRecords); + this.records[step] = planTable; + this.isSearching[step] = ''; + } + } + + public async resetFilterAndSearch(step: string, e: any) { + if (this.dashboardTypeSelected == 'standard') { + this.setFilterField(this.getFirstTextFilter(), e); + this.isSearching[step] = 'searching'; + let sortDetails = this.sortMap[step]; + 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[step] = ''; + } + } + + public setFilterField(filterField:FilterField, e: any) { + if (e) { + e.preventDefault(); + } + 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 dc3be92f0c..a189561a46 100644 --- a/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts +++ b/angular/projects/researchdatabox/dashboard/src/app/dashboard.module.ts @@ -1,13 +1,10 @@ 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 { 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 +15,7 @@ import { SortComponent } from './sort/sort.component'; ], imports: [ BrowserModule, - ReactiveFormsModule, FormsModule, - RouterModule, RedboxPortalCoreModule, 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 641432ca27..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 @@ -21,3 +21,37 @@ export class Plan { metadata: object = {}; dashboardTitle: string = ''; } + +export declare class FilterField { + name: string; + path: string; +} + +export declare class QueryFilter { + filterType: string; + filterFields: FilterField[]; +} + +export declare class SortGroupBy { + rowLevel: number; + compareFieldValue: string; + compareField: string; + relatedTo: string; +} + +export declare class FormatRules { + filterBy: any; + filterWorkflowStepsBy: string[]; + recordTypeFilterBy: string; + queryFilters: { [key: string]: 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/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/dashboardtype.js b/config/dashboardtype.js index a7cf6d0c63..24952313d4 100644 --- a/config/dashboardtype.js +++ b/config/dashboardtype.js @@ -1,21 +1,85 @@ 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: { + party: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + } + ], + rdmp: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + } + ], + dataRecord: [ + { + filterType: 'text', + filterFields: [ + { + name: 'Title', + path: 'metadata.title' + }, + { + name: 'Contributor', + path: 'metadata.contributor_ci.text_full_name' + } + ] + } + ], + dataPublication: [ + { + 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? + 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', groupBy: '', //values: empty (not grouped any order), groupedByRecordType, groupedByRelationships - sortGroupBy: [] //values: as many levels as required? + sortGroupBy: [], //values: as many levels as required + hideWorkflowStepTitleForRecordType: [] } }, "consolidated": { @@ -26,7 +90,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: [] } } }; diff --git a/config/recordtype.js b/config/recordtype.js index 135d89a9ff..e39480306c 100644 --- a/config/recordtype.js +++ b/config/recordtype.js @@ -604,5 +604,42 @@ module.exports.recordtype = { "packageName": "consolidated", "searchFilters": [], hooks: { } + }, + "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 64cb2fb21c..b4f9dbae79 100644 --- a/config/workflow.js +++ b/config/workflow.js @@ -282,5 +282,57 @@ module.exports.workflow = { starting: false, consolidated: true } + }, + "party": { + "draft": { + config: { + workflow: { + stage: 'draft', + stageLabel: 'Draft', + }, + authorization: { + viewRoles: ['Admin', 'Librarians'], + editRoles: ['Admin', 'Librarians'] + }, + form: 'generated-view-only', + dashboard: { + table: { + rowConfig: [ + { + title: 'Party Name', + variable: 'metadata.GIVEN_NAME', + template: `<%= metadata.GIVEN_NAME %> <%= metadata.FAMILY_NAME %>`, + initialSort: 'desc' + }, + { + title: 'Party Title', + variable: 'metadata.JOB_TITLE', + template: '<%= metadata.JOB_TITLE %>', + initialSort: 'desc' + }, + { + title: 'Party Email', + variable: 'metadata.EMAIL', + template: '<%= metadata.EMAIL %>', + 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 c2b563fd91..1c3a12c3ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "flat": "^6.0.1", "font-awesome-sass": "4.7.0", "fs-extra": "^11.2.0", - "glob": "^10.4.2", + "genson-js": "^0.0.8", + "glob": "^10.4.1", "har-validator": "5.1.5", "i18next": "^23.11.5", "i18next-fs-backend": "^2.3.1", @@ -3527,6 +3528,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 4935084491..3f9a33d0c6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "flat": "^6.0.1", "font-awesome-sass": "4.7.0", "fs-extra": "^11.2.0", - "glob": "^10.4.2", + "genson-js": "^0.0.8", + "glob": "^10.4.1", "har-validator": "5.1.5", "i18next": "^23.11.5", "i18next-fs-backend": "^2.3.1", diff --git a/test/unit/services/FormsService.test.js b/test/unit/services/FormsService.test.js index b39c73a4e8..c821c64f4a 100644 --- a/test/unit/services/FormsService.test.js +++ b/test/unit/services/FormsService.test.js @@ -9,22 +9,72 @@ 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}_${recordType}`); + 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, recordType, true).subscribe(function(form) { expect(form).to.have.property('name', formName); done(); }) }); - 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(); + }) + }); + + 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, + type: 'rdmp' + } + }; + FormsService.getForm(brand, '', true, '', record).then(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, + type: 'rdmp' + } + }; + FormsService.getForm(brand, '', true, '', record).then(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(brand, formName, true, 'rdmp', {}).then(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(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 203e790ad1..ae787d36a4 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, recordType, true).subscribe(form => { if (form['customAngularApp'] != null) { appSelector = form['customAngularApp']['appSelector']; appName = form['customAngularApp']['appName']; @@ -227,89 +227,78 @@ 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 recordType = 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 => { + let mergedForm: any = {}; + try { + if (_.isEmpty(oid)) { + //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 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; - }); - }); - } 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; + let form: any = {}; - return form; - }); - }); - }); + 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')}); + } + 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}`}); + } + } 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')}); + } + 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}`}); + } + let hasEditAccess = await this.hasEditAccess(brand, req.user, currentRec).toPromise(); + FormsService.filterFieldsHasEditAccess(form.fields, hasEditAccess); } - }); - } - obs.subscribe(form => { - if (!_.isEmpty(form)) { - this.ajaxOk(req, res, null, form); + + 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 { - 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}`}); } - }, 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/controllers/webservice/RecordController.ts b/typescript/api/controllers/webservice/RecordController.ts index 0263ff28d7..65c66bad64 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(!_.isEqual(meta1?.[key],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); + } } } } diff --git a/typescript/api/services/AgendaQueueService.ts b/typescript/api/services/AgendaQueueService.ts index ec7efb7b6b..241f53a120 100644 --- a/typescript/api/services/AgendaQueueService.ts +++ b/typescript/api/services/AgendaQueueService.ts @@ -87,8 +87,9 @@ export module Services { _.forOwn(sails.config.agendaQueue.options, (optionVal:any, optionName:string) => { this.setOptionIfDefined(agendaOpts, optionName, optionVal); }); + const dbManager = User.getDatastore().manager; 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); @@ -111,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)) { diff --git a/typescript/api/services/FormsService.ts b/typescript/api/services/FormsService.ts index 7505a8a5b7..dc1623c91b 100644 --- a/typescript/api/services/FormsService.ts +++ b/typescript/api/services/FormsService.ts @@ -20,11 +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; @@ -48,7 +50,10 @@ export module Services { 'flattenFields', 'getFormByName', 'filterFieldsHasEditAccess', - 'listForms' + 'listForms', + 'inferSchemaFromMetadata', + 'generateFormFromSchema', + 'getFormByStartingWorkflowStep' ]; public async bootstrap(workflowStep): Promise { @@ -157,10 +162,25 @@ export module Services { }); } - public getForm = (branding, recordType, editMode, starting: boolean): 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 await this.generateFormFromSchema(branding, recordType, currentRec); + } else { + + return await this.getFormByName(formName, editMode).toPromise(); + } + } + + 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 => { @@ -188,6 +208,225 @@ export module Services { }).filter(result => result !== null).last(); } + public inferSchemaFromMetadata(record: any): any { + const schema = createSchema(record.metadata); + return schema; + } + + public async generateFormFromSchema(branding: BrandingModel, recordType: string, record: any) { + + if(recordType == '') { + recordType = _.get(record,'metaMetadata.type',''); + if(recordType == '') { + return {}; + } + } + + 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 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 => { if (editMode) { diff --git a/typescript/api/services/RecordsService.ts b/typescript/api/services/RecordsService.ts index a2d950b569..cdc815473c 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); let metaMetadata = this.initRecordMetaMetadata(brand.id, user.username, recordType, wfStep, form, moment().format()); _.set(record,'metaMetadata',metaMetadata); 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); }; 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')%> +
+ + + +
+ +
+ <% }%>