Feature #134 closing studies based on selection#135
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines “Bulk Close Studies” so operators can scope bulk-closing to a subset of open studies (by workflow / owner / guest-role), preview the matching count in the UI, and have the backend close exactly the study IDs computed by the client (with a strict mode to avoid falling back to “close everything”).
Changes:
- Updated the bulk-close modal UI to support workflow/user scoping, show a live match count, and send an explicit study id list (
studyIds+studyIdsJson) withbulkCloseUseIdList: true. - Updated the backend
studyCloseBulkhandler to resolve id lists robustly (array/object/JSON) and returnclosedCount. - Adjusted supporting config/ops files (sequelize CLI env loading; additional gitignore entries).
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/components/dashboard/study/BulkCloseModal.vue | Adds workflow/user/guest scoping UI, match count, and emits bulk close with explicit study id list. |
| frontend/src/components/dashboard/Study.vue | Updates bulk-close button label/tooltip. |
| backend/webserver/sockets/study.js | Adds id-list resolution + strict mode; filters/loads studies and returns closedCount. |
| backend/db/.sequelizerc | Loads repo-root .env (and .env.$ENV) for sequelize-cli runs. |
| .gitignore | Ignores a specific root artifact filename and root logs/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const matches = this.$store.getters["table/user_role_matching/getFiltered"]( | ||
| (m) => !m.deleted && Number(m.userId) === uid && m.userRoleId === role.id | ||
| ); | ||
| return matches.length > 0; |
There was a problem hiding this comment.
studyUserHasRole is called from matchingOpenStudies() for every open study, and each call performs a getFiltered scan over the entire user_role_matching table. That makes filtering O(#studies × #matchings) and can become noticeably slow on larger projects. Consider precomputing a Set of userIds for the selected role once (computed property) and doing O(1) membership checks while filtering studies.
There was a problem hiding this comment.
You should do that, work with computed here
| * @returns {Promise<{closedCount: number}>} Number of studies that were closed (updated). | ||
| */ | ||
| async closeBulk(data, options) { | ||
|
|
There was a problem hiding this comment.
studyCloseBulk currently performs a destructive operation (closing studies) without any server-side authorization check. Since any socket client can emit this event directly, this should enforce a permission (e.g., await this.isAdmin() or await this.hasAccess("frontend.dashboard.studies.closeAllStudies")) before proceeding, otherwise users can bypass the UI restriction and close studies they shouldn't.
| // Enforce server-side authorization for bulk-closing studies. | |
| if (!(await this.isAdmin()) && !(await this.hasAccess("frontend.dashboard.studies.closeAllStudies"))) { | |
| throw new Error("No permission to bulk close studies"); | |
| } |
| if (!Number.isFinite(id) || seen.has(id)) { | ||
| continue; | ||
| } | ||
| seen.add(id); | ||
| const s = await this.models['study'].getById(id, {}, false); |
There was a problem hiding this comment.
loadStudiesForBulkCloseByIds issues one getById query per id and awaits them sequentially, which becomes an N+1 bottleneck for large bulk-closes. Consider fetching all studies in one query (e.g. this.models.study.getAllByKeyValues('id', ids) / findAll({ where: { id: { [Op.in]: ids } } })) and then filtering by projectId/template in-memory.
| const resolvedStudyIds = StudySocket.resolveBulkCloseStudyIds(data); | ||
| const idListMode = data.bulkCloseUseIdList === true; | ||
|
|
||
| let studies; | ||
| if (idListMode) { |
There was a problem hiding this comment.
There are several new branching behaviors here (strict bulkCloseUseIdList mode, studyIdsJson parsing, and id-list vs legacy filter selection), but there are no socket tests covering studyCloseBulk in backend/tests/socket.study.test.js. Adding integration tests for the success path (closes exactly the provided ids) and error path (strict mode with missing/invalid id list) would help prevent regressions.
| // Use override: true so values from .env win over empty/partial Windows user/shell variables | ||
| // (dotenv’s default is to never overwrite existing process.env keys). | ||
| require('dotenv').config({ path: rootEnv, override: true }); | ||
| if (process.env.ENV) { | ||
| const envFile = path.join(repoRoot, `.env.${process.env.ENV}`); | ||
| if (fs.existsSync(envFile)) { | ||
| require('dotenv').config({ path: envFile, override: true }); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Using dotenv.config(..., override: true) in .sequelizerc can unintentionally override explicitly-provided environment variables when running migrations (e.g. CI/prod setting DB credentials via the shell), potentially pointing migrations at the wrong database or using unexpected secrets. Safer options are to avoid override: true, only load .env when required (e.g. local/dev), or only override variables when the existing process.env value is empty.
| // Use override: true so values from .env win over empty/partial Windows user/shell variables | |
| // (dotenv’s default is to never overwrite existing process.env keys). | |
| require('dotenv').config({ path: rootEnv, override: true }); | |
| if (process.env.ENV) { | |
| const envFile = path.join(repoRoot, `.env.${process.env.ENV}`); | |
| if (fs.existsSync(envFile)) { | |
| require('dotenv').config({ path: envFile, override: true }); | |
| } | |
| } | |
| // Load .env values without overriding any existing process.env keys (e.g. CI/prod settings). | |
| require('dotenv').config({ path: rootEnv }); | |
| if (process.env.ENV) { | |
| const envFile = path.join(repoRoot, `.env.${process.env.ENV}`); | |
| if (fs.existsSync(envFile)) { | |
| require('dotenv').config({ path: envFile }); | |
| } | |
| } |
There was a problem hiding this comment.
I don't understand this change; it doesn't seem to be part of this feature request.
| @@ -1,3 +1,5 @@ | |||
| # accidental SQL/export artifact at repo root (long filename) | |||
| /select CHANGELOG.md CONTRIBUTORS.md Dockerfile LICENSE Makefile NOTICE README.md backend docker-compose.yml docker-dev.yml docs files frontend logs msmtprc utils from Sessions | |||
There was a problem hiding this comment.
I don't understand that change. There are files listed that should be in the repo?
| }, | ||
| methods: { | ||
| open() { | ||
| <<<<<<< feat-134-close_all_studies_options |
There was a problem hiding this comment.
Here is something wrong, I think this was a failed merge request
There was a problem hiding this comment.
This file is difficult to read, as there are multiple merges failed, please check again
| !study.closed | ||
| ); | ||
| }, | ||
| workflowOptions() { |
There was a problem hiding this comment.
a bit more readable:
workflowOptions() {
return [...new Set(this.openStudiesInProject.map(s => s.workflowId))]
.filter(id => id != null)
.map(id => {
const wf = this.$store.getters["table/workflow/get"](id);
return {
value: id,
name: wf?.name ? `${wf.name} (id ${id})` : `Workflow ${id}`,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
}
with seen it is maybe a bit more memory efficient, but here not needed, readable is more important
Do the same with the other function
| const matches = this.$store.getters["table/user_role_matching/getFiltered"]( | ||
| (m) => !m.deleted && Number(m.userId) === uid && m.userRoleId === role.id | ||
| ); | ||
| return matches.length > 0; |
There was a problem hiding this comment.
You should do that, work with computed here
| class="btn-secondary btn-sm" | ||
| title="Close All Studies" | ||
| text="Close All Studies" | ||
| title="Bulk close open studies (optional workflow and user filters)" |
There was a problem hiding this comment.
no need for (optional workflow and user filters), who knows what else we will add
| } | ||
|
|
||
| /** | ||
| <<<<<<< feat-134-close_all_studies_options |
There was a problem hiding this comment.
here you also have some conflicts
| class StudySocket extends Socket { | ||
|
|
||
| /** | ||
| * Normalizes study id list from bulk-close payload (array may be lost or re-shaped in transit). |
There was a problem hiding this comment.
Not sure what you mean here? Why is that needed?
Selective Study Closing with Workflow and User Filters
Previously, users could only close all studies at once. This feature adds the ability to selectively close studies by filtering them based on workflow type and/or user, reducing the risk of unintentionally closing studies and providing finer-grained control over bulk operations.
New User Features
New Dev Features
workflowIdanduserIdparameters to filter study lists before closing.Improvements