diff --git a/src/sites/iva/conf/browsers.settings.js b/src/sites/iva/conf/browsers.settings.js index 22bd3d59b..8ed956c77 100644 --- a/src/sites/iva/conf/browsers.settings.js +++ b/src/sites/iva/conf/browsers.settings.js @@ -767,9 +767,6 @@ const INTERPRETER_SETTINGS = { }, // hideGenomeBrowser: false }, - { - id: "review" - }, { id: "report" } diff --git a/src/sites/iva/conf/variant-interpreter.settings.js b/src/sites/iva/conf/variant-interpreter.settings.js index 07df252cc..f8909d17e 100644 --- a/src/sites/iva/conf/variant-interpreter.settings.js +++ b/src/sites/iva/conf/variant-interpreter.settings.js @@ -31,9 +31,6 @@ const VARIANT_INTERPRETER_SETTINGS = { }, // hideGenomeBrowser: false }, - { - id: "review" - }, { id: "report" } diff --git a/src/webcomponents/clinical/clinical-analysis-manager.js b/src/webcomponents/clinical/clinical-analysis-manager.js index 59cfa8c65..c203cebb2 100644 --- a/src/webcomponents/clinical/clinical-analysis-manager.js +++ b/src/webcomponents/clinical/clinical-analysis-manager.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import UtilsNew from "../../core/utils-new.js"; import LitUtils from "../commons/utils/lit-utils.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; @@ -283,6 +284,19 @@ export default class ClinicalAnalysisManager { this.#updateInterpretation(interpretationId, {locked: false}, `Interpretation '${interpretationId}' Unlocked.`, callback); } + downloadInterpretation(interpretationId) { + return this.opencgaSession.opencgaClient.clinical() + .infoInterpretation(interpretationId, { + study: this.opencgaSession.study.fqn, + }) + .then(response => { + UtilsNew.downloadJSON(response?.responses?.[0]?.results?.[0], `interpretation-${interpretationId}.json`); + }) + .catch(response => { + NotificationUtils.dispatch(this.ctx, NotificationUtils.NOTIFY_RESPONSE, response); + }); + } + updateVariant(variant, interpretation, callback) { this.opencgaSession.opencgaClient.clinical().updateInterpretation(this.clinicalAnalysis.id, interpretation.id, {primaryFindings: [variant]}, { study: this.opencgaSession.study.fqn, diff --git a/src/webcomponents/clinical/interpretation/clinical-interpretation-manager.js b/src/webcomponents/clinical/interpretation/clinical-interpretation-manager.js index 672248190..9fc98e24b 100644 --- a/src/webcomponents/clinical/interpretation/clinical-interpretation-manager.js +++ b/src/webcomponents/clinical/interpretation/clinical-interpretation-manager.js @@ -14,12 +14,11 @@ * limitations under the License. */ -import {LitElement, html} from "lit"; +import {LitElement, html, nothing} from "lit"; import {classMap} from "lit/directives/class-map.js"; import ClinicalAnalysisManager from "../clinical-analysis-manager.js"; import UtilsNew from "../../../core/utils-new.js"; import LitUtils from "../../commons/utils/lit-utils.js"; -import GridCommons from "../../commons/grid-commons.js"; import "./clinical-interpretation-summary.js"; import "./clinical-interpretation-create.js"; import "./clinical-interpretation-update.js"; @@ -31,7 +30,7 @@ export default class ClinicalInterpretationManager extends LitElement { super(); // Set status and init private properties - this._init(); + this.#init(); } createRenderRoot() { @@ -55,30 +54,21 @@ export default class ClinicalInterpretationManager extends LitElement { }; } - _init() { + #init() { this._prefix = UtilsNew.randomString(8); - - this.gridId = this._prefix + "Grid"; - this.interpretationVersions = []; - } - - connectedCallback() { - super.connectedCallback(); - - this._config = {...this.getDefaultConfig(), ...this.config}; - this.gridCommons = new GridCommons(this.gridId, this, this._config); - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); + this._config = this.getDefaultConfig(); + this.clinicalAnalysisManager = null; } update(changedProperties) { - if (changedProperties.has("clinicalAnalysis")) { - this.clinicalAnalysisObserver(); - } if (changedProperties.has("clinicalAnalysisId")) { this.clinicalAnalysisIdObserver(); } if (changedProperties.has("opencgaSession") || changedProperties.has("config")) { - this._config = {...this.getDefaultConfig(), ...this.config}; + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); } super.update(changedProperties); @@ -97,45 +87,13 @@ export default class ClinicalInterpretationManager extends LitElement { } } - clinicalAnalysisObserver() { - if (this.clinicalAnalysis && this.clinicalAnalysis.interpretation) { - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); - - // this.interpretations = [ - // { - // ...this.clinicalAnalysis.interpretation, primary: true - // }, - // ...this.clinicalAnalysis.secondaryInterpretations - // ]; - - const params = { - study: this.opencgaSession.study.fqn, - version: "all", - }; - this.opencgaSession.opencgaClient.clinical().infoInterpretation(this.clinicalAnalysis.interpretation.id, params) - .then(response => { - this.interpretationVersions = response.responses[0].results.reverse(); - - // We always refresh UI when clinicalAnalysisObserver is called - // await this.updateComplete; - this.requestUpdate(); - this.renderHistoryTable(); - }) - .catch(response => { - console.error("An error occurred fetching clinicalAnalysis: ", response); - }); - } - } - renderInterpretation(interpretation, primary) { - const interpretationLockAction = interpretation.locked ? - this.renderItemAction(interpretation, "unlock", "fa-unlock", "Unlock") : - this.renderItemAction(interpretation, "lock", "fa-lock", "Lock"); + const locked = interpretation?.locked; const interpretationTitle = interpretation.locked ? html` Interpretation #${interpretation.id.split(".")[1]} - ${interpretation.id}`: html`Interpretation #${interpretation.id.split(".")[1]} - ${interpretation.id}`; - const editInterpretationTitle = `Edit interpretation #${interpretation.id.split(".")[1]}: ${interpretation.id}`; + const editInterpretationTitle = `Edit Interpretation #${interpretation.id.split(".")[1]}: ${interpretation.id}`; return html`
@@ -170,35 +128,20 @@ export default class ClinicalInterpretationManager extends LitElement {
@@ -212,36 +155,17 @@ export default class ClinicalInterpretationManager extends LitElement { `; } - renderHistoryTable() { - this.table = $("#" + this.gridId); - this.table.bootstrapTable("destroy"); - this.table.bootstrapTable({ - theadClasses: "table-light", - buttonsClass: "light", - data: this.interpretationVersions, - columns: this._initTableColumns(), - uniqueId: "id", - iconsPrefix: GridCommons.GRID_ICONS_PREFIX, - icons: GridCommons.GRID_ICONS, - gridContext: this, - sidePagination: "local", - pagination: true, - formatNoMatches: () => "No previous versions", - // formatLoadingMessage: () => "
", - loadingTemplate: () => GridCommons.loadingFormatter(), - onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement), - }); - } - - renderItemAction(interpretation, action, icon, name) { + renderItemAction(interpretation, action, icon, name, defaultDisabled = false) { + const disabled = defaultDisabled || (interpretation?.locked && ((action !== "unlock") && (action !== "setAsPrimary"))); return html`
  • ${name} @@ -249,57 +173,15 @@ export default class ClinicalInterpretationManager extends LitElement { `; } - _initTableColumns() { - this._columns = [ - { - title: "ID", - field: "id" - }, - { - title: "Version", - field: "version" - }, - { - title: "Modification Date", - field: "modificationDate", - formatter: modificationDate => UtilsNew.dateFormatter(modificationDate, "D MMM YYYY, h:mm:ss a") - }, - { - title: "Primary Findings", - field: "primaryFindings", - formatter: primaryFindings => primaryFindings?.length - }, - { - title: "Status", - field: "internal.status.name" - }, - { - title: "Actions", - formatter: () => ` -
    - - -
    - `, - valign: "middle", - events: { - "click button": this.onActionClick.bind(this) - }, - visible: !this._config.columns?.hidden?.includes("actions") - } - ]; - - return this._columns; - } - onActionClick(e) { + e.preventDefault(); const {action, interpretationId, islocked} = e.currentTarget.dataset; const interpretationCallback = () => { this.onClinicalInterpretationUpdate(); }; - // islock is a strring - if (islocked === "true" && ((action !== "unlock") && (action !== "setAsPrimary"))) { + // Only some actions are allowed when the interpretation is locked: unclock, set as primary, and download + if (islocked === "true" && ((action !== "unlock") && (action !== "setAsPrimary") && (action !== "download"))) { NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_WARNING, { message: `${interpretationId} is locked!`, }); @@ -320,6 +202,9 @@ export default class ClinicalInterpretationManager extends LitElement { case "unlock": this.clinicalAnalysisManager.unLockInterpretation(interpretationId, interpretationCallback); break; + case "download": + this.clinicalAnalysisManager.downloadInterpretation(interpretationId); + break; } } } @@ -381,11 +266,6 @@ export default class ClinicalInterpretationManager extends LitElement { `} - -
    -

    Primary Interpretation History - ${this.clinicalAnalysis.interpretation.id}

    -
    -
    `; diff --git a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js index 9f6d3c06e..19e25ac38 100644 --- a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js +++ b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js @@ -176,6 +176,11 @@ export default class ClinicalInterpretationUpdate extends LitElement { } } }, + { + title: "Lock", + type: "toggle-switch", + field: "locked", + }, { title: "Disease Panels", field: "panels", @@ -197,7 +202,7 @@ export default class ClinicalInterpretationUpdate extends LitElement { .diseasePanels="${panelList}" .panel="${panels?.map(panel => panel.id).join(",")}" .showExtendedFilters="${false}" - .showSelectedPanels="${false}" + .showSelectedPanels="${true}" .classes="${updateParams.panels ? "selection-updated" : ""}" .disabled="${panelLock}" @filterChange="${e => handlePanelsFilterChange(e)}"> diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-save.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-save.js new file mode 100644 index 000000000..4c36da11f --- /dev/null +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-save.js @@ -0,0 +1,127 @@ +import {LitElement, html} from "lit"; +import UtilsNew from "../../../core/utils-new.js"; +import LitUtils from "../../commons/utils/lit-utils.js"; + +class VariantInterpreterBrowserSave extends LitElement { + + constructor() { + super(); + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + opencgaSession: { + type: Object, + }, + clinicalAnalysis: { + type: Object, + }, + state: { + type: Object, + }, + }; + } + + #init() { + this._prefix = UtilsNew.randomString(8); + } + + onSave() { + // 1. Dispatch evnet to save variants and include the (optional) comment + LitUtils.dispatchCustomEvent(this, "saveVariants", null, { + comment: { + message: this.querySelector(`textarea#${this._prefix}CommentMessage`).value || "", + tags: (this.querySelector(`input#${this._prefix}CommentTags`).value || "").trim().split(",").map(t => t.trim()).filter(Boolean), + }, + }); + // 2. Reset comment fields + this.querySelector(`textarea#${this._prefix}CommentMessage`).value = ""; + this.querySelector(`input#${this._prefix}CommentTags`).value = ""; + } + + onDiscard() { + LitUtils.dispatchCustomEvent(this, "discardVariants", null); + } + + onFilter() { + LitUtils.dispatchCustomEvent(this, "filterVariants", null); + } + + renderVariant(variant, color) { + const geneNames = Array.from(new Set(variant.annotation.consequenceTypes.filter(ct => ct.geneName).map(ct => ct.geneName))); + return html` +
    +
    ${variant.id} ${variant.annotation.displayConsequenceType || ""}
    +
    ${geneNames.join(", ")}
    +
    + `; + } + + renderVariantsList(title, variants, color) { + return html` +
    ${title} (${variants.length})
    +
    + ${variants.map(variant => this.renderVariant(variant, color))} +
    + `; + } + + render() { + const hasVariantsToSave = this.state.addedVariants?.length || this.state.removedVariants?.length || this.state.updatedVariants?.length; + const hasVariantsToFilter = this.state.addedVariants?.length || this.state.updatedVariants?.length; + return html` +
    +
    + Changed Variants +
    +
    + ${this.renderVariantsList("New selected variants", this.state?.addedVariants || [], "success")} + ${this.renderVariantsList("Updated variants", this.state?.updatedVariants || [], "warning")} + ${this.renderVariantsList("Removed variants", this.state?.removedVariants || [], "danger")} +
    + +
    + Add a new Interpretation Comment +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    + `; + } + + getDefaultConfig() { + return {}; + } + +} + +customElements.define("variant-interpreter-browser-save", VariantInterpreterBrowserSave); diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index cfdb33ba6..b118a7f3e 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -243,10 +243,10 @@ class VariantInterpreterBrowserTemplate extends LitElement { onFilterVariants(e) { const lockedFields = [...this._config?.filter?.activeFilters?.lockedFields.map(key => key.id), "study"]; - const variantIds = e.detail.variants.map(v => v.id); + const variantIds = new Set(e.detail.variants.map(v => v.id)); this.query = { ...UtilsNew.filterKeys(this.executedQuery, lockedFields), - id: variantIds.join(","), + id: Array.from(variantIds).join(","), }; this.notifyQueryChange(); this.requestUpdate(); diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-toolbar.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-toolbar.js index c668a5d84..694bcc496 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-toolbar.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-toolbar.js @@ -14,16 +14,16 @@ * limitations under the License. */ -import {LitElement, html} from "lit"; +import {LitElement, html, nothing} from "lit"; import UtilsNew from "../../../core/utils-new.js"; import LitUtils from "../../commons/utils/lit-utils.js"; +import "./variant-interpreter-browser-save.js"; class VariantInterpreterBrowserToolbar extends LitElement { constructor() { super(); - // Set status and init private properties - this._init(); + this.#init(); } createRenderRoot() { @@ -53,20 +53,20 @@ class VariantInterpreterBrowserToolbar extends LitElement { }; } - _init() { + #init() { this._prefix = UtilsNew.randomString(8); this.write = false; + this._config = this.getDefaultConfig(); } - connectedCallback() { - super.connectedCallback(); - this._config = {...this.getDefaultConfig(), ...this.config}; - } - - updated(changedProperties) { + update(changedProperties) { if (changedProperties.has("config")) { - this._config = {...this.getDefaultConfig(), ...this.config}; + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; } + super.update(changedProperties); } onFilterInclusionVariants() { @@ -81,26 +81,38 @@ class VariantInterpreterBrowserToolbar extends LitElement { LitUtils.dispatchCustomEvent(this, "filterVariants", null, { variants: this.clinicalAnalysis.interpretation.primaryFindings, }); + // Josemi 20240701 NOTE: this is a terrible and temporal fix to force closing the Save Menu + // when user clicks the 'Filter' button in the View menu (primary findings). + this.querySelector(`div#${this._prefix}View ul.dropdown-menu`)?.classList?.toggle?.("show"); } onFilterModifiedVariants() { LitUtils.dispatchCustomEvent(this, "filterVariants", null, { variants: [ ...this.state.addedVariants, + ...this.state.updatedVariants, ...this.state.removedVariants, ], }); + // Josemi 20240701 NOTE: this is a terrible and temporal fix to force closing the Save Menu + // when user clicks the 'Filter Variants' button in the Save menu. + this.querySelector(`div#${this._prefix}Save ul.dropdown-menu`)?.classList?.toggle?.("show"); } onResetModifiedVariants() { LitUtils.dispatchCustomEvent(this, "resetVariants", null); + // Josemi 20240701 NOTE: this is a terrible and temporal fix to force closing the Save Menu + // when user clicks the 'Discard Changes' button in the Save menu. + this.querySelector(`div#${this._prefix}Save ul.dropdown-menu`)?.classList?.toggle?.("show"); } - onSaveInterpretation() { + onSaveInterpretation(event) { LitUtils.dispatchCustomEvent(this, "saveInterpretation", null, { - comment: this.comment + comment: event?.detail?.comment || {}, }); - this.comment = {}; + // Josemi 20240701 NOTE: this is a terrible and temporal fix to force closing the Save Menu + // when user clicks the 'Save' button in the Save menu. + this.querySelector(`div#${this._prefix}Save ul.dropdown-menu`)?.classList?.toggle?.("show"); } onSaveFieldsChange(type, e) { @@ -115,6 +127,26 @@ class VariantInterpreterBrowserToolbar extends LitElement { } } + // onInterpretationChangesModalShow() { + // ModalUtils.show(); + // } + + // renderInterpretationChangesSaveModal() { + // return ModalUtils.create(this, `${this._prefix}InterpretationChangesSaveModal`, { + // display: { + // modalTitle: "Review and Save Interpretation Changes", + // modalDraggable: false, + // modalSize: "modal-lg" + // }, + // render: () => html` + // + // + // `, + // }); + // } + renderInclusionVariant(inclusion) { const iconHtml = html`
    ${variant.id} - Genotype: ${GT} (${FILTER}) + Genotype: ${GT} (${FILTER})
    `; }) : html `
    No variants found.
    ` @@ -152,22 +184,20 @@ class VariantInterpreterBrowserToolbar extends LitElement { `; } - renderVariant(variant, icon) { + renderVariant(variant) { const geneNames = Array.from(new Set(variant.annotation.consequenceTypes.filter(ct => ct.geneName).map(ct => ct.geneName))); - const iconHtml = icon ? html`` : ""; return html` -
    -
    ${variant.id} (${variant.type}) ${iconHtml}
    -
    ${variant.annotation.displayConsequenceType}
    -
    ${geneNames.join(", ")}
    +
    +
    ${variant.id} ${variant.annotation.displayConsequenceType || ""}
    +
    ${geneNames.join(", ")}
    `; } render() { - const primaryFindings = this.clinicalAnalysis.interpretation?.primaryFindings; const hasVariantsToSave = this.state.addedVariants?.length || this.state.removedVariants?.length || this.state.updatedVariants?.length; + const primaryFindings = this.clinicalAnalysis?.interpretation?.primaryFindings || []; return html`
    @@ -204,19 +234,17 @@ class VariantInterpreterBrowserToolbar extends LitElement {
  • - -
    - -
    - -
    - - -
    - -
    - -
    diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-landing.js b/src/webcomponents/variant/interpretation/variant-interpreter-landing.js index 25a271899..3d522d02e 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-landing.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-landing.js @@ -133,10 +133,8 @@ class VariantInterpreterLanding extends LitElement { const displayConfig = { width: 8, modalButtonClassName: "btn-light btn-sm", - buttonsLayout: "upper", - type: "tabs", + titleVisible: false, }; - return html`
    diff --git a/src/webcomponents/variant/interpretation/variant-interpreter.js b/src/webcomponents/variant/interpretation/variant-interpreter.js index f777010b9..a913fbcbd 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter.js @@ -18,6 +18,7 @@ import {LitElement, html, nothing} from "lit"; import UtilsNew from "../../../core/utils-new.js"; import ClinicalAnalysisManager from "../../clinical/clinical-analysis-manager.js"; import NotificationUtils from "../../commons/utils/notification-utils.js"; +import ModalUtils from "../../commons/modal/modal-utils.js"; import ExtensionsManager from "../../extensions-manager.js"; import {guardPage} from "../../commons/html-utils.js"; import "../../commons/tool-header.js"; @@ -34,6 +35,7 @@ import "../../commons/opencga-active-filters.js"; import "../../download-button.js"; import "../../loading-spinner.js"; import "../../clinical/clinical-analysis-review.js"; +import "../../clinical/interpretation/clinical-interpretation-update.js"; class VariantInterpreter extends LitElement { @@ -214,6 +216,30 @@ class VariantInterpreter extends LitElement { }); } + onInterpreationEdit() { + ModalUtils.show(`${this._prefix}InterpretationUpdateModal`); + } + + onInterpretationLock() { + const updateParams = { + locked: !this.clinicalAnalysis.interpretation.locked, + }; + this.opencgaSession.opencgaClient.clinical() + .updateInterpretation(this.clinicalAnalysis.id, this.clinicalAnalysis.interpretation.id, updateParams, { + study: this.opencgaSession.study.fqn, + }) + .then(() => { + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, { + message: `Interpretation '${this.clinicalAnalysis.interpretation.id}' has been ${updateParams.locked ? "locked" : "unlocked"}.`, + }); + this.onClinicalAnalysisUpdate(); + }) + .catch(error => { + console.error(error); + NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error); + }); + } + renderCustomAnalysisTab() { const analysisSettings = (this.settings?.tools || []).find(tool => tool?.id === "custom-analysis"); if (analysisSettings?.component === "steiner-analysis") { @@ -418,50 +444,55 @@ class VariantInterpreter extends LitElement { ` : null}
    + ${this.renderInterpretationUpdateModal()} +
    `; } @@ -550,7 +610,8 @@ class VariantInterpreter extends LitElement { id: "review", title: "Interpretation Review", description: "", - icon: "fa fa-edit" + icon: "fa fa-edit", + visible: false, }, { id: "report",