Skip to content

Commit

Permalink
infinite scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
amazy committed Dec 13, 2024
1 parent 5b9af91 commit 5ff5c86
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 83 deletions.
2 changes: 0 additions & 2 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ UI improvements:
}
- configure other viewers url (ex: radiant://?n=pstv&v=0020000D&v=%22StudyInstanceUID%22 or osirix or horos ...)

- make table sortable

- orthanc-share should generate QR code with publication links

- Q&R on multiple modalities at a same time (select the modalities you want to Q&R and display the modality in the study list)
Expand Down
49 changes: 29 additions & 20 deletions WebApplication/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion WebApplication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"vue": "^3.4.21",
"vue-i18n": "^9.10.2",
"vue-router": "^4.3.0",
"vuex": "^4.1.0"
"vuex": "^4.1.0",
"vue3-observe-visibility": "^1.0.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
Expand Down
3 changes: 3 additions & 0 deletions WebApplication/src/components/StudyItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ export default {
this.selected = false;
},
async clickedSelect() {
// console.log(this.studyId, this.selected);
await this.$store.dispatch('studies/selectStudy', { studyId: this.studyId, isSelected: !this.selected }); // this.selected is the value before the click
this.selected = !this.selected;
// console.log(this.studyId, this.selected);
}
},
computed: {
Expand Down
18 changes: 17 additions & 1 deletion WebApplication/src/components/StudyList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { endOfMonth, endOfYear, startOfMonth, startOfYear, subMonths, subDays, s
import api from "../orthancApi";
import { ref } from 'vue';
import SourceType from "../helpers/source-type";
import { ObserveVisibility as vObserveVisibility } from 'vue3-observe-visibility'
document._allowedFilters = ["StudyDate", "StudyTime", "AccessionNumber", "PatientID", "PatientName", "PatientBirthDate", "StudyInstanceUID", "StudyID", "StudyDescription", "ModalitiesInStudy", "labels"]
Expand Down Expand Up @@ -737,6 +738,11 @@ export default {
await this.$router.replace(newUrl);
},
async extendStudyList() {
if (this.sourceType == SourceType.LOCAL_ORTHANC && this['configuration/hasExtendedFind']) {
await this.$store.dispatch('studies/extendFilteredStudies');
}
},
async reloadStudyList() {
if (this.sourceType == SourceType.LOCAL_ORTHANC && this['configuration/hasExtendedFind']) {
await this.$store.dispatch('studies/clearStudies');
Expand Down Expand Up @@ -842,6 +848,16 @@ export default {
onDeletedStudy(studyId) {
this.$store.dispatch('studies/deleteStudy', { studyId: studyId });
},
visibilityChanged(isVisible, entry) {
if (isVisible) {
let studyId = entry.target.id;
if (studyId == this.studiesIds[this.studiesIds.length - 1]) {
// console.log("Last element shown -> should load more studies");
this.extendStudyList();
}
}
}
},
components: { StudyItem, ResourceButtonGroup }
}
Expand Down Expand Up @@ -983,7 +999,7 @@ export default {
</th>
</tr>
</thead>
<StudyItem v-for="studyId in studiesIds" :key="studyId" :studyId="studyId"
<StudyItem v-for="studyId in studiesIds" :key="studyId" :id="studyId" :studyId="studyId" v-observe-visibility="{callback: visibilityChanged, once: true}"
@deletedStudy="onDeletedStudy">
</StudyItem>

Expand Down
2 changes: 2 additions & 0 deletions WebApplication/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import axios from 'axios'
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import mitt from "mitt"
import VueObserveVisibility from 'vue3-observe-visibility'

// Names of the params that can contain an authorization token
// If one of these params contain a token, it will be passed as a header
Expand All @@ -30,6 +31,7 @@ axios.get('../api/pre-login-configuration').then((config) => {
app.use(router)
app.use(store)
app.use(i18n)
app.use(VueObserveVisibility)
app.component('Datepicker', Datepicker);

app.config.globalProperties.messageBus = messageBus;
Expand Down
6 changes: 5 additions & 1 deletion WebApplication/src/orthancApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default {
window.axiosFindStudiesAbortController = null;
}
},
async findStudies(filterQuery, labels, LabelsConstraint, orderBy) {
async findStudies(filterQuery, labels, LabelsConstraint, orderBy, since) {
await this.cancelFindStudies();
window.axiosFindStudiesAbortController = new AbortController();

Expand All @@ -95,6 +95,10 @@ export default {
payload["OrderBy"] = orderBy;
}

if (since) {
payload["Since"] = since;
}

return (await axios.post(orthancApiUrl + "tools/find", payload,
{
signal: window.axiosFindStudiesAbortController.signal
Expand Down
134 changes: 80 additions & 54 deletions WebApplication/src/store/modules/studies.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,76 @@ function insert_wildcards(initialValue) {
return finalValue.replaceAll('**', '');
}

async function get_studies_shared(context, append) {
const commit = context.commit;
const state = context.state;
const getters = context.getters;

if (!append) {
commit('setStudiesIds', { studiesIds: [] });
commit('setStudies', { studies: [] });
}

try {
commit('setIsSearching', { isSearching: true});
let studies = [];

if (state.sourceType == SourceType.LOCAL_ORTHANC) {
let orderBy = [...state.orderByFilters];
if (state.orderByFilters.length == 0) {
orderBy.push({'Type': 'Metadata', 'Key': 'LastUpdate', 'Direction': 'DESC'})
}
let since = (append ? state.studiesIds.length : null);
studies = (await api.findStudies(getters.filterQuery, state.labelFilters, "All", orderBy, since));
} else if (state.sourceType == SourceType.REMOTE_DICOM || state.sourceType == SourceType.REMOTE_DICOM_WEB) {
// make sure to fill all columns of the StudyList
let filters = {
"PatientBirthDate": "",
"PatientID": "",
"AccessionNumber": "",
"PatientBirthDate": "",
"StudyDescription": "",
"StudyDate": ""
};

// request values for e.g ModalitiesInStudy, NumberOfStudyRelatedSeries
for (let t of store.state.configuration.requestedTagsForStudyList) {
filters[t] = "";
}

// overwrite with the filtered values
for (const [k, v] of Object.entries(getters.filterQuery)) {
filters[k] = v;
}

let remoteStudies;
if (state.sourceType == SourceType.REMOTE_DICOM) {
remoteStudies = (await api.remoteDicomFind("Study", state.remoteSource, filters, true /* isUnique */));
} else if (state.sourceType == SourceType.REMOTE_DICOM_WEB) {
remoteStudies = (await api.qidoRs("Study", state.remoteSource, filters, true /* isUnique */));
}

// copy the tags in MainDicomTags, ... to have a common study structure between local and remote studies
studies = remoteStudies.map(s => { return {"MainDicomTags": s, "PatientMainDicomTags": s, "RequestedTags": s, "ID": s["StudyInstanceUID"]} });
}

studies = studies.map(s => {return {...s, "sourceType": state.sourceType} });
let studiesIds = studies.map(s => s['ID']);

if (!append) {
commit('setStudiesIds', { studiesIds: studiesIds });
commit('setStudies', { studies: studies });
} else {
commit('extendStudiesIds', { studiesIds: studiesIds });
commit('extendStudies', { studies: studies });
}
} catch (err) {
console.log("Find studies cancelled", err);
} finally {
commit('setIsSearching', { isSearching: false});
}
}

///////////////////////////// GETTERS
const getters = {
filterQuery: (state) => {
Expand Down Expand Up @@ -85,6 +155,12 @@ const mutations = {
setStudies(state, { studies }) {
state.studies = studies;
},
extendStudiesIds(state, { studiesIds }) {
state.studiesIds.push(...studiesIds);
},
extendStudies(state, { studies }) {
state.studies.push(...studies);
},
addStudy(state, { studyId, study }) {
if (!state.studiesIds.includes(studyId)) {
state.studiesIds.push(studyId);
Expand Down Expand Up @@ -221,61 +297,11 @@ const actions = {
commit('setStudiesIds', { studiesIds: [] });
commit('setStudies', { studies: [] });
},
async extendFilteredStudies({ commit, getters, state }) {
get_studies_shared({ commit, getters, state }, true);
},
async reloadFilteredStudies({ commit, getters, state }) {
commit('setStudiesIds', { studiesIds: [] });
commit('setStudies', { studies: [] });

try {
commit('setIsSearching', { isSearching: true});
let studies = [];

if (state.sourceType == SourceType.LOCAL_ORTHANC) {
let orderBy = [...state.orderByFilters];
if (state.orderByFilters.length == 0) {
orderBy.push({'Type': 'Metadata', 'Key': 'LastUpdate', 'Direction': 'DESC'})
}
studies = (await api.findStudies(getters.filterQuery, state.labelFilters, "All", orderBy));
} else if (state.sourceType == SourceType.REMOTE_DICOM || state.sourceType == SourceType.REMOTE_DICOM_WEB) {
// make sure to fill all columns of the StudyList
let filters = {
"PatientBirthDate": "",
"PatientID": "",
"AccessionNumber": "",
"PatientBirthDate": "",
"StudyDescription": "",
"StudyDate": ""
};

// request values for e.g ModalitiesInStudy, NumberOfStudyRelatedSeries
for (let t of store.state.configuration.requestedTagsForStudyList) {
filters[t] = "";
}

// overwrite with the filtered values
for (const [k, v] of Object.entries(getters.filterQuery)) {
filters[k] = v;
}

let remoteStudies;
if (state.sourceType == SourceType.REMOTE_DICOM) {
remoteStudies = (await api.remoteDicomFind("Study", state.remoteSource, filters, true /* isUnique */));
} else if (state.sourceType == SourceType.REMOTE_DICOM_WEB) {
remoteStudies = (await api.qidoRs("Study", state.remoteSource, filters, true /* isUnique */));
}

// copy the tags in MainDicomTags, ... to have a common study structure between local and remote studies
studies = remoteStudies.map(s => { return {"MainDicomTags": s, "PatientMainDicomTags": s, "RequestedTags": s, "ID": s["StudyInstanceUID"]} });
}

studies = studies.map(s => {return {...s, "sourceType": state.sourceType} });
let studiesIds = studies.map(s => s['ID']);
commit('setStudiesIds', { studiesIds: studiesIds });
commit('setStudies', { studies: studies });
} catch (err) {
console.log("Find studies cancelled", err);
} finally {
commit('setIsSearching', { isSearching: false});
}
get_studies_shared({ commit, getters, state }, false);
},
async cancelSearch() {
await api.cancelFindStudies();
Expand Down
10 changes: 6 additions & 4 deletions release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ Pending changes in the mainline
===============================

Changes:
- Allow sorting by columns when the Orthanc DB supports "ExtendedFind"
- Optimized loading of "most-recent" studies when the Orthanc DB supports "ExtendedFind"
- When Orthanc DB supports "ExtendedFind" (SQLite in 1.12.5+ and PosgreSQL 7.0+):
- new features in the local studies list:
- Allow sorting by columns
- Optimized loading of "most-recent" studies
- Load the following studies when scrolling to the bottom of the current list.
- New configuration "EnableLabelsCount" to enable/disable the display of the number of studies with each label.
- Disable some UI components on ReadOnly systems.
- New configuration "EnableLabelsCount" to enable/disable the display of the number of
studies with each label.
- The study list header is now sticking on top of the screen.

Fixes:
Expand Down

0 comments on commit 5ff5c86

Please sign in to comment.