diff --git a/js/components/atlas-state.js b/js/components/atlas-state.js
index 5f40c6437..88042e3bf 100644
--- a/js/components/atlas-state.js
+++ b/js/components/atlas-state.js
@@ -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(() => {
diff --git a/js/components/conceptAddBox/concept-add-box.js b/js/components/conceptAddBox/concept-add-box.js
index d0ea0fab3..ea4b92aea 100644
--- a/js/components/conceptAddBox/concept-add-box.js
+++ b/js/components/conceptAddBox/concept-add-box.js
@@ -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()) {
diff --git a/js/components/conceptset/const.js b/js/components/conceptset/const.js
index e8e6292ff..9a0954832 100644
--- a/js/components/conceptset/const.js
+++ b/js/components/conceptset/const.js
@@ -14,6 +14,7 @@ define([
RECOMMEND: 'recommend',
EXPORT: 'conceptset-export',
IMPORT: 'conceptset-import',
+ ANNOTATION: 'annotation'
};
const ConceptSetSources = {
diff --git a/js/components/faceted-datatable.js b/js/components/faceted-datatable.js
index 74f963074..a9ba55cd2 100644
--- a/js/components/faceted-datatable.js
+++ b/js/components/faceted-datatable.js
@@ -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()) {
diff --git a/js/pages/concept-sets/components/tabs/conceptset-annotation.html b/js/pages/concept-sets/components/tabs/conceptset-annotation.html
new file mode 100644
index 000000000..17b93b858
--- /dev/null
+++ b/js/pages/concept-sets/components/tabs/conceptset-annotation.html
@@ -0,0 +1,15 @@
+
${r.searchData}
` + } + }, + 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 `${r.vocabularyVersion}
` + } + }, + 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 `${r.conceptSetVersion}
` + } + }, + 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 `${r.createdBy}
` + } + }, + 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 `${r.createdDate}
` + } + }, + 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 `${r.copiedFromConceptSetIds}
` + } + }, + sortable: false + } + ]; + + if (this.canDeleteAnnotations()) { + cols.push({ + title: ko.i18n('columns.action', 'Action'), + sortable: false, + render: function () { + return ``; + } + }); + } + 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); +}); \ No newline at end of file diff --git a/js/pages/concept-sets/components/tabs/conceptset-annotation.less b/js/pages/concept-sets/components/tabs/conceptset-annotation.less new file mode 100644 index 000000000..47164968a --- /dev/null +++ b/js/pages/concept-sets/components/tabs/conceptset-annotation.less @@ -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; +} \ No newline at end of file diff --git a/js/pages/concept-sets/components/tabs/conceptset-expression.js b/js/pages/concept-sets/components/tabs/conceptset-expression.js index 992d03779..e827124fb 100644 --- a/js/pages/concept-sets/components/tabs/conceptset-expression.js +++ b/js/pages/concept-sets/components/tabs/conceptset-expression.js @@ -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() }))); @@ -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) { diff --git a/js/pages/concept-sets/conceptset-manager.js b/js/pages/concept-sets/conceptset-manager.js index 7402046ed..d58dcd853 100644 --- a/js/pages/concept-sets/conceptset-manager.js +++ b/js/pages/concept-sets/conceptset-manager.js @@ -44,7 +44,8 @@ define([ 'components/authorship', 'components/name-validation', 'components/ac-access-denied', - 'components/versions/versions' + 'components/versions/versions', + './components/tabs/conceptset-annotation' ], function ( ko, view, @@ -147,6 +148,14 @@ define([ } return this.conceptSetStore.current() && authApi.isPermittedDeleteConceptset(this.conceptSetStore.current().id); }); + + this.canDeleteAnnotations = ko.pureComputed(() => { + if (!config.userAuthenticationEnabled) { + return true; + } + return this.conceptSetStore.current() && authApi.isPermittedConceptSetAnnotationsDelete(this.conceptSetStore.current().id); + }); + this.canOptimize = ko.computed(() => { return ( this.currentConceptSet() @@ -313,6 +322,16 @@ define([ }, hidden: () => !!this.previewVersion() }, + { + title: ko.i18n('cs.manager.tabs.annotation', 'Annotation'), + key: ViewMode.ANNOTATION, + componentName: 'conceptset-annotation', + componentParams: { + getList: () => this.currentConceptSet().id ? conceptSetService.getConceptSetAnnotation(this.currentConceptSet().id) : [], + delete: (annotationId) => annotationId ? conceptSetService.deleteConceptSetAnnotation(this.currentConceptSet().id, annotationId) : null, + canDeleteAnnotations: this.canDeleteAnnotations, + } + }, { title: ko.i18n('cs.manager.tabs.versions', 'Versions'), key: ViewMode.VERSIONS, @@ -331,6 +350,7 @@ define([ this.selectedTab = ko.observable(0); this.activeUtility = ko.observable(""); + this.newConceptSetIdForCopyAnnotations = ko.observable(0); GlobalPermissionService.decorateComponent(this, { entityTypeGetter: () => entityType.CONCEPT_SET, @@ -469,6 +489,33 @@ define([ this.conceptSetCaption.dispose(); } + removeDataFilterStorage(){ + localStorage.removeItem('filter-data'); + localStorage.removeItem('filter-source'); + localStorage.removeItem('data-remove-selected-concept'); + localStorage.removeItem('data-add-selected-concept'); + } + + objectMap(obj) { + const newObject = {}; + Object.keys(obj).forEach((key) => { + if(typeof obj[key] === 'object'){ + newObject[key] = JSON.stringify(obj[key]); + }else{ + newObject[key] = obj[key]; + } + }); + return newObject; + } + + handleConvertDataToString(arr){ + const newDatas = []; + (arr || []).forEach(item => { + newDatas.push(this.objectMap(item)) + }) + return newDatas; + } + async saveConceptSet(conceptSet, nameElementId) { if (this.previewVersion() && !confirm(ko.i18n('common.savePreviewWarning', 'Save as current version?')())) { return; @@ -487,11 +534,24 @@ define([ this.raiseConceptSetNameProblem(ko.i18n('cs.manager.csAlreadyExistsMessage', 'A concept set with this name already exists. Please choose a different name.')(), nameElementId); } else { const savedConceptSet = await conceptSetService.saveConceptSet(conceptSet); + const savedVersions = await this.versionsParams()?.getList(); + let latestSavedVersion = 1; + + if (savedVersions && Array.isArray(savedVersions)) { + latestSavedVersion = savedVersions.reduce((max, obj) => Math.max(max, obj.version), 1); + } + + let annotationDataToAdd = JSON.parse(localStorage?.getItem('data-add-selected-concept') || null) || []; + const enrichedAnnotationDataToAdd = annotationDataToAdd.map(item => ({...item, "conceptSetVersion": latestSavedVersion})); + await conceptSetService.saveConceptSetItems(savedConceptSet.data.id, conceptSetItems); + await conceptSetService.saveConceptSetAnnotation(savedConceptSet.data.id, { newAnnotation: this.handleConvertDataToString(enrichedAnnotationDataToAdd), removeAnnotation: this.handleConvertDataToString(JSON.parse(localStorage?.getItem('data-remove-selected-concept') || null) || [])}); + this.removeDataFilterStorage(); const current = this.conceptSetStore.current(); current.modifiedBy = savedConceptSet.data.modifiedBy; current.modifiedDate = savedConceptSet.data.modifiedDate; + this.newConceptSetIdForCopyAnnotations(savedConceptSet.data.id); this.conceptSetStore.current(current); this.previewVersion(null); @@ -533,11 +593,17 @@ define([ } async copy() { + let sourceConceptSetId = this.currentConceptSet().id; const responseWithName = await conceptSetService.getCopyName(this.currentConceptSet().id); this.currentConceptSet().name(responseWithName.copyName); this.currentConceptSet().id = 0; this.currentConceptSetDirtyFlag().reset(); await this.saveConceptSet(this.currentConceptSet(), "#txtConceptSetName"); + let copyAnnotationsRequest = { + sourceConceptSetId: sourceConceptSetId, + targetConceptSetId: this.newConceptSetIdForCopyAnnotations(), + }; + await conceptSetService.copyAnnotations(copyAnnotationsRequest); } async optimize() { diff --git a/js/pages/concept-sets/const.js b/js/pages/concept-sets/const.js index 87b99eb2b..f54eec991 100644 --- a/js/pages/concept-sets/const.js +++ b/js/pages/concept-sets/const.js @@ -10,6 +10,7 @@ define( RECOMMEND: conceptSetConstants.ViewMode.RECOMMEND, EXPORT: conceptSetConstants.ViewMode.EXPORT, IMPORT: conceptSetConstants.ViewMode.IMPORT, + ANNOTATION: conceptSetConstants.ViewMode.ANNOTATION, EXPLORE: 'explore', COMPARE: 'compare', VERSIONS: 'versions', diff --git a/js/pages/configuration/configuration.js b/js/pages/configuration/configuration.js index 5d5aadbb4..6aa4919d7 100644 --- a/js/pages/configuration/configuration.js +++ b/js/pages/configuration/configuration.js @@ -276,14 +276,32 @@ define([ this.isInProgress(false); } + async updateCurrentVocabularyVersion(sourceKey) { + try { + const result = await sourceApi.getVocabularyInfo(sourceKey); + if (result && result.data && result.data.version != null) { + sharedState.currentVocabularyVersion(result.data.version); + return result.data.version; + } else { + throw new Error('Vocabulary info response does not contain version'); + } + } catch (err) { + alert(ko.unwrap(ko.i18n('configuration.alerts.failUpdateCurrentVocabVersion', 'Failed to update current vocabulary version'))); + } + } + updateVocabPriority() { var newVocabUrl = sharedState.vocabularyUrl(); + var newCurrentVocabularyVersion = sharedState.currentVocabularyVersion(); var selectedSource = sharedState.sources().find((item) => { return item.vocabularyUrl === newVocabUrl; }); sharedState.priorityScope() === 'application' && - sharedState.defaultVocabularyUrl(newVocabUrl); + sharedState.defaultVocabularyUrl(newVocabUrl) && + sharedState.defaultVocabularyVersion(newCurrentVocabularyVersion); + this.updateSourceDaimonPriority(selectedSource.sourceKey, 'Vocabulary'); + this.updateCurrentVocabularyVersion(selectedSource.sourceKey); return true; } diff --git a/js/pages/vocabulary/components/search.js b/js/pages/vocabulary/components/search.js index 1f17c040d..25a1203ee 100644 --- a/js/pages/vocabulary/components/search.js +++ b/js/pages/vocabulary/components/search.js @@ -369,6 +369,15 @@ define([ } async executeSearch() { + const filterObjString = localStorage.getItem('filter-data') + let filterObj = filterObjString ? JSON.parse(filterObjString): {} + + filterObj = { + ...filterObj, + searchText: this.currentSearch() + } + localStorage.setItem('filter-data', JSON.stringify(filterObj)) + if (!this.currentSearch() && !this.showAdvanced()) { this.data([]); return; diff --git a/js/services/AuthAPI.js b/js/services/AuthAPI.js index cd88bbb32..082377ba2 100644 --- a/js/services/AuthAPI.js +++ b/js/services/AuthAPI.js @@ -506,6 +506,10 @@ define(function(require, exports) { return isPermitted(`tag:management`); }; + const isPermittedConceptSetAnnotationsDelete = function (conceptSetId) { + return isPermitted('conceptset:' + conceptSetId + ':annotation:*:delete'); + }; + const isPermittedRunAs = () => isPermitted('user:runas:post'); const isPermittedViewDataSourceReport = sourceKey => isPermitted(`cdmresults:${sourceKey}:*:get`); @@ -634,6 +638,8 @@ define(function(require, exports) { isPermittedViewDataSourceReport, isPermittedViewDataSourceReportDetails, + isPermittedConceptSetAnnotationsDelete, + loadUserInfo, TOKEN_HEADER, runAs, diff --git a/js/services/ConceptSet.js b/js/services/ConceptSet.js index 435d050db..77e290942 100644 --- a/js/services/ConceptSet.js +++ b/js/services/ConceptSet.js @@ -74,6 +74,21 @@ define(function (require) { .catch(authApi.handleAccessDenied); } + function saveConceptSetAnnotation(id, conceptSetItems) { + return httpService.doPut(config.api.url + 'conceptset/' + id + '/annotation', conceptSetItems) + .catch(authApi.handleAccessDenied); + } + + function getConceptSetAnnotation(conceptSetId) { + return httpService.doGet(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1') + '/annotation') + .catch(authApi.handleAccessDenied); + } + + function deleteConceptSetAnnotation(conceptSetId, annotationId) { + return httpService.doDelete(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1') +'/annotation/' + (annotationId || '-1')) + .catch(authApi.handleAccessDenied); + } + function getConceptSet(conceptSetId) { return httpService.doGet(config.webAPIRoot + 'conceptset/' + (conceptSetId || '-1')) .catch(authApi.handleAccessDenied); @@ -84,6 +99,12 @@ define(function (require) { .then(({ data }) => data); } + function copyAnnotations(copyAnnotationsRequest) { + return httpService + .doPost(`${config.webAPIRoot}conceptset/copy-annotations`, copyAnnotationsRequest) + .then(({ data }) => data); + } + function runDiagnostics(conceptSet) { return httpService .doPost(`${config.webAPIRoot}conceptset/check`, conceptSet) @@ -129,6 +150,7 @@ define(function (require) { lookupIdentifiers, getInclusionCount, getCopyName, + copyAnnotations, getConceptSet, getGenerationInfo, deleteConceptSet, @@ -140,7 +162,10 @@ define(function (require) { getVersion, getVersionExpression, updateVersion, - copyVersion + copyVersion, + saveConceptSetAnnotation, + getConceptSetAnnotation, + deleteConceptSetAnnotation }; return api; diff --git a/js/services/SourceAPI.js b/js/services/SourceAPI.js index 6290fa504..d52e14a04 100644 --- a/js/services/SourceAPI.js +++ b/js/services/SourceAPI.js @@ -161,6 +161,7 @@ define(function (require, exports) { success: function (info) { source.version(info.version); source.dialect(info.dialect); + sharedState.currentVocabularyVersion() || sharedState.defaultVocabularyVersion(info.version); }, error: function (err) { source.version('unknown'); @@ -209,6 +210,9 @@ define(function (require, exports) { function getResultsUrl(sourceKey) { return config.api.url + 'cdmresults/' + sourceKey + '/'; } + function getVocabularyInfo(sourceKey) { + return httpService.doGet(config.webAPIRoot + 'vocabulary/' + sourceKey + '/info'); + } var api = { getSources: getSources, @@ -222,6 +226,7 @@ define(function (require, exports) { buttonCheckState: buttonCheckState, setSharedStateSources: setSharedStateSources, updateSourceDaimonPriority, + getVocabularyInfo }; return api;