diff --git a/ui/snapclient/src/app/source/source-import/source-import.component.ts b/ui/snapclient/src/app/source/source-import/source-import.component.ts
index 44dd3be0..b8354ff9 100644
--- a/ui/snapclient/src/app/source/source-import/source-import.component.ts
+++ b/ui/snapclient/src/app/source/source-import/source-import.component.ts
@@ -40,6 +40,8 @@ export class SourceImportComponent implements OnInit, OnDestroy, AfterViewChecke
private readonly MAXFILESIZE: number;
readonly MAX_NAME = 100;
readonly MAX_VERSION = 30;
+ readonly MAX_CODESYSTEM = 255;
+ readonly MAX_VALUESET = 255;
fileaccept = '.csv, .tsv, .txt';
sourceType = '';
contents = '';
diff --git a/ui/snapclient/src/app/store/fhir-feature/fhir.actions.ts b/ui/snapclient/src/app/store/fhir-feature/fhir.actions.ts
index 1d7e86ba..144ebb11 100644
--- a/ui/snapclient/src/app/store/fhir-feature/fhir.actions.ts
+++ b/ui/snapclient/src/app/store/fhir-feature/fhir.actions.ts
@@ -35,12 +35,12 @@ export enum FhirActionTypes {
LOOKUP_CONCEPT = "[Fhir] Lookup Concept",
LOOKUP_CONCEPT_SUCCESS = "[Fhir] Lookup Concept Succeeded",
LOOKUP_CONCEPT_FAILED = "[Fhir] Lookup Concept Failed",
- LOOKUP_MODULE = "[Fhir] Lookup Module",
- LOOKUP_MODULE_SUCCESS = "[Fhir] Lookup Module Succeeded",
- LOOKUP_MODULE_FAILED = "[Fhir] Lookup Module Failed",
+ DISPLAY_RESOLVED_LOOKUP_CONCEPT = "[Fhir] Display Resolved Lookup Concept",
+ DISPLAY_RESOLVED_LOOKUP_CONCEPT_SUCCESS = "Display Resolved Lookup Concept Succeeded",
+ DISPLAY_RESOLVED_LOOKUP_CONCEPT_FAILED = "Display Resolved Lookup Concept Failed",
CONCEPT_HIERARCHY = "[Fhir] Concept Hierarchy",
CONCEPT_HIERARCHY_SUCCESS = "Concept Hierarchy Succeeded",
- CONCEPT_HIERARCHY_FAILED = "Concept Hierarchy Failed"
+ CONCEPT_HIERARCHY_FAILED = "Concept Hierarchy Failed",
}
export class LoadReleases implements Action {
@@ -106,7 +106,7 @@ export class AutoSuggestFailure implements Action {
export class LookupConcept implements Action {
readonly type = FhirActionTypes.LOOKUP_CONCEPT;
- constructor(public payload: {code: string, system: string, version: string}) {
+ constructor(public payload: {code: string, system: string, version: string, properties: string[]}) {
}
}
@@ -124,22 +124,22 @@ export class LookupConceptFailure implements Action {
}
}
-export class LookupModule implements Action {
- readonly type = FhirActionTypes.LOOKUP_MODULE;
+export class DisplayResolvedLookupConcept implements Action {
+ readonly type = FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT;
- constructor(public payload: {code: string, system: string, version: string}) {
+ constructor(public payload: {code: string, system: string, version: string, properties: string[]}) {
}
}
-export class LookupModuleSuccess implements Action {
- readonly type = FhirActionTypes.LOOKUP_MODULE_SUCCESS;
+export class DisplayResolvedLookupConceptSuccess implements Action {
+ readonly type = FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT_SUCCESS;
constructor(public payload: Properties) {
}
}
-export class LookupModuleFailure implements Action {
- readonly type = FhirActionTypes.LOOKUP_MODULE_FAILED;
+export class DisplayResolvedLookupConceptFailure implements Action {
+ readonly type = FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT_FAILED;
constructor(public payload: { error: any }) {
}
@@ -178,9 +178,9 @@ export type FhirActions = LoadReleases
| LookupConcept
| LookupConceptSuccess
| LookupConceptFailure
- | LookupModule
- | LookupModuleSuccess
- | LookupModuleFailure
+ | DisplayResolvedLookupConcept
+ | DisplayResolvedLookupConceptSuccess
+ | DisplayResolvedLookupConceptFailure
| ConceptHierarchy
| ConceptHierarchySuccess
| ConceptHierarchyFailure
diff --git a/ui/snapclient/src/app/store/fhir-feature/fhir.effects.ts b/ui/snapclient/src/app/store/fhir-feature/fhir.effects.ts
index 8882dd16..a9893655 100644
--- a/ui/snapclient/src/app/store/fhir-feature/fhir.effects.ts
+++ b/ui/snapclient/src/app/store/fhir-feature/fhir.effects.ts
@@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { of } from 'rxjs/internal/observable/of';
import { FhirService } from '../../_services/fhir.service';
import { R4 } from '@ahryman40k/ts-fhir-types';
@@ -33,15 +33,13 @@ import {
AutoSuggestFailure,
ConceptHierarchySuccess,
ConceptHierarchyFailure,
- LookupModuleSuccess,
- LookupModuleFailure
+ DisplayResolvedLookupConceptSuccess,
+ DisplayResolvedLookupConceptFailure
} from './fhir.actions';
import { Release } from '../../_services/fhir.service';
-import {Match} from './fhir.reducer';
import {TranslateService} from '@ngx-translate/core';
import {SnomedUtils} from 'src/app/_utils/snomed_utils';
-import { ObservableInput } from 'rxjs';
export interface Properties {
[key: string]: any[]
@@ -125,8 +123,34 @@ export class FhirEffects {
case 'property': {
if (part) {
const partKey = part.find((sub: any) => sub?.name === 'code').valueCode;
- const partValue = FhirEffects.getValue(part.find((sub: any) => sub.name?.startsWith('value')));
- FhirEffects.updateProps(props, partKey, [partValue]);
+ if ("609096000" === partKey) { // role group
+ const subproperties = part.filter((sub: any) => sub.name?.startsWith('subproperty')).map((subproperty: any) => subproperty.part)
+ .sort((a:any, b:any) => {
+ const aValueStr = a.filter((subproperty: any) => {
+ if (subproperty.name === "code" && subproperty.hasOwnProperty("valueString")) {
+ return subproperty;
+ }
+ });
+ const bValueStr = b.filter((subproperty: any) => {
+ if (subproperty.name === "code" && subproperty.hasOwnProperty("valueString")) {
+ return subproperty;
+ }
+ });
+ return aValueStr[0].valueString.localeCompare(bValueStr[0].valueString);
+ });
+ FhirEffects.updateProps(props, "attributeRelationships", subproperties);
+ }
+ else if (!isNaN(+partKey)) {
+ part.filter((part: any) => part);
+ FhirEffects.updateProps(props, "attributeRelationships", part.filter((part: any) => part));
+ }
+ else {
+ let partValue = FhirEffects.getValue(part.find((sub: any) => sub.name?.startsWith('valueString')));
+ if (!partValue) {
+ partValue = FhirEffects.getValue(part.find((sub: any) => sub.name?.startsWith('value')));
+ }
+ FhirEffects.updateProps(props, partKey, [partValue]);
+ }
}
break;
}
@@ -161,24 +185,25 @@ export class FhirEffects {
return props;
}
- lookupModule$ = createEffect(() => this.actions$.pipe(
- ofType(FhirActionTypes.LOOKUP_MODULE),
- map(action => action.payload),
- switchMap((action) => this.fhirService.lookupConcept(action.code, action.system, action.version).pipe(
- map(parameters => this.mapParameters(parameters, action)),
- switchMap((props) => of(new LookupModuleSuccess(props))),
- catchError((err) => of(new LookupModuleFailure({ error: err })))
- ))), { dispatch: true });
lookupConcept$ = createEffect(() => this.actions$.pipe(
ofType(FhirActionTypes.LOOKUP_CONCEPT),
map(action => action.payload),
- switchMap((action) => this.fhirService.lookupConcept(action.code, action.system, action.version).pipe(
+ switchMap((action) => this.fhirService.lookupConcept(action.code, action.system, action.version, action.properties).pipe(
map(parameters => this.mapParameters(parameters, action)),
switchMap((props) => of(new LookupConceptSuccess(props))),
catchError((err) => of(new LookupConceptFailure({ error: err })))
))), { dispatch: true });
+ displayResolvedLookupConcept$ = createEffect(() => this.actions$.pipe(
+ ofType(FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT),
+ map(action => action.payload),
+ switchMap((action) => this.fhirService.displayResolvedLookupConcept(action.code, action.system, action.version, action.properties).pipe(
+ map(parameters => this.mapParameters(parameters, action)),
+ switchMap((props) => of(new DisplayResolvedLookupConceptSuccess(props))),
+ catchError((err) => of(new DisplayResolvedLookupConceptFailure({ error: err })))
+ ))), { dispatch: true });
+
constructor(
private actions$: Actions,
private fhirService: FhirService,
diff --git a/ui/snapclient/src/app/store/fhir-feature/fhir.reducer.ts b/ui/snapclient/src/app/store/fhir-feature/fhir.reducer.ts
index 59f14f64..b9e8f0d6 100644
--- a/ui/snapclient/src/app/store/fhir-feature/fhir.reducer.ts
+++ b/ui/snapclient/src/app/store/fhir-feature/fhir.reducer.ts
@@ -38,19 +38,26 @@ export interface Match {
}
export interface IFhirState {
- editionToVersionsMap : Map | undefined;
+ editionToVersionsMap: Map | undefined;
matches?: R4.IValueSet_Expansion;
nodes: ConceptNode[];
suggests?: Match[];
properties?: Properties;
- moduleProperties?: Properties;
+ resolvedDisplayProperties? : Properties;
errorMessage: any | null;
+ replacementSuggestions: {
+ sameAs: R4.IParameters,
+ replacedBy: R4.IParameters,
+ possiblyEquivalentTo: R4.IParameters,
+ alternative: R4.IParameters
+ } | null;
}
export const initialFhirState: IFhirState = {
editionToVersionsMap: new Map(),
nodes: [],
- errorMessage: null
+ errorMessage: null,
+ replacementSuggestions: null
};
export function fhirReducer(state = initialFhirState, action: FhirActions): IFhirState {
@@ -112,17 +119,17 @@ export function fhirReducer(state = initialFhirState, action: FhirActions): IFhi
errorMessage: action.payload.error
};
- case FhirActionTypes.LOOKUP_MODULE_SUCCESS:
+ case FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT_SUCCESS:
return {
...state,
- moduleProperties: action.payload,
+ resolvedDisplayProperties: action.payload,
errorMessage: null
};
- case FhirActionTypes.LOOKUP_MODULE_FAILED:
+ case FhirActionTypes.DISPLAY_RESOLVED_LOOKUP_CONCEPT_FAILED:
return {
...state,
- moduleProperties: undefined,
+ resolvedDisplayProperties: undefined,
errorMessage: action.payload.error
};
diff --git a/ui/snapclient/src/app/store/fhir-feature/fhir.selectors.ts b/ui/snapclient/src/app/store/fhir-feature/fhir.selectors.ts
index 8d6c48ef..32612d28 100644
--- a/ui/snapclient/src/app/store/fhir-feature/fhir.selectors.ts
+++ b/ui/snapclient/src/app/store/fhir-feature/fhir.selectors.ts
@@ -45,9 +45,9 @@ export const selectConceptProperties = createSelector(
(state: IFhirState) => state.properties
);
-export const selectModuleProperties = createSelector(
+export const selectDisplayResolvedConceptProperties = createSelector(
selectModules,
- (state: IFhirState) => state.moduleProperties
+ (state: IFhirState) => state.resolvedDisplayProperties
);
export const selectFhirError = createSelector(
diff --git a/ui/snapclient/src/app/store/mapping-feature/mapping.effects.ts b/ui/snapclient/src/app/store/mapping-feature/mapping.effects.ts
index a372df6f..69483390 100644
--- a/ui/snapclient/src/app/store/mapping-feature/mapping.effects.ts
+++ b/ui/snapclient/src/app/store/mapping-feature/mapping.effects.ts
@@ -72,16 +72,17 @@ export class MappingEffects {
map(p => ServiceUtils.extractIdFromHref(p._links.self.href, null)));
}
+ const theMapping = cloneDeep(new_mapping.mapping);
return forkJoin([pid, sid]).pipe(
switchMap(([projectid, sourceid]) => {
- return this.mapService.createMapping(new_mapping.mapping, projectid, sourceid).pipe(
+ return this.mapService.createMapping(theMapping, projectid, sourceid, new_mapping.dualMapMode).pipe(
map((m) => {
- new_mapping.mapping.id = ServiceUtils.extractIdFromHref(m._links.self.href, null);
- new_mapping.mapping.source = m.source as Source;
- if (new_mapping.mapping.project) {
- new_mapping.mapping.project.mapcount = 1;
+ theMapping.id = ServiceUtils.extractIdFromHref(m._links.self.href, null);
+ theMapping.source = m.source as Source;
+ if (theMapping.project) {
+ theMapping.project.mapcount = 1;
}
- return new_mapping.mapping;
+ return theMapping;
}),
switchMap((mapping: Mapping) => of(new AddMappingSuccess(mapping))),
catchError((err: any) => of(new AddMappingFailure(err))),
@@ -98,11 +99,11 @@ export class MappingEffects {
}),
switchMap((result: ImportMappingFileResult) => [
new ImportMappingFileSuccess(result),
- new AddMappingSuccess(new_mapping.mapping)
+ new AddMappingSuccess(theMapping)
]),
catchError((err, mapping) => [
new ImportMappingFileFailure(err),
- new AddMappingSuccess(new_mapping.mapping)
+ new AddMappingSuccess(theMapping)
])
);
})
@@ -229,8 +230,9 @@ export class MappingEffects {
map((action) => action.payload),
switchMap((payload) => {
const context = payload.context;
+ const filter = cloneDeep(context.filter);
return this.mapService.getMapView(payload.mapping, context.pageIndex,
- context.pageSize, context.sortColumn, context.sortDir, context.filter).pipe(
+ context.pageSize, context.sortColumn, context.sortDir, filter).pipe(
switchMap((mapView: MapViewResults) => of(new LoadMapViewSuccess(mapView))),
catchError((error: any) => of(new LoadMapViewFailure(error))),
);
@@ -282,6 +284,7 @@ function toMapping(mapDto: any, project?: Project): Mapping {
mapping.project = project;
} else {
mapping.project = toProject(mapDto.project);
+ mapping.project.dualMapMode = mapDto.project.dualMapMode;
mapping.project.owners = mapDto.owners.map(toUser);
mapping.project.members = mapDto.members?.map(toUser) ?? [];
mapping.project.guests = mapDto.guests?.map(toUser) ?? [];
@@ -308,6 +311,7 @@ function toProject(project: any, loadProject?: boolean): Project {
proj.id = project.id;
proj.maps = (project.maps ?? []).map((m: any) => toMapping(m, project));
proj.mapcount = project.mapCount;
+ proj.dualMapMode = project.dualMapMode;
if (loadProject) {
const allOwners = Array.from(new Set(project.maps.flatMap((map: any) => map?.project?.owners).concat(project.owners)));
diff --git a/ui/snapclient/src/app/store/task-feature/task.actions.ts b/ui/snapclient/src/app/store/task-feature/task.actions.ts
index 466985cd..ade499ae 100644
--- a/ui/snapclient/src/app/store/task-feature/task.actions.ts
+++ b/ui/snapclient/src/app/store/task-feature/task.actions.ts
@@ -41,7 +41,9 @@ export class LoadTasksForMap implements Action {
authPageSize: number | undefined,
authCurrentPage: number | undefined
reviewPageSize: number | undefined,
- reviewCurrentPage: number | undefined
+ reviewCurrentPage: number | undefined,
+ reconcilePageSize: number | undefined,
+ reconcileCurrentPage: number | undefined
}) {
}
}
diff --git a/ui/snapclient/src/app/store/task-feature/task.effects.ts b/ui/snapclient/src/app/store/task-feature/task.effects.ts
index cd0a577e..4dfebead 100644
--- a/ui/snapclient/src/app/store/task-feature/task.effects.ts
+++ b/ui/snapclient/src/app/store/task-feature/task.effects.ts
@@ -73,13 +73,22 @@ export class TaskEffects {
switchMap((resp: TaskPage) => of(new LoadTasksSuccess(resp))),
catchError((err) => of(new LoadTasksFailure({error: err})))
)
- return forkJoin([authTasks, reviewTasks]).pipe(
- switchMap(([authTasks, reviewTasks]) => {
- if (authTasks instanceof LoadTasksSuccess && reviewTasks instanceof LoadTasksSuccess) {
+ let reconcileTasks = this.taskService.getTasksByMapAndType(payload.id, TaskType.RECONCILE, payload.reconcilePageSize, payload.reconcileCurrentPage).pipe(
+ map((resp: TaskResults) => {
+ let tasks_conv: Task[] = resp._embedded.tasks.map((task: any) => TaskEffects.mapTaskFromPayload(task));
+ let taskPage: TaskPageDetails = resp.page;
+ return new TaskPage(taskPage, tasks_conv);
+ }),
+ switchMap((resp: TaskPage) => of(new LoadTasksSuccess(resp))),
+ catchError((err) => of(new LoadTasksFailure({error: err})))
+ )
+ return forkJoin([authTasks, reviewTasks, reconcileTasks]).pipe(
+ switchMap(([authTasks, reviewTasks, reconcileTasks]) => {
+ if (authTasks instanceof LoadTasksSuccess && reviewTasks instanceof LoadTasksSuccess && reconcileTasks instanceof LoadTasksSuccess) {
let taskPages: TaskPageForType[] = [];
let tasks: Task[] = [];
- taskPages = [{type: TaskType.AUTHOR, page: authTasks.payload}, {type: TaskType.REVIEW, page: reviewTasks.payload}];
- tasks = [...authTasks.payload.tasks, ...reviewTasks.payload.tasks];
+ taskPages = [{type: TaskType.AUTHOR, page: authTasks.payload}, {type: TaskType.REVIEW, page: reviewTasks.payload}, {type: TaskType.RECONCILE, page: reconcileTasks.payload}];
+ tasks = [...authTasks.payload.tasks, ...reviewTasks.payload.tasks, ...reconcileTasks.payload.tasks];
return of(new LoadAllTasksSuccess({ taskPages: taskPages, tasks: tasks }));
} else {
return EMPTY;
diff --git a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.html b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.html
index f9402740..328ae876 100644
--- a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.html
+++ b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.html
@@ -38,6 +38,27 @@
+
+
+ compare_arrows
+ {{'TASK.TAB_RECONCILE' | translate }}
+
+
+
+
+
+
checklist
diff --git a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.spec.ts b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.spec.ts
index e28e2404..3bba95ae 100644
--- a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.spec.ts
+++ b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.spec.ts
@@ -47,7 +47,7 @@ describe('AssignedWorkComponent', () => {
mapping.project.title = 'Test Map';
const task = new Task('1', TaskType.AUTHOR, 'test', mapping, user, '1-10', 10, '', '', false, false);
- const expectedTabLabels = ['add_taskTASK.ADD_TASK', 'editTASK.TAB_AUTHOR', 'checklistTASK.TAB_REVIEW'];
+ const expectedTabLabels = ['add_taskTASK.ADD_TASK', 'editTASK.TAB_AUTHOR', 'checklistTASK.TAB_REVIEW', 'compare_arrowsTASK.TAB_RECONCILE'];
beforeEach(async () => {
await TestBed.configureTestingModule({
diff --git a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.ts b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.ts
index d312f7fe..5336e9f5 100644
--- a/ui/snapclient/src/app/task/assigned-work/assigned-work.component.ts
+++ b/ui/snapclient/src/app/task/assigned-work/assigned-work.component.ts
@@ -41,6 +41,7 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
private subscription = new Subscription();
authorTasks: Task[] | null | undefined;
reviewTasks: Task[] | null | undefined;
+ reconcileTasks: Task[] | null | undefined;
loading = true;
currentUser: User = new User();
error: ErrorInfo = {};
@@ -56,8 +57,13 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
@Output() reviewPageSizeChange = new EventEmitter();
@Input() reviewCurrentPage: number | undefined;
@Output() reviewCurrentPageChange = new EventEmitter();
+ @Input() reconcilePageSize: number | undefined;
+ @Output() reconcilePageSizeChange = new EventEmitter();
+ @Input() reconcileCurrentPage: number | undefined;
+ @Output() reconcileCurrentPageChange = new EventEmitter();
authTotalElements = 0;
reviewTotalElements = 0;
+ reconcileTotalElements = 0;
pageSizeOptions: number[] = [10, 25, 50, 100];
@Input() mapping: Mapping | undefined;
@Input() mappingTableSelector: MappingTableSelectorComponent | null | undefined;
@@ -112,16 +118,22 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
data => {
let authPage = data.find(taskPage => taskPage.type === TaskType.AUTHOR);
let reviewPage = data.find(taskPage => taskPage.type === TaskType.REVIEW);
+ let reconcilePage = data.find(taskPage => taskPage.type === TaskType.RECONCILE);
self.authorTasks = authPage?.page.tasks
.sort((a, b) => AssignedWorkComponent.sortTasks(a, b));
self.reviewTasks = reviewPage?.page.tasks
.sort((a, b) => AssignedWorkComponent.sortTasks(a, b));
+ self.reconcileTasks = reconcilePage?.page.tasks
+ .sort((a, b) => AssignedWorkComponent.sortTasks(a, b));
if (authPage?.page) {
self.authTotalElements = authPage.page.page.totalElements;
}
if (reviewPage?.page) {
self.reviewTotalElements = reviewPage.page.page.totalElements;
}
+ if (reconcilePage?.page) {
+ self.reconcileTotalElements = reconcilePage.page.page.totalElements;
+ }
self.loading = false;
self.setTab();
},
@@ -146,8 +158,10 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
this.authCurrentPage = event.pageIndex;
this.authPageSizeChange.emit(this.authPageSize);
this.authCurrentPageChange.emit(this.authCurrentPage);
- this.store.dispatch(new LoadTasksForMap({id: this.mapping?.id, authPageSize: this.authPageSize,
- authCurrentPage: this.authCurrentPage, reviewPageSize: this.reviewPageSize, reviewCurrentPage: this.reviewCurrentPage}));
+ this.store.dispatch(new LoadTasksForMap({id: this.mapping?.id,
+ authPageSize: this.authPageSize, authCurrentPage: this.authCurrentPage,
+ reviewPageSize: this.reviewPageSize, reviewCurrentPage: this.reviewCurrentPage,
+ reconcilePageSize: this.reconcilePageSize, reconcileCurrentPage: this.reconcileCurrentPage}));
}
reviewPageChanged(event: PageEvent): void {
@@ -155,8 +169,21 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
this.reviewCurrentPage = event.pageIndex;
this.reviewCurrentPageChange.emit(this.reviewCurrentPage);
this.reviewPageSizeChange.emit(this.reviewPageSize);
- this.store.dispatch(new LoadTasksForMap({id: this.mapping?.id, authPageSize: this.authPageSize,
- authCurrentPage: this.authCurrentPage, reviewPageSize: this.reviewPageSize, reviewCurrentPage: this.reviewCurrentPage}));
+ this.store.dispatch(new LoadTasksForMap({id: this.mapping?.id,
+ authPageSize: this.authPageSize, authCurrentPage: this.authCurrentPage,
+ reviewPageSize: this.reviewPageSize, reviewCurrentPage: this.reviewCurrentPage,
+ reconcilePageSize: this.reconcilePageSize, reconcileCurrentPage: this.reconcileCurrentPage}));
+ }
+
+ reconcilePageChanged(event: PageEvent): void {
+ this.reconcilePageSize = event.pageSize;
+ this.reconcileCurrentPage = event.pageIndex;
+ this.reconcileCurrentPageChange.emit(this.reconcileCurrentPage);
+ this.reconcilePageSizeChange.emit(this.reconcilePageSize);
+ this.store.dispatch(new LoadTasksForMap({id: this.mapping?.id,
+ authPageSize: this.authPageSize, authCurrentPage: this.authCurrentPage,
+ reviewPageSize: this.reviewPageSize, reviewCurrentPage: this.reviewCurrentPage,
+ reconcilePageSize: this.reconcilePageSize, reconcileCurrentPage: this.reconcileCurrentPage}));
}
setTab(): void {
@@ -166,6 +193,15 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
self.activeTab = 1;
break;
case TaskType.REVIEW:
+ if (this.mapping?.project.dualMapMode) {
+ self.activeTab = 3;
+ }
+ else {
+ self.activeTab = 2;
+ }
+
+ break;
+ case TaskType.RECONCILE:
self.activeTab = 2;
break;
default:
@@ -196,6 +232,9 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
case TaskType.REVIEW:
this.reviewCurrentPage = 0;
break;
+ case TaskType.RECONCILE:
+ this.reconcileCurrentPage = 0;
+ break;
}
this.updateCurrentTaskPage.emit(this.selectedTaskType);
this.updateTableEvent.emit(this.selectedTaskType);
@@ -211,6 +250,9 @@ export class AssignedWorkComponent implements OnInit, AfterViewInit, OnDestroy {
case 2:
this.selectedTaskType = TaskType.REVIEW;
break;
+ case 3:
+ this.selectedTaskType = TaskType.RECONCILE;
+ break;
default:
this.selectedTaskType = '';
}
diff --git a/ui/snapclient/src/app/task/task-add/task-add.component.ts b/ui/snapclient/src/app/task/task-add/task-add.component.ts
index 7100554d..f7d196d0 100644
--- a/ui/snapclient/src/app/task/task-add/task-add.component.ts
+++ b/ui/snapclient/src/app/task/task-add/task-add.component.ts
@@ -48,7 +48,7 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
error: ErrorInfo = {};
task: Task | undefined = undefined;
members: User[] = [];
- type_options = [TaskType.AUTHOR, TaskType.REVIEW];
+ type_options: TaskType[] = [];
row_options = ['ALL', 'SELECTED'];
assignRows = '';
isMember = false;
@@ -81,9 +81,12 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
self.subscription.add(self.store.select(selectSelectedRows).subscribe(
(selectedRows) => {
if (self.task) {
- if (selectedRows.length > 0) {
- const sourceIndexes = selectedRows.map(selected => selected.sourceIndex);
- self.task.sourceRowSpecification = ServiceUtils.convertNumberArrayToRangeString(sourceIndexes);
+ if (this.mappingTableSelector?.isAllSelected) {
+ self.task.sourceRowSpecification = '*';
+ }
+ else if (selectedRows.length > 0) {
+ const sourceIndexes = selectedRows.map(selected => selected.sourceIndex);
+ self.task.sourceRowSpecification = ServiceUtils.convertNumberArrayToRangeString(sourceIndexes);
} else {
self.assignRows = '';
self.task.sourceRowSpecification = '';
@@ -108,6 +111,7 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
self.mapping = mapping;
self.initTask();
self.loadMemberList();
+ self.initTaskTypeOptions(self.mapping);
}
}));
}
@@ -132,8 +136,13 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
updateSelectedRows(): void {
if (this.mappingTableSelector && this.mappingTableSelector.selectedRows && this.task) {
if (this.mappingTableSelector.selectedRows.length > 0) {
- const sourceIndexes = this.mappingTableSelector.selectedRows.map(selected => selected.sourceIndex);
- this.task.sourceRowSpecification = ServiceUtils.convertNumberArrayToRangeString(sourceIndexes);
+ if (this.mappingTableSelector.isAllSelected) {
+ this.task.sourceRowSpecification = '*';
+ }
+ else {
+ const sourceIndexes = this.mappingTableSelector.selectedRows.map(selected => selected.sourceIndex);
+ this.task.sourceRowSpecification = ServiceUtils.convertNumberArrayToRangeString(sourceIndexes);
+ }
} else {
this.assignRows = '';
this.task.sourceRowSpecification = '';
@@ -144,7 +153,7 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
initTask(): void {
this.assignRows = '';
if (this.mapping && this.currentUser?.id) {
- this.task = new Task('', '', '',
+ this.task = new Task('', TaskType.AUTHOR, '',
this.mapping, this.currentUser, '', 0, '', '', false, false);
}
}
@@ -160,6 +169,15 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
+ initTaskTypeOptions(mapping: Mapping): void {
+ if (mapping.project.dualMapMode) {
+ this.type_options = [TaskType.AUTHOR, TaskType.RECONCILE, TaskType.REVIEW];
+ }
+ else {
+ this.type_options = [TaskType.AUTHOR, TaskType.REVIEW];
+ }
+ }
+
updateDescription(dirty: boolean | null): void {
// Default description only if none entered
if (!dirty && this.task) {
@@ -183,10 +201,10 @@ export class TaskAddComponent implements OnInit, AfterViewInit, OnDestroy {
onSubmit(form: NgForm, $event: Event): void {
const self = this;
try {
- if (self.currentUser && self.task && self.task.type !== '' && form.form.valid) {
+ if (self.currentUser && self.task && form.form.valid) {
if (self.task.assignee && self.task.assignee.id !== '') {
self.store.dispatch(new AddTask(self.task));
- self.newTaskEvent.emit(self.task?.type);
+ self.newTaskEvent.emit(self.task.type);
self.mappingTableSelector?.clearAllSelectedRows();
} else {
throwError('TASK.ASSIGNEE_NOT_SET');
diff --git a/ui/snapclient/src/app/task/task-create/task-create.component.ts b/ui/snapclient/src/app/task/task-create/task-create.component.ts
index 21e861f3..519272b4 100644
--- a/ui/snapclient/src/app/task/task-create/task-create.component.ts
+++ b/ui/snapclient/src/app/task/task-create/task-create.component.ts
@@ -105,6 +105,9 @@ export class TaskCreateComponent implements OnInit {
let type = '';
switch (this.data.task.type) {
// Note Opposite offending task
+ case TaskType.RECONCILE:
+ this.translate.get('TASK.TYPE_RECONCILE').subscribe((msg) => type = msg);
+ break;
case TaskType.REVIEW:
this.translate.get('TASK.TYPE_AUTHOR').subscribe((msg) => type = msg);
break;
diff --git a/ui/snapclient/src/app/task/task-item/task-item.component.css b/ui/snapclient/src/app/task/task-item/task-item.component.css
index 45bb4623..55ba50f9 100644
--- a/ui/snapclient/src/app/task/task-item/task-item.component.css
+++ b/ui/snapclient/src/app/task/task-item/task-item.component.css
@@ -15,3 +15,6 @@
.task-item.REVIEW {
color: #043c36;
}
+.task-item.RECONCILE {
+ color: #3a0e30;
+}
diff --git a/ui/snapclient/src/app/task/task-item/task-item.component.html b/ui/snapclient/src/app/task/task-item/task-item.component.html
index 6c37c59d..f913ded4 100644
--- a/ui/snapclient/src/app/task/task-item/task-item.component.html
+++ b/ui/snapclient/src/app/task/task-item/task-item.component.html
@@ -1,6 +1,7 @@
edit
+ compare_arrows
checklist
{{task.description | trim}}
diff --git a/ui/snapclient/src/app/user/gravatar/gravatar.component.ts b/ui/snapclient/src/app/user/gravatar/gravatar.component.ts
index d425551e..691a03b1 100644
--- a/ui/snapclient/src/app/user/gravatar/gravatar.component.ts
+++ b/ui/snapclient/src/app/user/gravatar/gravatar.component.ts
@@ -17,6 +17,8 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import {Md5} from 'ts-md5';
+const SESSION_STORAGE_FAILED_GRAVATARS_KEY = "failedGravatars";
+
@Component({
selector: 'app-gravatar',
templateUrl: './gravatar.component.html',
@@ -93,6 +95,14 @@ export class GravatarComponent implements OnInit {
handleError(): void {
this.gravatar = false;
+
+ // remember that this.src has failed previously .. stored for a session only
+ let failedGravatarsArray = this.getFailedGravatarsArray();
+ if (failedGravatarsArray.indexOf(this.src) === -1) {
+ failedGravatarsArray.push(this.src);
+ sessionStorage.setItem(SESSION_STORAGE_FAILED_GRAVATARS_KEY, JSON.stringify(failedGravatarsArray));
+ }
+
}
updateGravatar(email?: string): void {
@@ -104,6 +114,22 @@ export class GravatarComponent implements OnInit {
}
const emailHash = Md5.hashStr(email.trim().toLowerCase());
this.src = `//www.gravatar.com/avatar/${emailHash}?${this.param}`;
+
+ // don't look up the image if it has failed previously
+ let failedGravatarsArray = this.getFailedGravatarsArray();
+ if (failedGravatarsArray.indexOf(this.src) > -1) {
+ this.gravatar = false;
+ }
+ }
+
+ getFailedGravatarsArray() : string[] {
+ let failedGravatarsStr = sessionStorage.getItem(SESSION_STORAGE_FAILED_GRAVATARS_KEY);
+ let failedGravatarsArray = [];
+ if (failedGravatarsStr) {
+ failedGravatarsArray = JSON.parse(failedGravatarsStr);
+ }
+
+ return failedGravatarsArray;
}
}
diff --git a/ui/snapclient/src/assets/i18n/en.json b/ui/snapclient/src/assets/i18n/en.json
index b5365090..1d4214ea 100644
--- a/ui/snapclient/src/assets/i18n/en.json
+++ b/ui/snapclient/src/assets/i18n/en.json
@@ -23,12 +23,17 @@
"DIALOG_NOTE_DELETE": "DELETE NOTE",
"DIALOG_NOTE_TITLE": "Delete Note",
"NOTES": "Notes",
+ "SYSTEM_NOTES": "System Notes",
"HAS_NOTES": "Source has notes",
"SOURCE_DETAILS": "Source details",
"TARGET_SEARCH_RESULTS": "Target search results",
"TARGET_BY_RELATIONSHIP": "Target by relationship",
"MAPPED_SOURCE_TO_TARGET": "Mapped Source to Target",
"TARGET_PROPERTIES": "Target Properties",
+ "OUT_OF_SCOPE_USE_AUTHOR_TASK_TO_FIND_REPLACEMENTS": "Target out of scope - use the AUTHOR task to find suggested replacements",
+ "OUT_OF_SCOPE_USE_AUTHOR_RECONCILE_TASK_TO_FIND_REPLACEMENTS": "Target out of scope - use the AUTHOR or RECONCILE task to find suggested replacements",
+ "OUT_OF_SCOPE_FIND_REPLACEMENTS": "Target out of scope - click to find suggested replacements",
+ "OUT_OF_SCOPE_NO_SUGGESTED_REPLACEMENTS": "Target out of scope - no active in scope replacements suggested",
"NOTE_DELETE": "Delete note",
"NOTE_TEXT": "Add your notes here",
"NOTE_TEXT_ERROR": "Notes text entry cannot start with a space",
@@ -94,7 +99,11 @@
"NOT_AUTHORIZED_RETURN": "Return to Home",
"EXPORT_FAILED": "Export Failed",
"EXISTING_VERSION": "A map with the same version already exists",
- "PROJECT_LOAD": "Project access may not be authorized"
+ "PROJECT_LOAD": "Project access may not be authorized",
+ "RECONCILE_NO_MAP_AND_TARGETS": "This map is not yet fully reconciled. No Map cannot be selected when targets are specified",
+ "RECONCILE_NO_NO_MAP_OR_TARGETS": "This map is not yet fully reconciled. Either No Map needs to be selected or targets should be specified",
+ "RECONCILE_SAME_TARGET_MULTIPLE_RELATIONSHIPS": "This map is not yet fully reconciled. The same target exists with multiple relationships",
+ "RECONCILE_DUPLICATE_TARGET": "This map is not yet fully reconciled. A target is duplicated"
},
"FORM": {
"CANCEL": "Cancel",
@@ -112,7 +121,8 @@
"VIEW": "VIEW",
"IMPORT_EXISTING_MAP": "Import existing map",
"SELECT_FILE": "Select file",
- "CLEAR_SELECTED_FILE": "Clear selected file"
+ "CLEAR_SELECTED_FILE": "Clear selected file",
+ "FHIR_METADATA": "Additional FHIR metadata (for FHIR ConceptMap export)"
},
"MAP": {
"BULK_CHANGE_TOOLTIP": "Perform bulk changes to selected rows",
@@ -136,8 +146,11 @@
"MEMBERS": "Members",
"OWNERS": "Owners",
"GUESTS": "Guests",
+ "DUAL_MAP": "dual map",
+ "SINGLE_MAP": "single map",
"MAP_EDIT_BUTTON": "EDIT",
"MAP_VIEW_BUTTON": "VIEW",
+ "NUM_TARGETS_OUT_OF_SCOPE": "Number targets out of scope",
"MAPPING": "MAPPING",
"MARK_MAPPED": "Mark all as MAPPED",
"MARK_ACCEPTED": "Mark all as ACCEPTED",
@@ -190,6 +203,8 @@
"EXPORT_CSV": "Comma separated",
"EXPORT_TSV": "Tab separated",
"EXPORT_XLSX": "Excel (xlsx)",
+ "EXPORT_FHIR_JSON": "FHIR (json)",
+ "EXPORT_XLSX_EXTENDED": "Excel (xlsx) extended",
"CODE_SYSTEMS": "Code systems",
"LAST_UPDATED": "Last updated",
"UNSAVED_CHANGES": "Warning: you have unsaved changes",
@@ -198,7 +213,10 @@
"FAILED_TO_IMPORT_MAPPING_FILE_ERROR": "Details of error: {{error}}",
"VALIDATE_TARGETS": "VALIDATE",
"VALIDATE_TARGETS_TOOLTIP": "Flag all rows where the target code is outside the target scope",
- "VALIDATE_TARGETS_ERROR": "Unexpected error occurred during validation of target scope"
+ "VALIDATE_TARGETS_ERROR": "Unexpected error occurred during validation of target scope",
+ "SINGLE_AUTHOR_MODE": "Single Author Mode",
+ "DUAL_AUTHOR_MODE": "Dual Author Mode",
+ "CLEARED_AND_REBLINDED_WARN": "Any rows undergoing dual mapping will be cleared and reblinded in the new version of a dual map"
},
"TABLE": {
"SOURCE_CODE": "Source code",
@@ -208,24 +226,30 @@
"TARGET_CODE": "Target code",
"TARGET_DISPLAY": "Target display",
"STATUS": "Status",
+ "TARGET_OUT_OF_SCOPE": "Tags",
+ "TARGET_OUT_OF_SCOPE_TOOLTIP": "Target out of scope",
"NO_MAP": "No map",
"ACTIONS": "Actions",
"DETAILS": "Details",
"FLAG": "Flag",
"NOTES": "Notes",
"AUTHOR": "Assigned author",
+ "RECONCILER": "Assigned reconciler",
"REVIEWER": "Assigned reviewer",
"BULK_CHANGE": "BULK EDIT",
+ "LAST_AUTHOR": "Last author",
"LAST_AUTHOR_REVIEWER": "Last author/ reviewer"
},
"TASK": {
"AUTHOR": "AUTHOR",
- "REVIEW": "REVIEW",
+ "REVIEW": "REVIEW",
+ "RECONCILE": "RECONCILE",
"ROWS_LABEL": "Source indexes:",
"MY_TASKS": "My tasks",
"ADD_TASK": "New Task",
"TAB_AUTHOR": "Author",
"TAB_REVIEW": "Review",
+ "TAB_RECONCILE": "Reconcile",
"ASSIGN": "Assign",
"ASSIGNED_WORK": "Assigned Tasks",
"ASSIGNEE_NOT_SET": "Assignee for task is not set",
@@ -236,6 +260,7 @@
"NOT_AVAILABLE": "You are not assigned to this task",
"TYPE_AUTHOR": "Author",
"TYPE_REVIEW": "Reviewer",
+ "TYPE_RECONCILE": "Reconciler",
"TYPE_UNKNOWN": "Unknown",
"TYPE_DUAL": "Dual Independent",
"DESCRIPTION": "Task Description",
@@ -254,10 +279,13 @@
"DEFAULT": "Map all rows",
"AUTHOR_TASK_FOR_ALL_ROWS": "Author task for all source terms",
"REVIEW_TASK_FOR_ALL_ROWS": "Review task for all source terms",
+ "RECONCILE_TASK_FOR_ALL_ROWS": "Reconcile task for all source terms",
"AUTHOR_TASK_FOR_SELECTED_ROWS": "Author task for selected source terms",
"REVIEW_TASK_FOR_SELECTED_ROWS": "Review task for selected source terms",
+ "RECONCILE_TASK_FOR_SELECTED_ROWS": "Reconcile task for selected source terms",
"AUTHOR_TASK_FOR__ROWS": "Author task",
"REVIEW_TASK_FOR__ROWS": "Review task",
+ "RECONCILE_TASK_FOR__ROWS": "Reconcile task",
"_TASK_FOR_ALL_ROWS": "Task for all source terms",
"_TASK_FOR_CUSTOM_ROWS": "Task for custom source term list",
"_TASK_FOR_SELECTED_ROWS": "Task for selected source terms",
@@ -313,6 +341,10 @@
"NAME_DUPLICATE": "Source name and version combination must be unique",
"NAME_ERROR": "A source name is required",
"NAME_PLACEHOLDER": "Provide a title or brief description",
+ "CODESYSTEM": "CodeSystem URI",
+ "CODESYSTEM_PLACEHOLDER": "The URI that identifies the CodeSystem containing these codes",
+ "VALUESET": "ValueSet URI",
+ "VALUESET_PLACEHOLDER": "The URI that identifies the ValueSet consisting of these codes",
"NO_FILE": "No file selected",
"SELECT_COLUMNS": "We have selected columns for source code and display to be used for mapping. Please check these are correct as they cannot be changed later.",
"SOURCE": "Source",
@@ -341,6 +373,7 @@
"DRAFT": "DRAFT",
"MAPPED": "MAPPED",
"INREVIEW": "IN REVIEW",
+ "RECONCILE": "RECONCILE",
"ACCEPTED": "ACCEPTED",
"REJECTED": "REJECTED",
"SWITCH": "Switch to "
@@ -414,6 +447,10 @@
"NO_MATCHES": "No search results",
"INPUT_LABEL": "Text search"
},
+ "PROPERTIES": {
+ "TARGET_PROPERTIES": "Target Properties",
+ "ATTRIBUTE_RELATIONSHIPS": "Attribute Relationships"
+ },
"AUTOMAP": {
"AUTOMAP": "AUTOMAP",
"AUTOMAP_RUNNING": "Running automap",
@@ -435,6 +472,7 @@
"CANNOT_OPEN_PROJECT_NO_ROLE": "Request a role from a project owner to open this project",
"DELETE": "DELETE MAP",
"DELETE_TOOLTIP": "Delete this map",
+ "DELETE_NOTE_TOOLTIP": "Delete this note",
"TITLE_CONFIRM": "Delete Map",
"CANCEL": "CANCEL",
"CONFIRM_DELETE": "Are you sure you wish to delete this map? This will delete all versions of the map. Any mapping that has been performed will be removed and this cannot be undone."
@@ -463,7 +501,9 @@
"ERROR_DEFAULT_MESSAGE": "Unexpected error occured during the Bulk change operation.",
"SELECTED_TARGET": "Selected target:",
"UPDATED_ROWS_MESSAGE": "{{updated}} out of {{selected}} rows have been updated.",
- "UPDATED_ROWS_ERROR_MESSAGE": "{{updated}} out of {{selected}} rows have been updated. {{diff}} row(s) unable to be changed due to invalid status or relationship changes."
+ "UPDATED_ROWS_ERROR_MESSAGE": "{{updated}} out of {{selected}} rows have been updated. {{diff}} row(s) unable to be changed due to invalid status or relationship changes.",
+ "REDO_DUAL_MAPPING": "Clears targets and enables row to be dual mapped again",
+ "RECONCILE_STATUS_INFO": "To update any rows in a MAPPED, ACCEPTED or REJECTED state, change the row status to RECONCILE, then alter the row via the RECONCILE TASK"
},
"NOTESDIALOG": {
@@ -485,5 +525,14 @@
"IMPORT_SELECT_COLUMNS": "We have selected columns for import where possible. Please check these are correct as they cannot be changed later.",
"IMPORT_HAS_HEADER": "The imported file has a header row",
"IMPORT_COL_NOT_SPECIFIED": "Not Specified"
+ },
+
+ "FOOTER" : {
+ "COPYRIGHT_SNOMED_INTERNATIONAL": "Copyright 2023 SNOMED International",
+ "SNOMED_INTERNATIONAL": "SNOMED International",
+ "TERMS_OF_SERVICE": "Terms Of Service",
+ "PRIVACY_POLICY": "Privacy Policy",
+ "FEEDBACK": "Feedback",
+ "USER_GUIDE": "User Guide"
}
}