Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending a Concept Set with a new tab 'Annotations' #2971

Merged
merged 15 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions js/components/atlas-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,28 @@ define(['knockout', 'lscache', 'services/job/jobDetail', 'assets/ohdsi.util', 'c
state.vocabularyUrl = ko.observable(sessionStorage.vocabularyUrl);
state.evidenceUrl = ko.observable(sessionStorage.evidenceUrl);
state.resultsUrl = ko.observable(sessionStorage.resultsUrl);
state.currentVocabularyVersion = ko.observable(sessionStorage.currentVocabularyVersion);
state.vocabularyUrl.subscribe(value => updateKey('vocabularyUrl', value));
state.evidenceUrl.subscribe(value => updateKey('evidenceUrl', value));
state.resultsUrl.subscribe(value => updateKey('resultsUrl', value));
state.currentVocabularyVersion.subscribe(value => updateKey('currentVocabularyVersion', value));

// This default values are stored during initialization
// and used to reset after session finished
state.defaultVocabularyUrl = ko.observable();
state.defaultEvidenceUrl = ko.observable();
state.defaultResultsUrl = ko.observable();
state.defaultVocabularyVersion = ko.observable();
state.defaultVocabularyUrl.subscribe((value) => state.vocabularyUrl(value));
state.defaultEvidenceUrl.subscribe((value) => state.evidenceUrl(value));
state.defaultResultsUrl.subscribe((value) => state.resultsUrl(value));
state.defaultVocabularyVersion.subscribe((value) => state.currentVocabularyVersion(value));

state.resetCurrentDataSourceScope = function() {
state.vocabularyUrl(state.defaultVocabularyUrl());
state.evidenceUrl(state.defaultEvidenceUrl());
state.resultsUrl(state.defaultResultsUrl());
state.currentVocabularyVersion(state.defaultVocabularyVersion());
}

state.sourceKeyOfVocabUrl = ko.computed(() => {
Expand Down
14 changes: 14 additions & 0 deletions js/components/conceptAddBox/concept-add-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ define([

sharedState.activeConceptSet(conceptSet);

const filterSource = localStorage?.getItem('filter-source') || null;
const filterData = JSON.parse(localStorage?.getItem('filter-data') || null);
const datasAdded = JSON.parse(localStorage?.getItem('data-add-selected-concept') || null) || [];
const dataSearch = { filterData, filterSource }
const payloadAdd = this.conceptsToAdd().map(item => {
return {
"searchData": dataSearch,
"vocabularyVersion": sharedState.currentVocabularyVersion(),
"conceptId": item.CONCEPT_ID
}
})

localStorage.setItem('data-add-selected-concept', JSON.stringify([...datasAdded, ...payloadAdd]))

// if concepts were previewed, then they already built and can have individual option flags!
if (this.previewConcepts().length > 0) {
if (!conceptSet.current()) {
Expand Down
1 change: 1 addition & 0 deletions js/components/conceptset/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ define([
RECOMMEND: 'recommend',
EXPORT: 'conceptset-export',
IMPORT: 'conceptset-import',
ANNOTATION: 'annotation'
};

const ConceptSetSources = {
Expand Down
45 changes: 45 additions & 0 deletions js/components/faceted-datatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,52 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'utils/Commo

self.outsideFilters = (params.outsideFilters || ko.observable()).extend({notify: 'always'});

self.setDataLocalStorage = (data, nameItem) => {
const filterArrayString = localStorage.getItem(nameItem)
let filterArrayObj = filterArrayString? JSON.parse(filterArrayString): []

if(!data?.selected()){
filterArrayObj.push({title:data.facet.caption(), value:`${data.key} (${data.value})`,key:data.key})
}else{
filterArrayObj = filterArrayObj.filter((item)=> item.key !== data.key)
}
localStorage.setItem(nameItem, JSON.stringify(filterArrayObj))
}

self.setDataObjectLocalStorage = (data, nameItem) => {
const filterObjString = localStorage.getItem(nameItem)
let filterObj = filterObjString ? JSON.parse(filterObjString): {}
let newFilterObj = {}

if(!data?.selected()){
const dataPush = { title: data.facet.caption(), value: `${data.key} (${data.value})`, key: data.key };
newFilterObj.filterColumns = filterObj['filterColumns'] ? [...filterObj['filterColumns'], dataPush] : [dataPush]
newFilterObj = { ...filterObj, filterColumns : newFilterObj.filterColumns };
}else{
newFilterObj.filterColumns = filterObj['filterColumns'].filter((item)=> item.key !== data.key);
newFilterObj = { ...filterObj, filterColumns : newFilterObj.filterColumns };
}
localStorage.setItem(nameItem, JSON.stringify(newFilterObj))
}

self.updateFilters = function (data, event) {
const currentPath = window.location?.href;
if (currentPath?.includes('/conceptset/')) {
if (currentPath?.includes('/included-sourcecodes')) {
localStorage.setItem('filter-source', 'Included Source Codes');
} else if (currentPath?.includes('/included')) {
localStorage.setItem('filter-source', 'Included Concepts');
}
self.setDataLocalStorage(data, 'filter-data');
}
const isAddConcept = currentPath?.split('?').reduce((prev, curr) => prev || curr.includes('search'), false) &&
currentPath?.split('?').reduce((prev, curr) => prev || curr.includes('query'), false) ||
currentPath?.includes('/concept/')

if (isAddConcept) {
localStorage.setItem('filter-source', 'Search');
self.setDataObjectLocalStorage(data, 'filter-data')
}
var facet = data.facet;
data.selected(!data.selected());
if (data.selected()) {
Expand Down
15 changes: 15 additions & 0 deletions js/pages/concept-sets/components/tabs/conceptset-annotation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<loading data-bind="visible: isLoading()" params="status: ko.i18n('components.annotation.loading', 'Loading annotation...')"></loading>
<div data-bind="visible: !isLoading()">
<faceted-datatable params="
order: [],
autoWidth: false,
reference: data,
columns: columns,
options: null,
pageLength: pageLength,
lengthMenu: lengthMenu,
rowClick: onRowClick,
language: ko.i18n('datatable.language')
">
</faceted-datatable>
</div>
175 changes: 175 additions & 0 deletions js/pages/concept-sets/components/tabs/conceptset-annotation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
define([
'knockout',
'text!./conceptset-annotation.html',
'components/Component',
'utils/AutoBind',
'utils/CommonUtils',
'services/AuthAPI',
'faceted-datatable',
'less!./conceptset-annotation.less',
], function (
ko,
view,
Component,
AutoBind,
commonUtils,
authApi,
) {
class ConceptsetAnnotation extends AutoBind(Component) {
constructor(params) {
super(params);
this.isLoading = ko.observable(true);
this.data = ko.observable();
this.getList = params.getList;
this.delete = params.delete;
this.canDeleteAnnotations = params.canDeleteAnnotations;

const { pageLength, lengthMenu } = commonUtils.getTableOptions('M');
this.pageLength = params.pageLength || pageLength;
this.lengthMenu = params.lengthMenu || lengthMenu;

this.columns = ko.computed(() => {
let cols = [
{
title: ko.i18n('columns.conceptID', 'Concept Id'),
data: 'conceptId',
},
{
title: ko.i18n('columns.searchData', 'Search Data'),
className: this.classes('tbl-col', 'search-data'),
render: (d, t, r) => {
if (r.searchData === null || r.searchData === undefined || !r.searchData) {
return 'N/A';
} else {
return `<p>${r.searchData}</p>`
}
},
sortable: false
},
{
title: ko.i18n('columns.vocabularyVersion', 'Vocabulary Version'),
data: 'vocabularyVersion',
render: (d, t, r) => {
if (r.vocabularyVersion === null || r.vocabularyVersion === undefined || !r.vocabularyVersion) {
return 'N/A';
} else {
return `<p>${r.vocabularyVersion}</p>`
}
},
sortable: false
},
{
title: ko.i18n('columns.conceptSetVersion', 'Concept Set Version'),
data: 'conceptSetVersion',
render: (d, t, r) => {
if (r.conceptSetVersion === null || r.conceptSetVersion === undefined || !r.conceptSetVersion) {
return 'N/A';
} else {
return `<p>${r.conceptSetVersion}</p>`
}
},
sortable: false
},
{
title: ko.i18n('columns.createdBy', 'Created By'),
data: 'createdBy',
render: (d, t, r) => {
if (r.createdBy === null || r.createdBy === undefined || !r.createdBy) {
return 'N/A';
} else {
return `<p>${r.createdBy}</p>`
}
},
sortable: false
},
{
title: ko.i18n('columns.createdDate', 'Created Date'),
render: (d, t, r) => {
if (r.createdDate === null || r.createdDate === undefined) {
return 'N/A';
} else {
return `<p>${r.createdDate}</p>`
}
},
sortable: false
},
{
title: ko.i18n('columns.originConceptSets', 'Origin Concept Sets'),
render: (d, t, r) => {
if (r.copiedFromConceptSetIds === null || r.copiedFromConceptSetIds === undefined) {
return 'N/A';
} else {
return `<p>${r.copiedFromConceptSetIds}</p>`
}
},
sortable: false
}
];

if (this.canDeleteAnnotations()) {
cols.push({
title: ko.i18n('columns.action', 'Action'),
sortable: false,
render: function () {
return `<i class="deleteIcon fa fa-trash" aria-hidden="true"></i>`;
}
});
}
return cols;
});

this.loadData();
}

objectMap(obj) {
const newObject = {};
const keysNotToParse = ['createdBy', 'createdDate', 'vocabularyVersion', 'conceptSetVersion', 'copiedFromConceptSetIds', 'searchData'];
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string' && !keysNotToParse.includes(key)) {
newObject[key] = JSON.parse(obj[key] || null);
} else {
newObject[key] = obj[key];
}
});
return newObject;
}

async onRowClick(d, e){
try {
const { id } = d;
if(e.target.className === 'deleteIcon fa fa-trash') {
const res = await this.delete(id);
if(res){
this.loadData();
}
}
} catch (ex) {
console.log(ex);
} finally {
this.isLoading(false);
}
}

handleConvertData(arr){
const newDatas = [];
(arr || []).forEach(item => {
newDatas.push(this.objectMap(item))
})
return newDatas;
}

async loadData() {
this.isLoading(true);
try {
const data = await this.getList();
this.data(this.handleConvertData(data.data));
} catch (ex) {
console.log(ex);
} finally {
this.isLoading(false);
}
}

}
return commonUtils.build('conceptset-annotation', ConceptsetAnnotation, view);
});
23 changes: 23 additions & 0 deletions js/pages/concept-sets/components/tabs/conceptset-annotation.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.conceptset-annotation {

&__tbl-col {
&--search-data {
min-width: 40%;
}
&--concept-data{
max-width: 500px;
text-overflow: ellipsis;
white-space: nowrap;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}

.deleteIcon {
color: #d9534f;
cursor: pointer;
min-width: 30px;
}
30 changes: 29 additions & 1 deletion js/pages/concept-sets/components/tabs/conceptset-expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ define([
});

this.datatableLanguage = ko.i18n('datatable.language');
this.currentConceptSetId = ko.observable(params.router.routerParams().conceptSetId);

this.data = ko.pureComputed(() => this.conceptSetItems().map((item, idx) => ({ ...item, idx, isSelected: ko.observable() })));

Expand Down Expand Up @@ -113,7 +114,34 @@ define([

removeConceptsFromConceptSet() {
const idxForRemoval = this.data().filter(concept => concept.isSelected()).map(item => item.idx);
this.conceptSetStore.removeItemsByIndex(idxForRemoval);

const removeItems = this.data().filter(concept => concept.isSelected());
const datasAdded = JSON.parse(localStorage.getItem('data-add-selected-concept') || null) || [];
const datasDeleted = JSON.parse(localStorage.getItem('data-remove-selected-concept') || null) || [];

const datasRemove = [];
const payloadRemove = removeItems.map(item => {
if((datasAdded.map(item => item.conceptId)).includes(item.concept.CONCEPT_ID)){
datasRemove.push(item.concept.CONCEPT_ID);
return null;
}
return {
"searchData": "",
"relatedConcepts": "",
"conceptHierarchy": "",
"conceptSetData": { id: this.currentConceptSetId(), name: this.conceptSetStore.current().name()},
"conceptData": item,
"conceptId": item.concept.CONCEPT_ID
}
});

const dataRemoveSelected = [...datasDeleted, ...payloadRemove].filter((item, i, arr) => item && arr.indexOf(item) === i);
localStorage.setItem('data-remove-selected-concept', JSON.stringify(dataRemoveSelected));
if(datasRemove?.length){
const newAddDatas = datasAdded.filter(data => !datasRemove.includes(data.conceptId));
localStorage.setItem('data-add-selected-concept', JSON.stringify(newAddDatas));
}
this.conceptSetStore.removeItemsByIndex(idxForRemoval);
}

async selectAllConceptSetItems(key, areAllSelected) {
Expand Down
Loading
Loading