Skip to content

Commit 811e771

Browse files
committed
Concept Set Snapshots/Locking/Unlocking feature implementation
1 parent d762e9a commit 811e771

14 files changed

+906
-13
lines changed

js/components/circe/components/ConceptSetBrowser.js

+41-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ define([
77
'services/AuthAPI',
88
'utils/DatatableUtils',
99
'utils/CommonUtils',
10+
'services/ConceptSet',
1011
'components/ac-access-denied',
1112
'databindings',
1213
'css!./style.css'
13-
], function (ko, template, VocabularyProvider, appConfig, ConceptSet, authApi, datatableUtils, commonUtils) {
14+
], function (ko, template, VocabularyProvider, appConfig, ConceptSet, authApi, datatableUtils, commonUtils, conceptSetService) {
1415
function CohortConceptSetBrowser(params) {
1516
var self = this;
1617

@@ -96,17 +97,42 @@ define([
9697

9798
self.loadConceptSetsFromRepository = function (url) {
9899
self.loading(true);
99-
100+
100101
VocabularyProvider.getConceptSetList(url)
101102
.done(function (results) {
102103
datatableUtils.coalesceField(results, 'modifiedDate', 'createdDate');
103104
datatableUtils.addTagGroupsToFacets(results, self.options.Facets);
104-
datatableUtils.addTagGroupsToColumns(results, self.columns);
105-
self.repositoryConceptSets(results);
106-
self.loading(false);
105+
106+
// Extract IDs for batch locked status check
107+
const conceptSetIds = results.map(cs => cs.id);
108+
const isLockedBatchCheckRequest = {
109+
conceptSetIds: conceptSetIds
110+
};
111+
conceptSetService.getLockedStatusesForConceptSets(isLockedBatchCheckRequest).then(response => {
112+
const lockStatusMap = response.data.lockStatus;
113+
// Apply the locking status to each ConceptSet
114+
results.forEach(conceptSet => {
115+
conceptSet.isLocked = lockStatusMap[conceptSet.id] || false;
116+
});
117+
// Update the observable array with locked statuses
118+
self.repositoryConceptSets(results);
119+
datatableUtils.addTagGroupsToColumns(results, self.columns);
120+
self.loading(false);
121+
}).catch(error => {
122+
console.error('Error while batch-fetching locked statuses for concept sets', error);
123+
// Defaulting isLocked to false in case of error
124+
results.forEach(conceptSet => {
125+
conceptSet.isLocked = false;
126+
});
127+
self.repositoryConceptSets(results);
128+
datatableUtils.addTagGroupsToColumns(results, self.columns);
129+
self.loading(false);
130+
});
131+
107132
})
108133
.fail(function (err) {
109-
console.log(err);
134+
console.log('Error fetching concept sets:', err);
135+
self.loading(false);
110136
});
111137
}
112138

@@ -150,6 +176,15 @@ define([
150176
};
151177

152178
this.columns = ko.observableArray([
179+
{
180+
title: '',
181+
data: 'isLocked',
182+
sortable: false,
183+
render: function (data, type, row) {
184+
return data ? '<span class="fa fa-lock" aria-hidden="true"></span>' : '';
185+
},
186+
width: '20px', // Fixed width for the lock icon column
187+
},
153188
{
154189
title: ko.i18n('columns.id', 'Id'),
155190
data: 'id'

js/components/conceptset/const.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ define([
1414
RECOMMEND: 'recommend',
1515
EXPORT: 'conceptset-export',
1616
IMPORT: 'conceptset-import',
17-
ANNOTATION: 'annotation'
17+
ANNOTATION: 'annotation',
18+
LOCK_HISTORY: 'lockHistory',
1819
};
1920

2021
const ConceptSetSources = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<atlas-modal
2+
params="data: $component, showModal: $component.isModalShown, title: ko.i18n('components.snapshot.title', 'Snapshot/Lock/Unlock')">
3+
<div class="modal-body" data-bind="visible: true">
4+
<!-- Unlocked Concept Set Content -->
5+
<div data-bind="visible: !isLocked()">
6+
<p class="modal-text"><strong>You are about to create a snapshot for this concept set.</strong></p>
7+
<p class="modal-text">
8+
There are two options: Plain snapshot or a snapshot + lock.
9+
<br><strong>Plain snapshot:</strong>
10+
<ul>
11+
<li>Concept set expression will be saved for this concept set</li>
12+
<li>Included concepts and included source codes will be saved</li>
13+
<li>Current vocabulary version and schema will be saved</li>
14+
<li>Concept set snapshots will not be impacted by future vocabulary updates</li>
15+
<li>Current SYSTEM vocabulary version: <strong data-bind="text: currentVocabularyVersion"></strong></li>
16+
<li>Current SYSTEM vocabulary schema: <strong data-bind="text: currentVocabularySchema"></strong></li>
17+
</ul>
18+
<br><strong>Snapshot with Lock (along with the above actions):</strong>
19+
<ul>
20+
<li>Concept Set will be switched to read-only mode and marked locked</li>
21+
</ul>
22+
</p>
23+
<p class="modal-text" style="margin-top: 20px;"><strong>Snapshot description message<span
24+
style="color: red;">*</span>:</strong></p>
25+
<input type="text" rows="4" data-bind="textInput: snapshotDescriptionMessage,
26+
css: classes({
27+
element: 'input',
28+
extra: 'form-control'
29+
}),
30+
placeholder: ko.i18n('snapshot.description.placeholder', 'Type your snapshot description here')" />
31+
32+
<div class="center-buttons">
33+
<button class="btn btn-success btn-sm"
34+
data-bind="click: snapshotAndLock, enable: canExecuteActions()">Snapshot + Lock</button>
35+
<button class="btn btn-success btn-sm"
36+
data-bind="click: snapshotOnly, enable: canExecuteActions()">Snapshot Only</button>
37+
<button class="btn btn-danger btn-sm" data-bind="click: () => isModalShown(false)">Cancel</button>
38+
</div>
39+
</div>
40+
41+
<!-- Locked Concept Set Content -->
42+
<div data-bind="visible: isLocked()">
43+
<p class="modal-text">You are about to UNLOCK this concept set.</p>
44+
<p class="modal-text">
45+
The following will happen:
46+
<ul>
47+
<li>Concept Set read-only mode will be disabled</li>
48+
<li>Optional: Take a snapshot after unlock</li>
49+
</ul>
50+
</p>
51+
<div class="checkbox-container">
52+
<input id="snapshotCheckbox" name="snapshotCheckbox" type="checkbox"
53+
data-bind="checked: takeSnapshotWhenUnlocking, enable: true" />
54+
<label for="snapshotCheckbox" style="font-size: 13px; align-items: center;">Take snapshot</label>
55+
</div>
56+
<p class="modal-text" style="margin-top: 20px;"><strong>Unlock confirmation message<span
57+
style="color: red;">*</span>:</strong></p>
58+
<textarea class="form-control large-text-input"
59+
data-bind="textInput: snapshotDescriptionMessage, enable: true" rows="4"></textarea>
60+
<div class="center-buttons">
61+
<button class="btn btn-success btn-sm"
62+
data-bind="click: () => unlockConceptSet(takeSnapshotWhenUnlocking()), enable: canExecuteActions()"
63+
style="background-color: green">Confirm</button>
64+
<button class="btn btn-primary btn-sm" data-bind="click: () => isModalShown(false)"
65+
style="background-color: blue">Cancel</button>
66+
</div>
67+
</div>
68+
</div>
69+
</atlas-modal>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
define([
2+
'knockout',
3+
'text!./snapshot-lock-modal.html',
4+
'components/Component',
5+
'utils/CommonUtils',
6+
'utils/AutoBind',
7+
'services/AuthAPI',
8+
'services/SourceAPI',
9+
'services/ConceptSet',
10+
'atlas-state',
11+
'less!./snapshot-lock-modal.less',
12+
'databindings',
13+
], function (
14+
ko,
15+
view,
16+
Component,
17+
commonUtils,
18+
AutoBind,
19+
authApi,
20+
sourceApi,
21+
conceptSetService,
22+
sharedState,
23+
) {
24+
class SnapshotLockModal extends AutoBind(Component) {
25+
constructor(params) {
26+
super(params);
27+
this.isModalShown = params.isModalShown;
28+
this.isLocked = params.isLocked;
29+
this.takeSnapshotWhenUnlocking = ko.observable(false);
30+
this.currentConceptSetId = params.currentConceptSetId;
31+
this.currentVocabularyVersion = params.currentVocabularyVersion;
32+
this.currentVocabularySchema = ko.observable();
33+
this.snapshotDescriptionMessage = ko.observable('');
34+
this.canExecuteActions = ko.pureComputed(() => {
35+
const hasSnapshotDesc = this.snapshotDescriptionMessage().trim().length > 0;
36+
return hasSnapshotDesc;
37+
});
38+
this.fetchAndSetVocabularySchema();
39+
}
40+
41+
async fetchAndSetVocabularySchema() {
42+
try {
43+
const source = await sourceApi.getSourceInfo(sharedState.sourceKeyOfVocabUrl());
44+
this.processSourceData(source);
45+
} catch (error) {
46+
console.error(`Error fetching source information: ${error}`);
47+
this.currentVocabularySchema(undefined);
48+
}
49+
}
50+
51+
processSourceData(source) {
52+
if (!source || !source.daimons) {
53+
this.currentVocabularySchema(undefined);
54+
} else {
55+
const vocabularyDaimon = source.daimons.find(daimon => daimon.daimonType === 'Vocabulary');
56+
this.currentVocabularySchema(vocabularyDaimon ? vocabularyDaimon.tableQualifier : undefined);
57+
}
58+
}
59+
60+
61+
createSnapshotActionRequest(action, takeSnapshot = true) {
62+
return {
63+
sourceKey: sharedState.sourceKeyOfVocabUrl(),
64+
action: action,
65+
user: authApi.subject(),
66+
message: this.snapshotDescriptionMessage(),
67+
takeSnapshot: takeSnapshot
68+
};
69+
}
70+
71+
snapshotAndLock() {
72+
const request = this.createSnapshotActionRequest("LOCK");
73+
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
74+
.then(() => {
75+
this.isLocked(true);
76+
this.snapshotDescriptionMessage("");
77+
this.isModalShown(false);
78+
console.log("Concept set locked and snapshot created");
79+
})
80+
.catch(error => console.error(`Error locking concept set: ${error}`));
81+
}
82+
83+
snapshotOnly() {
84+
const request = this.createSnapshotActionRequest("SNAPSHOT");
85+
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
86+
.then(() => {
87+
this.snapshotDescriptionMessage("");
88+
console.log("Concept set snapshot created");
89+
this.isModalShown(false);
90+
})
91+
.catch(error => console.error(`Error creating snapshot: ${error}`));
92+
}
93+
94+
unlockConceptSet(takeSnapshot) {
95+
const request = this.createSnapshotActionRequest("UNLOCK", takeSnapshot);
96+
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
97+
.then(() => {
98+
this.isLocked(false);
99+
this.snapshotDescriptionMessage("");
100+
this.isModalShown(false);
101+
console.log("Concept set unlocked");
102+
})
103+
.catch(error => console.error(`Error unlocking concept set: ${error}`));
104+
}
105+
}
106+
107+
return commonUtils.build('snapshot-lock-modal', SnapshotLockModal, view);
108+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
.snapshot-lock-modal {
2+
3+
.modal-body {
4+
padding: 20px;
5+
}
6+
7+
.modal-text {
8+
white-space: pre-wrap;
9+
margin-bottom: 20px;
10+
}
11+
12+
.center-buttons {
13+
display: flex;
14+
justify-content: space-around;
15+
margin-top: 20px;
16+
}
17+
18+
.center-buttons>button {
19+
flex-grow: 1;
20+
flex-basis: 0%;
21+
margin: 0 5px;
22+
}
23+
24+
.large-text-input {
25+
display: block;
26+
width: 80%;
27+
margin: 10px auto;
28+
min-height: 65px;
29+
max-height: 150px;
30+
height: auto;
31+
padding: 10px;
32+
}
33+
34+
.full-width {
35+
width: 100%;
36+
}
37+
38+
.bold {
39+
font-weight: bold;
40+
}
41+
42+
&__tag-groups-label,
43+
&__tag-list-label,
44+
&__new-tag-label {
45+
font-size: 1.4rem;
46+
}
47+
48+
&__tag-list-label,
49+
&__new-tag-label {
50+
margin-top: 1.3rem;
51+
}
52+
53+
&__new-tag-label {
54+
color: #0070dd;
55+
cursor: pointer;
56+
}
57+
58+
&__new-tag-label:hover {
59+
color: #23527c;
60+
}
61+
62+
.cell-tag-name {
63+
width: 100px;
64+
}
65+
66+
.cell-group-description {
67+
width: 460px;
68+
}
69+
70+
.cell-tag-created {
71+
width: 120px;
72+
}
73+
74+
.cell-tag-author {
75+
width: 100px;
76+
}
77+
78+
.cell-tag-description {
79+
width: 200px;
80+
}
81+
82+
.cell-tag-type {
83+
width: 80px;
84+
}
85+
86+
.cell-tag-name,
87+
.cell-group-description,
88+
.cell-tag-description,
89+
.cell-tag-type,
90+
.cell-tag-created,
91+
.cell-tag-author,
92+
.cell-tag-action {
93+
display: block;
94+
white-space: nowrap;
95+
overflow: hidden;
96+
text-overflow: ellipsis;
97+
}
98+
99+
.groups-dropdown {
100+
padding: 7px;
101+
margin-left: -1px;
102+
width: fit-content;
103+
width: -moz-fit-content;
104+
}
105+
106+
&__action-link {
107+
cursor: pointer;
108+
}
109+
110+
.dataTable {
111+
width: 100% !important;
112+
}
113+
114+
.dataTable th,
115+
.dataTable td {
116+
padding: 6px;
117+
}
118+
}

0 commit comments

Comments
 (0)