From 72f2f06c12d557dfd218002cffc17ae36f61a297 Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Sun, 24 May 2026 23:11:50 -0300 Subject: [PATCH 1/9] Implementation of Tags in Add and Edit Biomarkers and add tags panel in Biomarkers --- .../biomarkers/BiomarkerManager.tsx | 795 ++++++++++++++++++ .../biomarkers/BiomarkerTagPanel.tsx | 96 +++ .../components/biomarkers/BiomarkersPanel.tsx | 741 +++++++++------- .../manualForm/ManualForm.tsx | 18 +- .../newBiomarkerForm/NewBiomarkerForm.tsx | 27 +- .../BiomarkerFromCorrelationModal.tsx | 89 +- 6 files changed, 1415 insertions(+), 351 deletions(-) create mode 100644 src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx create mode 100644 src/frontend/static/frontend/src/components/biomarkers/BiomarkerTagPanel.tsx diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx new file mode 100644 index 00000000..05faf131 --- /dev/null +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx @@ -0,0 +1,795 @@ +import React from 'react' +import { Grid, Header, Button, Modal, DropdownItemProps } from 'semantic-ui-react' +import { DjangoTag, DjangoUserFile, TagType, DjangoInstitution, DjangoMethylationPlatform, DjangoResponseUploadUserFileError, DjangoUserFileUploadErrorInternalCode, DjangoSurvivalColumnsTupleSimple, RowHeader } from '../../utils/django_interfaces' +import ky from 'ky' +import { getDjangoHeader, alertGeneralError, getFileTypeSelectOptions, getDefaultNewTag, copyObject, getInputFileCSVColumns } from '../../utils/util_functions' +import { FileType, Nullable } from '../../utils/interfaces' +import { startUpload, UploadState } from '../../utils/file_uploader' +import { PaginationCustomFilter } from '../common/PaginatedTable' +import { TagsPanel } from '../files-manager/TagsPanel' + +/** Structure returned from the chunk upload service. */ +type UploadResponse = { + /** If the upload was successful. */ + ok: boolean, + /** Error message to show (only set in case ok === false). */ + errorMsg?: string +} + +const FILE_INPUT_LABEL = 'Add a new file' + +// URLs defined in files.html +declare const urlTagsCRUD: string +declare const urlUserFilesCRUD: string +declare const urlUserInstitutions: string +declare const urlChunkUpload: string +declare const urlChunkUploadComplete: string +declare const downloadFileHeaders: string + +/** + * New File Form fields + */ +interface NewFile { + id?: number, + newFileName: string, + newFileNameUser: string, // Filename input for user customization + newFileDescription: string, + newFileType: FileType, + isCpGSiteId: boolean, + platform: DjangoMethylationPlatform, + newTag: Nullable, + institutions: number[], + survivalColumns: DjangoSurvivalColumnsTupleSimple[] +} + +/** + * Component's state + */ +interface BiomarkerManagerState { + tags: DjangoTag[], + files: DjangoUserFile[], + userInstitutions: DjangoInstitution[], + newTag: DjangoTag, + showDeleteTagModal: boolean, + showDeleteFileModal: boolean, + selectedTagToDelete: Nullable, + selectedFileToDelete: Nullable, + deletingTag: boolean, + deletingFile: boolean, + uploadingFile: boolean, + newFile: NewFile, + addingTag: boolean, + uploadPercentage: number, + uploadState: Nullable + /** posibles values for survival tuple */ + survivalTuplesPossiblesValues: string[], +} + +/** + * Renders a manager to list, add, download and remove source files (which are used to make experiments). + * Also, this component renders a CRUD of Tags for files + */ +interface BiomarkerManagerProps { + handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: () => void) => void + onTagsChange?: (tags: DjangoTag[]) => void +} + +class BiomarkerManager extends React.Component { + private newFileInputRef: React.RefObject = React.createRef() + filterTimeout: number | undefined + abortController = new AbortController() + + constructor (props) { + super(props) + + this.state = { + tags: [], + files: [], + userInstitutions: [], + newTag: getDefaultNewTag(), + showDeleteTagModal: false, + showDeleteFileModal: false, + selectedTagToDelete: null, + selectedFileToDelete: null, + deletingTag: false, + deletingFile: false, + uploadingFile: false, + newFile: this.getDefaultNewFile(), + addingTag: false, + uploadPercentage: 0, + uploadState: null, + survivalTuplesPossiblesValues: [], + } + } + + /** + * Generates a default new file form + * @returns An object with all the field with default values + */ + getDefaultNewFile (): NewFile { + return { + newFileName: FILE_INPUT_LABEL, + newFileNameUser: '', + newFileDescription: '', + newFileType: FileType.MRNA, + newTag: null, + institutions: [], + isCpGSiteId: false, + platform: DjangoMethylationPlatform.PLATFORM_450, + survivalColumns: [] + } + } + + /** + * Handles file input changes to set data to show in form + */ + fileChange = () => { + const newFileForm = this.state.newFile + // Get Filename if file was selected + const newFile = this.newFileInputRef.current + const newFileName = (newFile && newFile.files.length > 0) ? newFile.files[0].name : FILE_INPUT_LABEL + + // If there wasn't a File name written by the user, loads the filename in the input + const newFileNameUser = (newFileForm.newFileNameUser.trim().length > 0) ? newFileForm.newFileNameUser : newFileName + // Sets the new field values + newFileForm.newFileName = newFileName + newFileForm.newFileNameUser = newFileNameUser + getInputFileCSVColumns(newFile.files[0]).then((headersColumnsNames) => { + this.setState({ newFile: newFileForm, survivalTuplesPossiblesValues: headersColumnsNames }) + }) + } + + /** + * Prevents users from closing browser tag when upload is in process. + * @param e Event + */ + onUnload = e => { // the method that will be used for both add and remove event + if (this.state.uploadingFile) { + e.preventDefault() + e.returnValue = 'A file is being uploaded. If you close the tab the upload will be canceled.' + } + } + + /** + * When the component has been mounted, It requests for + * tags and files. + */ + componentDidMount () { + window.addEventListener('beforeunload', this.onUnload) + this.getUserTags() + this.getUserInstitutions() + } + + /** Removes event on component unmount and Abort controller if component unmount. */ + componentWillUnmount () { + window.removeEventListener('beforeunload', this.onUnload) + this.abortController.abort() + } + + /** + * Fetches the Institutions of which the User is part of + */ + getUserInstitutions () { + ky.get(urlUserInstitutions, { signal: this.abortController.signal }) + .then((response) => { + response.json() + .then((userInstitutions) => { + this.setState({ userInstitutions }) + }) + .catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }) + .catch((err) => { + console.log("Error getting user's tags ->", err) + }) + } + + /** + * Fetches the User's defined tags + */ + getUserTags () { + // Gets only File's Tags + const searchParams = { type: TagType.FILE } + + ky.get(urlTagsCRUD, { searchParams, signal: this.abortController.signal }).then((response) => { + response.json().then((tags) => { + this.setState({ tags }, () => { + this.props.onTagsChange?.(tags) + }) + }).catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.log("Error getting user's tags ->", err) + }) + } + + /** + * Selects a new Tag to edit + * @param selectedTag Tag to edit + */ + editTag = (selectedTag: DjangoTag) => { this.setState({ newTag: copyObject(selectedTag) }) } + + /** + * Does a request to add a new Tag + */ + addOrEditTag () { + if (this.state.addingTag) { + return + } + + // Sets the Request's Headers + const myHeaders = getDjangoHeader() + + // If exists an id then we are editing, otherwise It's a new Tag + let addOrEditURL, requestMethod + + if (this.state.newTag.id !== null) { + addOrEditURL = `${urlTagsCRUD}${this.state.newTag.id}/` + requestMethod = ky.patch + } else { + addOrEditURL = urlTagsCRUD + requestMethod = ky.post + } + + this.setState({ addingTag: true }, () => { + requestMethod(addOrEditURL, { headers: myHeaders, json: this.state.newTag }).then((response) => { + this.setState({ addingTag: false }) + response.json().then((responseJSON: DjangoTag) => { + if (responseJSON && responseJSON.id) { + // If all is OK, resets the form and gets the User's tag to refresh the list + this.setState({ newTag: getDefaultNewTag() }) + this.getUserTags() + } + }).catch((err) => { + alertGeneralError() + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + this.setState({ addingTag: false }) + alertGeneralError() + console.log('Error adding new Tag ->', err) + }) + }) + } + + /** + * Makes a request to delete a Tag + */ + deleteTag = () => { + if (this.state.selectedTagToDelete === null) { + return + } + + // Sets the Request's Headers + const myHeaders = getDjangoHeader() + const deleteURL = `${urlTagsCRUD}${this.state.selectedTagToDelete.id}` + this.setState({ deletingTag: true }, () => { + ky.delete(deleteURL, { headers: myHeaders }).then((response) => { + // If OK is returned refresh the tags + if (response.ok) { + this.setState({ + deletingTag: false, + showDeleteTagModal: false + }) + this.getUserTags() + } + }).catch((err) => { + this.setState({ deletingTag: false }) + alertGeneralError() + console.log('Error deleting Tag ->', err) + }) + }) + } + + /** + * Makes a request to delete a File + */ + deleteFile = () => { + if (this.state.selectedFileToDelete === null) { + return + } + + // Sets the Request's Headers + const myHeaders = getDjangoHeader() + const deleteURL = `${urlUserFilesCRUD}${this.state.selectedFileToDelete.id}` + this.setState({ deletingFile: true }, () => { + ky.delete(deleteURL, { headers: myHeaders }).then((response) => { + // If OK is returned refresh the tags + if (response.ok) { + this.setState({ + deletingFile: false, + showDeleteFileModal: false + }) + + // If the file which was deleted is the same which is being edited, cleans the form + if (this.state.selectedFileToDelete?.id === this.state.newFile.id) { + this.resetNewFileForm() + } + } + }).catch((err) => { + this.setState({ deletingFile: false }) + alertGeneralError() + console.log('Error deleting new Tag ->', err) + }) + }) + } + + /** + * Handles New Tag Input changes + * @param name State field to change + * @param value Value to assign to the specified field + */ + handleAddTagInputsChange = (name: string, value) => { + const newTag = this.state.newTag + newTag[name] = value + this.setState(prevState => ({ + newTag: { + ...prevState.newTag, + [name]: value, + } + })) + } + + /** + * Handles New Tag Input Key Press + * @param e Event of change + */ + handleKeyDown = (e) => { + // If pressed Enter key submits the new Tag + if (e.which === 13 || e.keyCode === 13) { + this.addOrEditTag() + } else { + if (e.which === 27 || e.keyCode === 27) { + this.setState({ newTag: getDefaultNewTag() }) + } + } + } + + /** + * Show a modal to confirm a Tag deletion + * @param tag Selected Tag to delete + */ + confirmTagDeletion = (tag: DjangoTag) => { + this.setState({ + selectedTagToDelete: tag, + showDeleteTagModal: true + }) + } + + /** + * Show a modal to confirm a File deletion + * @param file Selected Tag to delete + */ + confirmFileDeletion = (file: DjangoUserFile) => { + this.setState({ + selectedFileToDelete: file, + showDeleteFileModal: true + }) + } + + /** + * Closes the deletion confirm modals + */ + handleClose = () => { + this.setState({ + showDeleteTagModal: false, + showDeleteFileModal: false + }) + } + + /** + * Checks if survivals columns are valid + * @returns True if are valid, false otherwise + */ + survivalColumnsAreValid (): boolean { + const survivalColumns = this.state.newFile.survivalColumns + return survivalColumns.length === 0 || + ( + survivalColumns.length > 0 && + survivalColumns.find((survivalColumn) => { + return !survivalColumn.time_column.trim().length || + !survivalColumn.event_column.trim().length + }) === undefined + ) + } + + /** + * Check if user can upload a new file + * @returns True if the new file is valid, false otherwise + */ + newFileIsValid (): boolean { + const isEditing = this.isEditing() + return !this.state.uploadingFile && + ( + (!isEditing && + this.newFileInputRef.current !== null && + this.newFileInputRef.current.files.length > 0 + ) || isEditing + ) && this.state.newFile.newFileNameUser.trim().length > 0 && + this.survivalColumnsAreValid() + } + + /** + * Resets the new file form + */ + resetNewFileForm = () => { + // Cleans the ref + this.newFileInputRef.current.value = '' + + // Cleans the state + this.setState({ newFile: this.getDefaultNewFile() }) + } + + /** + * Checks if It's editing an existing file + * @returns True if It's editing, false otherwise + */ + isEditing = (): boolean => this.state.newFile.id !== null && this.state.newFile.id !== undefined + + /** + * On success callback during file upload + * @param responseJSON JSON response with uploaded UserFile data + */ + uploadSuccess = (responseJSON: UploadResponse) => { + if (responseJSON.ok) { + // If everything gone OK, resets the New File Form... + this.setState({ newFile: this.getDefaultNewFile() }) + } else { + if (responseJSON.errorMsg) { + alert(responseJSON.errorMsg) + } else { + alertGeneralError() + } + } + } + + /** + * On error callback during file upload + * @param error Error object + */ + uploadError = (error) => { + error.response.json().then((errorBody: DjangoResponseUploadUserFileError) => { + console.error(errorBody) + // NOTE: Parses int as Django Rest Framework returns as string + // Related issue https://github.com/encode/django-rest-framework/issues/7532 + const internalCode = errorBody && errorBody.file_obj + ? parseInt(errorBody.file_obj.status.internal_code as unknown as string) + : null + + if (internalCode === DjangoUserFileUploadErrorInternalCode.INVALID_FORMAT_NON_NUMERIC) { + alert('The file has an incorrect format: all columns except the index must be numerical data') + } else { + alertGeneralError() + } + }).catch(alertGeneralError) + console.log('Error uploading file ->', error) + } + + /** + * Uploads a file + */ + uploadFile = () => { + if (!this.newFileIsValid()) { + return + } + + const myHeaders = getDjangoHeader() + const newFileForm = this.state.newFile + + const formData = new FormData() + formData.append('name', newFileForm.newFileNameUser) + formData.append('description', newFileForm.newFileDescription) + + formData.append('file_type', newFileForm.newFileType.toString()) + + if (newFileForm.newTag) { + formData.append('tag', newFileForm.newTag.toString()) + } + + // Adds the Institution's id, if selected + newFileForm.institutions.forEach((institutionId) => { + formData.append('institutions', institutionId.toString()) + }) + + // Adds the survival columns tuples, if needed + if (newFileForm.survivalColumns.length > 0) { + formData.append('survival_columns', JSON.stringify(newFileForm.survivalColumns)) + } + + // CpG info + formData.append('is_cpg_site_id', newFileForm.isCpGSiteId.toString()) + + if (newFileForm.isCpGSiteId) { + formData.append('platform', newFileForm.platform.toString()) + } + + // Checks if it is an edition or creation + this.setState({ uploadingFile: true }, () => { + if (this.isEditing()) { + // In case of edition, just call Django REST API as no file upload is required + const editUrl = `${urlUserFilesCRUD}${newFileForm.id}/` + this.setState({ uploadingFile: true }, () => { + ky.patch(editUrl, { headers: myHeaders, body: formData, timeout: false }) + .then((response) => { + response.json().then(() => { + this.setState({ newFile: this.getDefaultNewFile() }) + }).catch((err) => { + console.log('Error parsing JSON ->', err) + alertGeneralError() + }) + }) + .catch(this.uploadError) + .finally(() => { + this.setState({ uploadingFile: false }) + }) + }) + } else { + // In case of creation, an upload in chunks is required + startUpload({ + url: urlChunkUpload, + urlComplete: urlChunkUploadComplete, + headers: myHeaders, + file: this.newFileInputRef.current.files[0], + completeData: formData, + onChunkUpload: (percentDone) => { this.setState({ uploadPercentage: percentDone }) }, + onUploadStateChange: (currentState) => { this.setState({ uploadState: currentState }) } + }).then((response) => this.uploadSuccess(response as UploadResponse)) + .catch((err) => { + console.log('Error uploading file ->', err) + alertGeneralError() + }) + .finally(() => { + this.setState({ uploadingFile: false, uploadPercentage: 0 }) + }) + } + }) + } + + /** + * Generates the modal to confirm a Tag deletion + * @returns Modal component. Null if no Tag was selected to delete + */ + getTagDeletionConfirmModals () { + if (!this.state.selectedTagToDelete) { + return null + } + + return ( + +
+ +

Are you sure you want to delete the Tag "{this.state.selectedTagToDelete.name}"?

+
+ + + + + + ) + } + + /** + * Generates the modal to confirm a File deletion + * @returns Modal component. Null if no File was selected to delete + */ + getFileDeletionConfirmModals () { + if (!this.state.selectedFileToDelete) { + return null + } + + const warningMessage = this.state.selectedFileToDelete.file_type === FileType.CLINICAL + ? 'This file will be UNLINKED from all the associated experiments' + : 'All the associated experiments to this file will be DELETED' + + return ( + +
+ + Are you sure you want to delete the file {this.state.selectedFileToDelete.name}? {warningMessage} + + + + + + + ) + } + + /** + * Loads an existing User's file to edit its data + * @param fileToEdit Selected file to edit + */ + editFile = (fileToEdit: DjangoUserFile) => { + if (fileToEdit.file_type === FileType.CLINICAL) { + ky.get(`${downloadFileHeaders}${fileToEdit.id}`, { signal: this.abortController.signal }).then((response) => { + response.json().then((fileHeaders) => { + // Recieve file separates by , to get array of headers + const survivalTuplesPossiblesValues = fileHeaders + this.setState({ + survivalTuplesPossiblesValues, + newFile: { + id: fileToEdit.id, + newFileName: fileToEdit.name, + newFileNameUser: fileToEdit.name, + newFileType: fileToEdit.file_type, + newFileDescription: fileToEdit.description ?? '', + newTag: fileToEdit.tag ? fileToEdit.tag.id : null, + isCpGSiteId: fileToEdit.is_cpg_site_id, + platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, + institutions: fileToEdit.institutions.map((institution) => institution.id), + survivalColumns: fileToEdit.survival_columns ?? [] + } + }) + }).catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.log('Error getting file content ->', err) + }) + } else { + this.setState({ + survivalTuplesPossiblesValues: [], + newFile: { + id: fileToEdit.id, + newFileName: fileToEdit.name, + newFileNameUser: fileToEdit.name, + newFileType: fileToEdit.file_type, + newFileDescription: fileToEdit.description ?? '', + newTag: fileToEdit.tag ? fileToEdit.tag.id : null, + isCpGSiteId: fileToEdit.is_cpg_site_id, + platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, + institutions: fileToEdit.institutions.map((institution) => institution.id), + survivalColumns: fileToEdit.survival_columns ?? [] + } + }) + } + } + + /** + * Adds a Survival data tuple + */ + addSurvivalFormTuple = () => { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: [ + ...prevState.newFile.survivalColumns, + { event_column: '', time_column: '' } + ], + }, + })) + } + + /** + * Removes a Survival data tuple for a CGDSDataset + * @param idxSurvivalTuple Index in survival tuple + */ + removeSurvivalFormTuple = (idxSurvivalTuple: number) => { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), + }, + })) + } + + /** + * Handles CGDS Dataset form changes in fields of Survival data tuples + * @param idxSurvivalTuple Index in survival tuple + * @param name Field of the CGDS dataset to change + * @param value Value to assign to the specified field + */ + handleSurvivalFormDatasetChanges = (idxSurvivalTuple: number, name: string, value: any) => { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.map((t, i) => + i === idxSurvivalTuple ? { ...t, [name]: value } : t + ), + }, + })) + } + + /** + * Generates default table's headers + * @returns Default object for table's headers + */ + getDefaultHeaders (): RowHeader[] { + return [ + { name: 'Name', serverCodeToSort: 'name' }, + { name: 'Description', serverCodeToSort: 'description', width: 3 }, + { name: 'Type', serverCodeToSort: 'file_type' }, + { name: 'Date', serverCodeToSort: 'upload_date' }, + { name: 'Institutions', width: 2 }, + { name: 'Tag', serverCodeToSort: 'tag', width: 2 }, + { name: 'Public', width: 1 }, + { name: 'Actions', width: 2 } + ] + } + + /** + * Generates default table's Filters + * @returns Default object for table's Filters + */ + getDefaultFilters (): PaginationCustomFilter[] { + const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + + tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) + + const selectVisibilityOptions = [ + { key: 'all', text: 'All', value: 'all' }, + { key: 'private', text: 'Private', value: 'private' } + ] + + const institutionsOptions: DropdownItemProps[] = this.state.userInstitutions.map((institution) => { + return { key: institution.id, value: institution.id, text: institution.name } + }) + + return [ + { label: 'Tag', keyForServer: 'tag', defaultValue: '', placeholder: 'Select existing Tag', options: tagOptions, width: 3 }, + { label: 'Visibility', keyForServer: 'visibility', defaultValue: 'all', options: selectVisibilityOptions, clearable: false, width: 2 }, + { + label: 'Institutions', + keyForServer: 'institutions', + defaultValue: '', + options: institutionsOptions, + disabledFunction: (actualValues) => actualValues.visibility === 'private', + width: 3 + }, + { + label: 'File type', + keyForServer: 'file_type', + defaultValue: FileType.ALL, + options: getFileTypeSelectOptions(), + clearable: false, + width: 2 + } + ] + } + + render () { + // Tag and File deletion modals + const tagDeletionConfirmModal = this.getTagDeletionConfirmModals() + const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + + tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) + + return ( + <> + {/* Tag deletion modal */} + {tagDeletionConfirmModal} + + + + + + + ) + } +} + +export { NewFile, BiomarkerManager } diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkerTagPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkerTagPanel.tsx new file mode 100644 index 00000000..2c346713 --- /dev/null +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkerTagPanel.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import { Segment, Header, Icon, List } from 'semantic-ui-react' +import { DjangoTag } from '../../utils/django_interfaces' +import { Nullable } from '../../utils/interfaces' +import { TagForm } from '../common/TagForm' + +/** + * Component's props + */ +interface BiomarkerTagsPanelProps { + newTag: DjangoTag, + tags: DjangoTag[], + selectedTag?: Nullable, + addingTag: boolean, + handleAddTagInputsChange: (name: string, value: any) => void + handleKeyDown: (any) => void, + confirmTagDeletion: (any) => void, + editTag: (any) => void, + handleFilterChanges?: (string, any) => void +} + +/** + * Renders a CRUD panel for Tags (model: api_service.models.Tag) + * @param props Component's props + * @returns Component + */ +export const BiomarkerTagsPanel = (props: BiomarkerTagsPanelProps) => { + const selectedTagId = props.selectedTag ? props.selectedTag.id : null + const tagsItems = props.tags.map((tag) => { + const isActive = tag.id === selectedTagId + + return ( + { + if (props.handleFilterChanges !== undefined) { + props.handleFilterChanges('tag', tag) + } + }} + className='clickable ellipsis' + > + + {/* Edit button */} + props.editTag(tag)} + /> + + {/* Delete button */} + props.confirmTagDeletion(tag)} + /> + + + + {tag.name} + {tag.description} + + + ) + }) + + return ( + +
+ + My Tags +
+ + {/* Add tag input */} + + + {/* Tag List */} + + {tagsItems} + +
+ ) +} diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 177baab3..ddcca840 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -1,10 +1,10 @@ import React from 'react' import { Base } from '../Base' -import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' +import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' +import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource } from '../../utils/util_functions' -import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces' +import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' @@ -26,6 +26,7 @@ import { SharedInstitutionsBiomarker, SharedInstitutionsBiomarkerPropsExtend } f import { EditBiomarkerIcon } from './EditBiomarkerIcon' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { PopupIcons } from '../common/PopupIcons' +import { BiomarkerManager, NewFile } from './BiomarkerManager' // URLs defined in biomarkers.html declare const urlBiomarkersCRUD: string @@ -45,14 +46,15 @@ declare const urlCloneBiomarker: string declare const urlStopFSExperiment: string const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds - +const FILE_INPUT_LABEL = 'Add a new file' /** A matched molecule with the search query and the validated alias. */ type MoleculeFinderResult = { molecule: string, standard: string } /** Extremely simple struct of a Biomarker (useful for simple updates). */ type BiomarkerNameAndDesc = { name: string, - description: string + description: string, + tag?: { id: number } | null } /** Some flags to validate the Biomarkers form. */ @@ -99,6 +101,8 @@ interface BiomarkersPanelState { modalInstitutions: SharedInstitutionsBiomarkerPropsExtend, /** modal to handle shared users */ modalUsers: SharedUsersBiomarkerPropsExtend, + newFile: NewFile, + } /** @@ -134,7 +138,8 @@ export class BiomarkersPanel extends React.Component { + handleChangeInputForm = (value: string, name: 'biomarkerName' | 'biomarkerDescription' | 'tag') => { this.setState(prevState => ({ formBiomarker: { ...prevState.formBiomarker, @@ -1376,6 +1400,17 @@ export class BiomarkersPanel extends React.Component { + const newFileForm = this.state.newFile + newFileForm[name] = value + this.setState({ newFile: newFileForm }) + } + /** * Generates a default new file form * @returns An object with all the field with default values @@ -1510,6 +1545,19 @@ export class BiomarkersPanel extends React.Component { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), + }, + })) + } + /** * Generates the modal to confirm a biomarker deletion * @returns Modal component. Null if no Tag was selected to delete @@ -1586,6 +1634,23 @@ export class BiomarkersPanel extends React.Component { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.map((t, i) => + i === idxSurvivalTuple ? { ...t, [name]: value } : t + ), + }, + })) + } + /** * Function to go back to step 1 */ @@ -1782,328 +1847,368 @@ export class BiomarkersPanel extends React.Component - headerTitle='Biomarkers' - headers={[ - { name: 'Name', serverCodeToSort: 'name', width: 3 }, - { name: 'Description', serverCodeToSort: 'description', width: 4 }, - { name: 'Tag', serverCodeToSort: 'tag' }, - { name: 'State', serverCodeToSort: 'state', textAlign: 'center' }, - { name: 'Origin', serverCodeToSort: 'origin', textAlign: 'center' }, - { name: 'Date', serverCodeToSort: 'upload_date' }, - { name: '# mRNAS', serverCodeToSort: 'number_of_mrnas', width: 1 }, - { name: '# miRNAS', serverCodeToSort: 'number_of_mirnas', width: 1 }, - { name: '# CNA', serverCodeToSort: 'number_of_cnas', width: 1 }, - { name: '# Methylation', serverCodeToSort: 'number_of_methylations', width: 1 }, - { name: 'Public', width: 1 }, - { name: 'Shared', width: 1 }, - { name: 'Actions', width: 2 } - ]} - defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} - customFilters={this.getDefaultFilters()} - showSearchInput - customElements={[ - - - - ]} - searchLabel='Name' - searchPlaceholder='Search by name' - urlToRetrieveData={urlBiomarkersCRUD} - updateWSKey='update_biomarkers' - mapFunction={(biomarker: BiomarkerSimple) => { - const showNumberOfMolecules = biomarker.state === BiomarkerState.COMPLETED - const canEditMolecules = this.canEditBiomarker(biomarker) - const currentBiomarkerIsLoading = biomarker.id === this.state.loadingFullBiomarkerId - const isInProcess = biomarker.state === BiomarkerState.IN_PROCESS || + + + this.setState({ tags })} + /> + this.handleCancelConfirmModalState()} + onConfirm={() => { + this.handleCancelConfirmModalState() + this.state.confirmModal.onConfirm() + }} + /> + + + {/* Files overview panel */} + + + headerTitle='Biomarkers' + headers={[ + { name: 'Name', serverCodeToSort: 'name', width: 3 }, + { name: 'Description', serverCodeToSort: 'description', width: 4 }, + { name: 'Tag', serverCodeToSort: 'tag' }, + { name: 'State', serverCodeToSort: 'state', textAlign: 'center' }, + { name: 'Origin', serverCodeToSort: 'origin', textAlign: 'center' }, + { name: 'Date', serverCodeToSort: 'upload_date' }, + { name: '# mRNAS', serverCodeToSort: 'number_of_mrnas', width: 1 }, + { name: '# miRNAS', serverCodeToSort: 'number_of_mirnas', width: 1 }, + { name: '# CNA', serverCodeToSort: 'number_of_cnas', width: 1 }, + { name: '# Methylation', serverCodeToSort: 'number_of_methylations', width: 1 }, + { name: 'Public', width: 1 }, + { name: 'Shared', width: 1 }, + { name: 'Actions', width: 2 } + ]} + defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }} + customFilters={this.getDefaultFilters()} + showSearchInput + customElements={[ + + + + ]} + searchLabel='Name' + searchPlaceholder='Search by name' + urlToRetrieveData={urlBiomarkersCRUD} + updateWSKey='update_biomarkers' + mapFunction={(biomarker: BiomarkerSimple) => { + const showNumberOfMolecules = biomarker.state === BiomarkerState.COMPLETED + const canEditMolecules = this.canEditBiomarker(biomarker) + const currentBiomarkerIsLoading = biomarker.id === this.state.loadingFullBiomarkerId + const isInProcess = biomarker.state === BiomarkerState.IN_PROCESS || biomarker.state === BiomarkerState.WAITING_FOR_QUEUE - return ( - - - - - - - - {showNumberOfMolecules ? biomarker.number_of_mrnas : '-'} - {showNumberOfMolecules ? biomarker.number_of_mirnas : '-'} - {showNumberOfMolecules ? biomarker.number_of_cnas : '-'} - {showNumberOfMolecules ? biomarker.number_of_methylations : '-'} - - { - biomarker.is_public - ? ( + return ( + + + + + + + + {showNumberOfMolecules ? biomarker.number_of_mrnas : '-'} + {showNumberOfMolecules ? biomarker.number_of_mirnas : '-'} + {showNumberOfMolecules ? biomarker.number_of_cnas : '-'} + {showNumberOfMolecules ? biomarker.number_of_methylations : '-'} + + { + biomarker.is_public + ? ( + + ) + : ( + + ) + } + + + + + + + {/* Users can modify or delete own biomarkers or the ones which the user is admin of */} + <> + {/* Details button */} + this.openBiomarkerDetailsModal(biomarker)} /> - ) - } - - - - - - - {/* Users can modify or delete own biomarkers or the ones which the user is admin of */} - <> - {/* Details button */} - this.openBiomarkerDetailsModal(biomarker)} - /> - - {/* Edit button */} - - - {/* Clone button */} - this.setState({ biomarkerToClone: biomarker })} - /> - - {/* Stop button */} - {isInProcess && ( - this.setState({ biomarkerToStop: biomarker })} - /> - )} - {/* Delete button */} - {!isInProcess && !biomarker.is_public && ( - this.confirmBiomarkerDeletion(biomarker)} - ownerId={biomarker.user.id} - /> - )} - {/* Public switch */} - { - biomarker.id && ( - + + {/* Clone button */} + this.setState({ biomarkerToClone: biomarker })} /> - ) - } - - )} - /> - - - - ) - }} - /> - - {/* Create/Edit modal. */} - -
- - Are you sure you want to clone the Biomarker "{this.state.biomarkerToClone?.name}"? - - - - - - - - {/* Create/Edit modal. */} - } - closeOnEscape={false} - closeOnDimmerClick={false} - closeOnDocumentClick={false} - className={this.state.biomarkerTypeSelected !== BiomarkerOrigin.BASE ? 'space-modal large-modal' : undefined} - style={this.state.biomarkerTypeSelected === BiomarkerOrigin.BASE ? { width: '60%', minHeight: '60%' } : undefined} - onClose={() => { - if (this.state.biomarkerTypeSelected !== BiomarkerOrigin.BASE) { - this.handleChangeConfirmModalState( - true, - 'You are going to lose all the data inserted', - 'Are you sure?', - this.closeBiomarkerModal - ) - } else { - this.closeBiomarkerModal() - } - }} - > - {this.state.biomarkerTypeSelected === BiomarkerOrigin.BASE && - } - - {this.state.biomarkerTypeSelected === BiomarkerOrigin.MANUAL && ( - this.setState({ biomarkerToStop: biomarker })} + /> + )} + + {/* Delete button */} + {!isInProcess && !biomarker.is_public && ( + this.confirmBiomarkerDeletion(biomarker)} + ownerId={biomarker.user.id} + /> + )} + {/* Public switch */} + { + biomarker.id && ( + + ) + } + + )} + /> + + + + + ) + }} + /> + + {/* Create/Edit modal. */} + +
+ + Are you sure you want to clone the Biomarker "{this.state.biomarkerToClone?.name}"? + + + + + + + + {/* Create/Edit modal. */} + } + closeOnEscape={false} + closeOnDimmerClick={false} + closeOnDocumentClick={false} + className={this.state.biomarkerTypeSelected !== BiomarkerOrigin.BASE ? 'space-modal large-modal' : undefined} + style={this.state.biomarkerTypeSelected === BiomarkerOrigin.BASE ? { width: '60%', minHeight: '60%' } : undefined} + onClose={() => { + if (this.state.biomarkerTypeSelected !== BiomarkerOrigin.BASE) { + this.handleChangeConfirmModalState( + true, + 'You are going to lose all the data inserted', + 'Are you sure?', + this.closeBiomarkerModal + ) + } else { + this.closeBiomarkerModal() + } + }} + > + {this.state.biomarkerTypeSelected === BiomarkerOrigin.BASE && + } + + {this.state.biomarkerTypeSelected === BiomarkerOrigin.MANUAL && ( + { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + ]} + uploadingFile={false} + handleAddFileInputsChange={this.handleAddFileInputsChange} + newFile={this.state.newFile} + handleSurvivalFormDatasetChanges={this.handleSurvivalFormDatasetChanges} + removeSurvivalFormTuple={this.removeSurvivalFormTuple} + /> + )} + + {this.state.biomarkerTypeSelected === BiomarkerOrigin.FEATURE_SELECTION && ( + this.handleChangeConfirmModalState(true, 'You are going to lose all the data inserted', 'Are you sure?', this.closeBiomarkerModal)} + /> + )} + + + {/* Biomarker details modal. */} + } + closeOnEscape={false} + closeOnDimmerClick={false} + closeOnDocumentClick={false} + centered={false} + onClose={this.closeBiomarkerDetailsModal} + open={this.state.openDetailsModal} + > + + + + this.handleCancelConfirmModalState()} + onConfirm={() => { + this.state.confirmModal.onConfirm() + + this.setState(prevState => { + return { + confirmModal: { + ...prevState.confirmModal, + confirmModal: false + } + } + }) + }} + /> + - )} - - {this.state.biomarkerTypeSelected === BiomarkerOrigin.FEATURE_SELECTION && ( - this.handleChangeConfirmModalState(true, 'You are going to lose all the data inserted', 'Are you sure?', this.closeBiomarkerModal)} + - )} - - - {/* Biomarker details modal. */} - } - closeOnEscape={false} - closeOnDimmerClick={false} - closeOnDocumentClick={false} - centered={false} - onClose={this.closeBiomarkerDetailsModal} - open={this.state.openDetailsModal} - > - - - - this.handleCancelConfirmModalState()} - onConfirm={() => { - this.state.confirmModal.onConfirm() - - this.setState(prevState => { - return { - confirmModal: { - ...prevState.confirmModal, - confirmModal: false - } - } - }) - }} - /> - - - - + + + + ) } diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index 6fd318c0..f40a0f88 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -1,16 +1,25 @@ import React from 'react' -import { Grid } from 'semantic-ui-react' +import { DropdownItemProps, Grid } from 'semantic-ui-react' import { BiomarkerType, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection } from './../../types' import { NewBiomarkerForm } from './newBiomarkerForm/NewBiomarkerForm' import { MoleculesSectionsContainer } from './MoleculeSectionContainer' +import { NewFile } from '../../BiomarkerManager' +import { DjangoTag } from '../../../../utils/django_interfaces' /** ManualForm's props. */ interface ManualFormProps { + tagOptions: DropdownItemProps[] + tags: DjangoTag[] + uploadingFile: boolean + handleAddFileInputsChange: (name: string, value: any) => void + newFile: NewFile biomarkerForm: FormBiomarkerData, /** Value for Checkbox. */ checkedIgnoreProposedAlias: boolean, /** Handle change for Checkbox. */ handleChangeIgnoreProposedAlias: (value: boolean) => void, + removeSurvivalFormTuple: (idx: number) => void, + handleSurvivalFormDatasetChanges: (idx: number, name: string, value) => void, cleanForm: () => void, isFormEmpty: () => boolean, handleChangeMoleculeSelected: (value: BiomarkerType) => void, @@ -26,7 +35,7 @@ interface ManualFormProps { handleValidateForm: () => { haveAmbiguous: boolean, haveInvalid: boolean }, handleSendForm: () => void, handleChangeCheckBox: (value: boolean) => void, - handleChangeInputForm: (value: string, name: 'biomarkerName' | 'biomarkerDescription') => void, + handleChangeInputForm: (value: any, name: 'biomarkerName' | 'biomarkerDescription' | 'tag') => void, } @@ -50,6 +59,11 @@ export const ManualForm = (props: ManualFormProps) => { handleValidateForm={props.handleValidateForm} handleSendForm={props.handleSendForm} handleChangeCheckBox={props.handleChangeCheckBox} + tagOptions={props.tagOptions} + newFile={props.newFile} + uploadingFile={props.uploadingFile} + handleAddFileInputsChange={props.handleAddFileInputsChange} + tags={props.tags} /> void, + handleAddFileInputsChange: (name: string, value: any) => void, isFormEmpty: () => boolean, cleanForm: () => void, handleChangeMoleculeSelected: (name: BiomarkerType) => void, @@ -50,7 +57,7 @@ interface NewBiomarkerFormProps { handleValidateForm: () => { haveAmbiguous: boolean, haveInvalid: boolean }, handleSendForm: () => void, handleChangeCheckBox: (value: boolean) => void, - handleChangeInputForm: (value: string, name: 'biomarkerName' | 'biomarkerDescription') => void, + handleChangeInputForm: (value: any, name: 'biomarkerName' | 'biomarkerDescription' | 'tag') => void, } /** @@ -111,6 +118,22 @@ export const NewBiomarkerForm = (props: NewBiomarkerFormProps) => { onChange={(_, { value }) => props.handleChangeMoleculeSelected(Object.values(BiomarkerType).includes(value as BiomarkerType) ? value as BiomarkerType : BiomarkerType.MRNA)} /> + { + const selectedTag = props.tags.find(t => t.id === value) ?? null + props.handleChangeInputForm(selectedTag, 'tag') + }} + value={props.biomarkerForm.tag?.id ?? ''} + placeholder='Tag (optional)' + /> + { + handleChangeInputForm = (value: any, name: 'biomarkerName' | 'biomarkerDescription' | 'tag') => { const formBiomarker = this.state.formBiomarker formBiomarker[name] = value this.setState({ formBiomarker }) @@ -1296,42 +1318,33 @@ export class BiomarkerFromCorrelationModal extends React.Component { - const newBiomarker = this.state.newBiomarker - const dataset = newBiomarker[datasetName] - - if (dataset !== null && dataset.survival_columns !== undefined) { - dataset.survival_columns.splice(idxSurvivalTuple, 1) - this.setState({ newBiomarker }) - } + removeSurvivalFormTuple = (idxSurvivalTuple: number) => { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), + }, + })) } /** - * TODO: Check if needed * Handles CGDS Dataset form changes in fields of Survival data tuples - * @param datasetName Name of the edited CGDS dataset * @param idxSurvivalTuple Index in survival tuple * @param name Field of the CGDS dataset to change * @param value Value to assign to the specified field */ - handleSurvivalFormDatasetChanges = ( - datasetName: NameOfCGDSDataset, - idxSurvivalTuple: number, - name: string, - value: any - ) => { - const newBiomarker = this.state.newBiomarker - const dataset = newBiomarker[datasetName] - - if (dataset !== null && dataset.survival_columns !== undefined) { - dataset.survival_columns[idxSurvivalTuple][name] = value - this.setState({ newBiomarker }) - } + handleSurvivalFormDatasetChanges = (idxSurvivalTuple: number, name: string, value: any) => { + this.setState(prevState => ({ + newFile: { + ...prevState.newFile, + survivalColumns: prevState.newFile.survivalColumns.map((t, i) => + i === idxSurvivalTuple ? { ...t, [name]: value } : t + ), + }, + })) } /** @@ -1420,6 +1433,17 @@ export class BiomarkerFromCorrelationModal extends React.Component { + const newFileForm = this.state.newFile + newFileForm[name] = value + this.setState({ newFile: newFileForm }) + } + /** * Generates default table's Filters. * @returns Default object for table's Filters @@ -1562,6 +1586,13 @@ export class BiomarkerFromCorrelationModal extends React.Component From ab212d6d0b7bedda1b68f8170a86dc239c880198 Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Sun, 31 May 2026 13:44:40 -0300 Subject: [PATCH 2/9] Unification of BiomarkerManager in BimoarkerPanel --- .../biomarkers/BiomarkerManager.tsx | 795 ------------------ .../components/biomarkers/BiomarkersPanel.tsx | 190 ++++- 2 files changed, 183 insertions(+), 802 deletions(-) delete mode 100644 src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx deleted file mode 100644 index 05faf131..00000000 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkerManager.tsx +++ /dev/null @@ -1,795 +0,0 @@ -import React from 'react' -import { Grid, Header, Button, Modal, DropdownItemProps } from 'semantic-ui-react' -import { DjangoTag, DjangoUserFile, TagType, DjangoInstitution, DjangoMethylationPlatform, DjangoResponseUploadUserFileError, DjangoUserFileUploadErrorInternalCode, DjangoSurvivalColumnsTupleSimple, RowHeader } from '../../utils/django_interfaces' -import ky from 'ky' -import { getDjangoHeader, alertGeneralError, getFileTypeSelectOptions, getDefaultNewTag, copyObject, getInputFileCSVColumns } from '../../utils/util_functions' -import { FileType, Nullable } from '../../utils/interfaces' -import { startUpload, UploadState } from '../../utils/file_uploader' -import { PaginationCustomFilter } from '../common/PaginatedTable' -import { TagsPanel } from '../files-manager/TagsPanel' - -/** Structure returned from the chunk upload service. */ -type UploadResponse = { - /** If the upload was successful. */ - ok: boolean, - /** Error message to show (only set in case ok === false). */ - errorMsg?: string -} - -const FILE_INPUT_LABEL = 'Add a new file' - -// URLs defined in files.html -declare const urlTagsCRUD: string -declare const urlUserFilesCRUD: string -declare const urlUserInstitutions: string -declare const urlChunkUpload: string -declare const urlChunkUploadComplete: string -declare const downloadFileHeaders: string - -/** - * New File Form fields - */ -interface NewFile { - id?: number, - newFileName: string, - newFileNameUser: string, // Filename input for user customization - newFileDescription: string, - newFileType: FileType, - isCpGSiteId: boolean, - platform: DjangoMethylationPlatform, - newTag: Nullable, - institutions: number[], - survivalColumns: DjangoSurvivalColumnsTupleSimple[] -} - -/** - * Component's state - */ -interface BiomarkerManagerState { - tags: DjangoTag[], - files: DjangoUserFile[], - userInstitutions: DjangoInstitution[], - newTag: DjangoTag, - showDeleteTagModal: boolean, - showDeleteFileModal: boolean, - selectedTagToDelete: Nullable, - selectedFileToDelete: Nullable, - deletingTag: boolean, - deletingFile: boolean, - uploadingFile: boolean, - newFile: NewFile, - addingTag: boolean, - uploadPercentage: number, - uploadState: Nullable - /** posibles values for survival tuple */ - survivalTuplesPossiblesValues: string[], -} - -/** - * Renders a manager to list, add, download and remove source files (which are used to make experiments). - * Also, this component renders a CRUD of Tags for files - */ -interface BiomarkerManagerProps { - handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: () => void) => void - onTagsChange?: (tags: DjangoTag[]) => void -} - -class BiomarkerManager extends React.Component { - private newFileInputRef: React.RefObject = React.createRef() - filterTimeout: number | undefined - abortController = new AbortController() - - constructor (props) { - super(props) - - this.state = { - tags: [], - files: [], - userInstitutions: [], - newTag: getDefaultNewTag(), - showDeleteTagModal: false, - showDeleteFileModal: false, - selectedTagToDelete: null, - selectedFileToDelete: null, - deletingTag: false, - deletingFile: false, - uploadingFile: false, - newFile: this.getDefaultNewFile(), - addingTag: false, - uploadPercentage: 0, - uploadState: null, - survivalTuplesPossiblesValues: [], - } - } - - /** - * Generates a default new file form - * @returns An object with all the field with default values - */ - getDefaultNewFile (): NewFile { - return { - newFileName: FILE_INPUT_LABEL, - newFileNameUser: '', - newFileDescription: '', - newFileType: FileType.MRNA, - newTag: null, - institutions: [], - isCpGSiteId: false, - platform: DjangoMethylationPlatform.PLATFORM_450, - survivalColumns: [] - } - } - - /** - * Handles file input changes to set data to show in form - */ - fileChange = () => { - const newFileForm = this.state.newFile - // Get Filename if file was selected - const newFile = this.newFileInputRef.current - const newFileName = (newFile && newFile.files.length > 0) ? newFile.files[0].name : FILE_INPUT_LABEL - - // If there wasn't a File name written by the user, loads the filename in the input - const newFileNameUser = (newFileForm.newFileNameUser.trim().length > 0) ? newFileForm.newFileNameUser : newFileName - // Sets the new field values - newFileForm.newFileName = newFileName - newFileForm.newFileNameUser = newFileNameUser - getInputFileCSVColumns(newFile.files[0]).then((headersColumnsNames) => { - this.setState({ newFile: newFileForm, survivalTuplesPossiblesValues: headersColumnsNames }) - }) - } - - /** - * Prevents users from closing browser tag when upload is in process. - * @param e Event - */ - onUnload = e => { // the method that will be used for both add and remove event - if (this.state.uploadingFile) { - e.preventDefault() - e.returnValue = 'A file is being uploaded. If you close the tab the upload will be canceled.' - } - } - - /** - * When the component has been mounted, It requests for - * tags and files. - */ - componentDidMount () { - window.addEventListener('beforeunload', this.onUnload) - this.getUserTags() - this.getUserInstitutions() - } - - /** Removes event on component unmount and Abort controller if component unmount. */ - componentWillUnmount () { - window.removeEventListener('beforeunload', this.onUnload) - this.abortController.abort() - } - - /** - * Fetches the Institutions of which the User is part of - */ - getUserInstitutions () { - ky.get(urlUserInstitutions, { signal: this.abortController.signal }) - .then((response) => { - response.json() - .then((userInstitutions) => { - this.setState({ userInstitutions }) - }) - .catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }) - .catch((err) => { - console.log("Error getting user's tags ->", err) - }) - } - - /** - * Fetches the User's defined tags - */ - getUserTags () { - // Gets only File's Tags - const searchParams = { type: TagType.FILE } - - ky.get(urlTagsCRUD, { searchParams, signal: this.abortController.signal }).then((response) => { - response.json().then((tags) => { - this.setState({ tags }, () => { - this.props.onTagsChange?.(tags) - }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - console.log("Error getting user's tags ->", err) - }) - } - - /** - * Selects a new Tag to edit - * @param selectedTag Tag to edit - */ - editTag = (selectedTag: DjangoTag) => { this.setState({ newTag: copyObject(selectedTag) }) } - - /** - * Does a request to add a new Tag - */ - addOrEditTag () { - if (this.state.addingTag) { - return - } - - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - - // If exists an id then we are editing, otherwise It's a new Tag - let addOrEditURL, requestMethod - - if (this.state.newTag.id !== null) { - addOrEditURL = `${urlTagsCRUD}${this.state.newTag.id}/` - requestMethod = ky.patch - } else { - addOrEditURL = urlTagsCRUD - requestMethod = ky.post - } - - this.setState({ addingTag: true }, () => { - requestMethod(addOrEditURL, { headers: myHeaders, json: this.state.newTag }).then((response) => { - this.setState({ addingTag: false }) - response.json().then((responseJSON: DjangoTag) => { - if (responseJSON && responseJSON.id) { - // If all is OK, resets the form and gets the User's tag to refresh the list - this.setState({ newTag: getDefaultNewTag() }) - this.getUserTags() - } - }).catch((err) => { - alertGeneralError() - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - this.setState({ addingTag: false }) - alertGeneralError() - console.log('Error adding new Tag ->', err) - }) - }) - } - - /** - * Makes a request to delete a Tag - */ - deleteTag = () => { - if (this.state.selectedTagToDelete === null) { - return - } - - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - const deleteURL = `${urlTagsCRUD}${this.state.selectedTagToDelete.id}` - this.setState({ deletingTag: true }, () => { - ky.delete(deleteURL, { headers: myHeaders }).then((response) => { - // If OK is returned refresh the tags - if (response.ok) { - this.setState({ - deletingTag: false, - showDeleteTagModal: false - }) - this.getUserTags() - } - }).catch((err) => { - this.setState({ deletingTag: false }) - alertGeneralError() - console.log('Error deleting Tag ->', err) - }) - }) - } - - /** - * Makes a request to delete a File - */ - deleteFile = () => { - if (this.state.selectedFileToDelete === null) { - return - } - - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - const deleteURL = `${urlUserFilesCRUD}${this.state.selectedFileToDelete.id}` - this.setState({ deletingFile: true }, () => { - ky.delete(deleteURL, { headers: myHeaders }).then((response) => { - // If OK is returned refresh the tags - if (response.ok) { - this.setState({ - deletingFile: false, - showDeleteFileModal: false - }) - - // If the file which was deleted is the same which is being edited, cleans the form - if (this.state.selectedFileToDelete?.id === this.state.newFile.id) { - this.resetNewFileForm() - } - } - }).catch((err) => { - this.setState({ deletingFile: false }) - alertGeneralError() - console.log('Error deleting new Tag ->', err) - }) - }) - } - - /** - * Handles New Tag Input changes - * @param name State field to change - * @param value Value to assign to the specified field - */ - handleAddTagInputsChange = (name: string, value) => { - const newTag = this.state.newTag - newTag[name] = value - this.setState(prevState => ({ - newTag: { - ...prevState.newTag, - [name]: value, - } - })) - } - - /** - * Handles New Tag Input Key Press - * @param e Event of change - */ - handleKeyDown = (e) => { - // If pressed Enter key submits the new Tag - if (e.which === 13 || e.keyCode === 13) { - this.addOrEditTag() - } else { - if (e.which === 27 || e.keyCode === 27) { - this.setState({ newTag: getDefaultNewTag() }) - } - } - } - - /** - * Show a modal to confirm a Tag deletion - * @param tag Selected Tag to delete - */ - confirmTagDeletion = (tag: DjangoTag) => { - this.setState({ - selectedTagToDelete: tag, - showDeleteTagModal: true - }) - } - - /** - * Show a modal to confirm a File deletion - * @param file Selected Tag to delete - */ - confirmFileDeletion = (file: DjangoUserFile) => { - this.setState({ - selectedFileToDelete: file, - showDeleteFileModal: true - }) - } - - /** - * Closes the deletion confirm modals - */ - handleClose = () => { - this.setState({ - showDeleteTagModal: false, - showDeleteFileModal: false - }) - } - - /** - * Checks if survivals columns are valid - * @returns True if are valid, false otherwise - */ - survivalColumnsAreValid (): boolean { - const survivalColumns = this.state.newFile.survivalColumns - return survivalColumns.length === 0 || - ( - survivalColumns.length > 0 && - survivalColumns.find((survivalColumn) => { - return !survivalColumn.time_column.trim().length || - !survivalColumn.event_column.trim().length - }) === undefined - ) - } - - /** - * Check if user can upload a new file - * @returns True if the new file is valid, false otherwise - */ - newFileIsValid (): boolean { - const isEditing = this.isEditing() - return !this.state.uploadingFile && - ( - (!isEditing && - this.newFileInputRef.current !== null && - this.newFileInputRef.current.files.length > 0 - ) || isEditing - ) && this.state.newFile.newFileNameUser.trim().length > 0 && - this.survivalColumnsAreValid() - } - - /** - * Resets the new file form - */ - resetNewFileForm = () => { - // Cleans the ref - this.newFileInputRef.current.value = '' - - // Cleans the state - this.setState({ newFile: this.getDefaultNewFile() }) - } - - /** - * Checks if It's editing an existing file - * @returns True if It's editing, false otherwise - */ - isEditing = (): boolean => this.state.newFile.id !== null && this.state.newFile.id !== undefined - - /** - * On success callback during file upload - * @param responseJSON JSON response with uploaded UserFile data - */ - uploadSuccess = (responseJSON: UploadResponse) => { - if (responseJSON.ok) { - // If everything gone OK, resets the New File Form... - this.setState({ newFile: this.getDefaultNewFile() }) - } else { - if (responseJSON.errorMsg) { - alert(responseJSON.errorMsg) - } else { - alertGeneralError() - } - } - } - - /** - * On error callback during file upload - * @param error Error object - */ - uploadError = (error) => { - error.response.json().then((errorBody: DjangoResponseUploadUserFileError) => { - console.error(errorBody) - // NOTE: Parses int as Django Rest Framework returns as string - // Related issue https://github.com/encode/django-rest-framework/issues/7532 - const internalCode = errorBody && errorBody.file_obj - ? parseInt(errorBody.file_obj.status.internal_code as unknown as string) - : null - - if (internalCode === DjangoUserFileUploadErrorInternalCode.INVALID_FORMAT_NON_NUMERIC) { - alert('The file has an incorrect format: all columns except the index must be numerical data') - } else { - alertGeneralError() - } - }).catch(alertGeneralError) - console.log('Error uploading file ->', error) - } - - /** - * Uploads a file - */ - uploadFile = () => { - if (!this.newFileIsValid()) { - return - } - - const myHeaders = getDjangoHeader() - const newFileForm = this.state.newFile - - const formData = new FormData() - formData.append('name', newFileForm.newFileNameUser) - formData.append('description', newFileForm.newFileDescription) - - formData.append('file_type', newFileForm.newFileType.toString()) - - if (newFileForm.newTag) { - formData.append('tag', newFileForm.newTag.toString()) - } - - // Adds the Institution's id, if selected - newFileForm.institutions.forEach((institutionId) => { - formData.append('institutions', institutionId.toString()) - }) - - // Adds the survival columns tuples, if needed - if (newFileForm.survivalColumns.length > 0) { - formData.append('survival_columns', JSON.stringify(newFileForm.survivalColumns)) - } - - // CpG info - formData.append('is_cpg_site_id', newFileForm.isCpGSiteId.toString()) - - if (newFileForm.isCpGSiteId) { - formData.append('platform', newFileForm.platform.toString()) - } - - // Checks if it is an edition or creation - this.setState({ uploadingFile: true }, () => { - if (this.isEditing()) { - // In case of edition, just call Django REST API as no file upload is required - const editUrl = `${urlUserFilesCRUD}${newFileForm.id}/` - this.setState({ uploadingFile: true }, () => { - ky.patch(editUrl, { headers: myHeaders, body: formData, timeout: false }) - .then((response) => { - response.json().then(() => { - this.setState({ newFile: this.getDefaultNewFile() }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - alertGeneralError() - }) - }) - .catch(this.uploadError) - .finally(() => { - this.setState({ uploadingFile: false }) - }) - }) - } else { - // In case of creation, an upload in chunks is required - startUpload({ - url: urlChunkUpload, - urlComplete: urlChunkUploadComplete, - headers: myHeaders, - file: this.newFileInputRef.current.files[0], - completeData: formData, - onChunkUpload: (percentDone) => { this.setState({ uploadPercentage: percentDone }) }, - onUploadStateChange: (currentState) => { this.setState({ uploadState: currentState }) } - }).then((response) => this.uploadSuccess(response as UploadResponse)) - .catch((err) => { - console.log('Error uploading file ->', err) - alertGeneralError() - }) - .finally(() => { - this.setState({ uploadingFile: false, uploadPercentage: 0 }) - }) - } - }) - } - - /** - * Generates the modal to confirm a Tag deletion - * @returns Modal component. Null if no Tag was selected to delete - */ - getTagDeletionConfirmModals () { - if (!this.state.selectedTagToDelete) { - return null - } - - return ( - -
- -

Are you sure you want to delete the Tag "{this.state.selectedTagToDelete.name}"?

-
- - - - - - ) - } - - /** - * Generates the modal to confirm a File deletion - * @returns Modal component. Null if no File was selected to delete - */ - getFileDeletionConfirmModals () { - if (!this.state.selectedFileToDelete) { - return null - } - - const warningMessage = this.state.selectedFileToDelete.file_type === FileType.CLINICAL - ? 'This file will be UNLINKED from all the associated experiments' - : 'All the associated experiments to this file will be DELETED' - - return ( - -
- - Are you sure you want to delete the file {this.state.selectedFileToDelete.name}? {warningMessage} - - - - - - - ) - } - - /** - * Loads an existing User's file to edit its data - * @param fileToEdit Selected file to edit - */ - editFile = (fileToEdit: DjangoUserFile) => { - if (fileToEdit.file_type === FileType.CLINICAL) { - ky.get(`${downloadFileHeaders}${fileToEdit.id}`, { signal: this.abortController.signal }).then((response) => { - response.json().then((fileHeaders) => { - // Recieve file separates by , to get array of headers - const survivalTuplesPossiblesValues = fileHeaders - this.setState({ - survivalTuplesPossiblesValues, - newFile: { - id: fileToEdit.id, - newFileName: fileToEdit.name, - newFileNameUser: fileToEdit.name, - newFileType: fileToEdit.file_type, - newFileDescription: fileToEdit.description ?? '', - newTag: fileToEdit.tag ? fileToEdit.tag.id : null, - isCpGSiteId: fileToEdit.is_cpg_site_id, - platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, - institutions: fileToEdit.institutions.map((institution) => institution.id), - survivalColumns: fileToEdit.survival_columns ?? [] - } - }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - console.log('Error getting file content ->', err) - }) - } else { - this.setState({ - survivalTuplesPossiblesValues: [], - newFile: { - id: fileToEdit.id, - newFileName: fileToEdit.name, - newFileNameUser: fileToEdit.name, - newFileType: fileToEdit.file_type, - newFileDescription: fileToEdit.description ?? '', - newTag: fileToEdit.tag ? fileToEdit.tag.id : null, - isCpGSiteId: fileToEdit.is_cpg_site_id, - platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, - institutions: fileToEdit.institutions.map((institution) => institution.id), - survivalColumns: fileToEdit.survival_columns ?? [] - } - }) - } - } - - /** - * Adds a Survival data tuple - */ - addSurvivalFormTuple = () => { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: [ - ...prevState.newFile.survivalColumns, - { event_column: '', time_column: '' } - ], - }, - })) - } - - /** - * Removes a Survival data tuple for a CGDSDataset - * @param idxSurvivalTuple Index in survival tuple - */ - removeSurvivalFormTuple = (idxSurvivalTuple: number) => { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), - }, - })) - } - - /** - * Handles CGDS Dataset form changes in fields of Survival data tuples - * @param idxSurvivalTuple Index in survival tuple - * @param name Field of the CGDS dataset to change - * @param value Value to assign to the specified field - */ - handleSurvivalFormDatasetChanges = (idxSurvivalTuple: number, name: string, value: any) => { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.map((t, i) => - i === idxSurvivalTuple ? { ...t, [name]: value } : t - ), - }, - })) - } - - /** - * Generates default table's headers - * @returns Default object for table's headers - */ - getDefaultHeaders (): RowHeader[] { - return [ - { name: 'Name', serverCodeToSort: 'name' }, - { name: 'Description', serverCodeToSort: 'description', width: 3 }, - { name: 'Type', serverCodeToSort: 'file_type' }, - { name: 'Date', serverCodeToSort: 'upload_date' }, - { name: 'Institutions', width: 2 }, - { name: 'Tag', serverCodeToSort: 'tag', width: 2 }, - { name: 'Public', width: 1 }, - { name: 'Actions', width: 2 } - ] - } - - /** - * Generates default table's Filters - * @returns Default object for table's Filters - */ - getDefaultFilters (): PaginationCustomFilter[] { - const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { - const id = tag.id as number - return { key: id, value: id, text: tag.name } - }) - - tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) - - const selectVisibilityOptions = [ - { key: 'all', text: 'All', value: 'all' }, - { key: 'private', text: 'Private', value: 'private' } - ] - - const institutionsOptions: DropdownItemProps[] = this.state.userInstitutions.map((institution) => { - return { key: institution.id, value: institution.id, text: institution.name } - }) - - return [ - { label: 'Tag', keyForServer: 'tag', defaultValue: '', placeholder: 'Select existing Tag', options: tagOptions, width: 3 }, - { label: 'Visibility', keyForServer: 'visibility', defaultValue: 'all', options: selectVisibilityOptions, clearable: false, width: 2 }, - { - label: 'Institutions', - keyForServer: 'institutions', - defaultValue: '', - options: institutionsOptions, - disabledFunction: (actualValues) => actualValues.visibility === 'private', - width: 3 - }, - { - label: 'File type', - keyForServer: 'file_type', - defaultValue: FileType.ALL, - options: getFileTypeSelectOptions(), - clearable: false, - width: 2 - } - ] - } - - render () { - // Tag and File deletion modals - const tagDeletionConfirmModal = this.getTagDeletionConfirmModals() - const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { - const id = tag.id as number - return { key: id, value: id, text: tag.name } - }) - - tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) - - return ( - <> - {/* Tag deletion modal */} - {tagDeletionConfirmModal} - - - - - - - ) - } -} - -export { NewFile, BiomarkerManager } diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index ddcca840..a809ddf5 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -3,7 +3,7 @@ import { Base } from '../Base' import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' -import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource } from '../../utils/util_functions' +import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject } from '../../utils/util_functions' import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' @@ -26,7 +26,8 @@ import { SharedInstitutionsBiomarker, SharedInstitutionsBiomarkerPropsExtend } f import { EditBiomarkerIcon } from './EditBiomarkerIcon' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { PopupIcons } from '../common/PopupIcons' -import { BiomarkerManager, NewFile } from './BiomarkerManager' +import { NewFile } from './BiomarkerManager' +import { TagsPanel } from '../files-manager/TagsPanel' // URLs defined in biomarkers.html declare const urlBiomarkersCRUD: string @@ -76,6 +77,9 @@ interface BiomarkersPanelState { deletingBiomarker: boolean, /** Indicates if there's a Biomarker being stopped. */ stoppingExperiment: boolean, + newTag: DjangoTag, + selectedTagToDelete: Nullable, + addingTag: boolean, /** Biomarker to stop. */ biomarkerToStop: Nullable, addingOrEditingBiomarker: boolean, @@ -102,7 +106,8 @@ interface BiomarkersPanelState { /** modal to handle shared users */ modalUsers: SharedUsersBiomarkerPropsExtend, newFile: NewFile, - + showDeleteTagModal: boolean, + deletingTag: boolean, } /** @@ -126,6 +131,7 @@ export class BiomarkersPanel extends React.Component +
+ +

Are you sure you want to delete the Tag "{this.state.selectedTagToDelete.name}"?

+
+ + + + + + ) + } + + /** + * Handles New Tag Input changes + * @param name State field to change + * @param value Value to assign to the specified field + */ + handleAddTagInputsChange = (name: string, value) => { + const newTag = this.state.newTag + newTag[name] = value + this.setState(prevState => ({ + newTag: { + ...prevState.newTag, + [name]: value, + } + })) + } + + /** + * Makes a request to delete a Tag + */ + deleteTag = () => { + if (this.state.selectedTagToDelete === null) { + return + } + + // Sets the Request's Headers + const myHeaders = getDjangoHeader() + const deleteURL = `${urlTagsCRUD}${this.state.selectedTagToDelete.id}` + this.setState({ deletingTag: true }, () => { + ky.delete(deleteURL, { headers: myHeaders }).then((response) => { + // If OK is returned refresh the tags + if (response.ok) { + this.setState({ + deletingTag: false, + showDeleteTagModal: false + }) + this.getUserTags() + } + }).catch((err) => { + this.setState({ deletingTag: false }) + alertGeneralError() + console.log('Error deleting Tag ->', err) + }) + }) + } + + /** + * Handles New Tag Input Key Press + * @param e Event of change + */ + handleKeyDown = (e) => { + // If pressed Enter key submits the new Tag + if (e.which === 13 || e.keyCode === 13) { + this.addOrEditTag() + } else { + if (e.which === 27 || e.keyCode === 27) { + this.setState({ newTag: getDefaultNewTag() }) + } + } + } + + /** + * Show a modal to confirm a Tag deletion + * @param tag Selected Tag to delete + */ + confirmTagDeletion = (tag: DjangoTag) => { + this.setState({ + selectedTagToDelete: tag, + showDeleteTagModal: true + }) + } + + /** + * Does a request to add a new Tag + */ + addOrEditTag () { + if (this.state.addingTag) { + return + } + + // Sets the Request's Headers + const myHeaders = getDjangoHeader() + + // If exists an id then we are editing, otherwise It's a new Tag + let addOrEditURL, requestMethod + + if (this.state.newTag.id !== null) { + addOrEditURL = `${urlTagsCRUD}${this.state.newTag.id}/` + requestMethod = ky.patch + } else { + addOrEditURL = urlTagsCRUD + requestMethod = ky.post + } + + this.setState({ addingTag: true }, () => { + requestMethod(addOrEditURL, { headers: myHeaders, json: this.state.newTag }).then((response) => { + this.setState({ addingTag: false }) + response.json().then((responseJSON: DjangoTag) => { + if (responseJSON && responseJSON.id) { + // If all is OK, resets the form and gets the User's tag to refresh the list + this.setState({ newTag: getDefaultNewTag() }) + this.getUserTags() + } + }).catch((err) => { + alertGeneralError() + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + this.setState({ addingTag: false }) + alertGeneralError() + console.log('Error adding new Tag ->', err) + }) + }) + } + + /** + * Selects a new Tag to edit + * @param selectedTag Tag to edit + */ + editTag = (selectedTag: DjangoTag) => { this.setState({ newTag: copyObject(selectedTag) }) } + render () { // Biomarker deletion modal const deletionConfirmModal = this.getDeletionConfirmModal() @@ -1839,6 +1996,14 @@ export class BiomarkersPanel extends React.Component { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + + tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) return ( {/* Biomarker deletion modal */} @@ -1847,12 +2012,23 @@ export class BiomarkersPanel extends React.Component - this.setState({ tags })} - /> + + + + Date: Sun, 31 May 2026 13:52:59 -0300 Subject: [PATCH 3/9] Fix importations --- .../frontend/src/components/biomarkers/BiomarkersPanel.tsx | 3 +-- .../biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx | 2 +- .../manualForm/newBiomarkerForm/NewBiomarkerForm.tsx | 2 +- .../experiment-result/BiomarkerFromCorrelationModal.tsx | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index a809ddf5..9b6bd975 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -18,7 +18,6 @@ import { BiomarkerStateLabel } from './labels/BiomarkerStateLabel' import { BiomarkerOriginLabel } from './BiomarkerOriginLabel' import { BiomarkerDetailsModal } from './BiomarkerDetailsModal' import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters, getNumberOfMoleculesOfBiomarker } from './utils' - import { StopExperimentButton } from '../pipeline/all-experiments-view/StopExperimentButton' import { DeleteButton } from '../common/DeleteButton' import { SharedUsersBiomarker, SharedUsersBiomarkerPropsExtend } from './SharedUsersBiomarker' @@ -26,8 +25,8 @@ import { SharedInstitutionsBiomarker, SharedInstitutionsBiomarkerPropsExtend } f import { EditBiomarkerIcon } from './EditBiomarkerIcon' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { PopupIcons } from '../common/PopupIcons' -import { NewFile } from './BiomarkerManager' import { TagsPanel } from '../files-manager/TagsPanel' +import { NewFile } from '../files-manager/FilesManager' // URLs defined in biomarkers.html declare const urlBiomarkersCRUD: string diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index f40a0f88..a7cb8319 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -3,8 +3,8 @@ import { DropdownItemProps, Grid } from 'semantic-ui-react' import { BiomarkerType, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection } from './../../types' import { NewBiomarkerForm } from './newBiomarkerForm/NewBiomarkerForm' import { MoleculesSectionsContainer } from './MoleculeSectionContainer' -import { NewFile } from '../../BiomarkerManager' import { DjangoTag } from '../../../../utils/django_interfaces' +import { NewFile } from '../../../files-manager/FilesManager' /** ManualForm's props. */ interface ManualFormProps { diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx index 3df1742e..8d2cea38 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx @@ -5,8 +5,8 @@ import { BiomarkerType, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOf import { ButtonsForTypeOfInsert } from './ButtonsForTypeOfInsert' import { SelectDropDownSingleMolecule } from './SelectDropDownSingleMolecule' import { InfoPopup } from '../../../../pipeline/experiment-result/gene-gem-details/InfoPopup' -import { NewFile } from '../../../BiomarkerManager' import { DjangoTag } from '../../../../../utils/django_interfaces' +import { NewFile } from '../../../../files-manager/FilesManager' /** All the types of molecules in a Biomarker. */ const BIOMARKER_OPTIONS = [ diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx index 5f62c7ca..aa442a86 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx @@ -12,7 +12,7 @@ import { isEqual } from 'lodash' import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters } from '../../biomarkers/utils' import { BiomarkerDetailsModal } from '../../biomarkers/BiomarkerDetailsModal' import { Alert } from '../../common/Alert' -import { NewFile } from '../../biomarkers/BiomarkerManager' +import { NewFile } from '../../files-manager/FilesManager' // URLs defined in gem.html declare const urlBiomarkersCRUD: string From 805452c076184eb02535d6912a942bed02cb553e Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Thu, 4 Jun 2026 19:35:07 -0300 Subject: [PATCH 4/9] Fix unused props and methods --- .../manualForm/newBiomarkerForm/NewBiomarkerForm.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx index 8d2cea38..c4035cd7 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/newBiomarkerForm/NewBiomarkerForm.tsx @@ -6,7 +6,6 @@ import { ButtonsForTypeOfInsert } from './ButtonsForTypeOfInsert' import { SelectDropDownSingleMolecule } from './SelectDropDownSingleMolecule' import { InfoPopup } from '../../../../pipeline/experiment-result/gene-gem-details/InfoPopup' import { DjangoTag } from '../../../../../utils/django_interfaces' -import { NewFile } from '../../../../files-manager/FilesManager' /** All the types of molecules in a Biomarker. */ const BIOMARKER_OPTIONS = [ @@ -39,14 +38,10 @@ interface NewBiomarkerFormProps { biomarkerForm: FormBiomarkerData, /** Value for Checkbox. */ checkedIgnoreProposedAlias: boolean, - newFile: NewFile, tagOptions: DropdownItemProps[], tags: DjangoTag[], - uploadingFile: boolean, /** Handle change for Checkbox. */ handleChangeIgnoreProposedAlias: (value: boolean) => void, - handleAddFileInputsChange: (name: string, value: any) => void, - isFormEmpty: () => boolean, cleanForm: () => void, handleChangeMoleculeSelected: (name: BiomarkerType) => void, handleChangeMoleculeInputSelected: (value: MoleculesTypeOfSelection) => void, From f2550169de000cdb42bce0a2ec771459df61ef43 Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Thu, 4 Jun 2026 20:39:56 -0300 Subject: [PATCH 5/9] Fix tags not rendered and clean unused props --- .../components/biomarkers/BiomarkersPanel.tsx | 134 ++++++------------ .../manualForm/ManualForm.tsx | 4 - 2 files changed, 44 insertions(+), 94 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 9b6bd975..70e0f99a 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -1,10 +1,10 @@ import React from 'react' import { Base } from '../Base' import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' +import { DjangoCGDSStudy, DjangoInstitution, DjangoMethylationPlatform, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject } from '../../utils/util_functions' -import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' +import { Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' @@ -44,6 +44,7 @@ declare const maxFeaturesBlindSearch: number declare const minFeaturesMetaheuristics: number declare const urlCloneBiomarker: string declare const urlStopFSExperiment: string +declare const urlUserInstitutions: string const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds const FILE_INPUT_LABEL = 'Add a new file' @@ -65,8 +66,6 @@ type ValidationForm = { /** BiomarkersPanel's state */ interface BiomarkersPanelState { - biomarkers: BiomarkerSimple[], - newBiomarker: Biomarker, /** PK of the Biomarker that's being loaded. */ loadingFullBiomarkerId: Nullable, selectedBiomarkerToDeleteOrSync: Nullable, @@ -96,10 +95,10 @@ interface BiomarkersPanelState { openDetailsModal: boolean, /** Selected Biomarker instance to show its details. */ selectedBiomarker: Nullable, + /** Alert structure to display messages. */ alert: CustomAlert, featureSelection: FeatureSelectionPanelData, submittingFSExperiment: boolean, - openDetailsModal2: boolean, /** modal to handle shared institutions */ modalInstitutions: SharedInstitutionsBiomarkerPropsExtend, /** modal to handle shared users */ @@ -107,6 +106,8 @@ interface BiomarkersPanelState { newFile: NewFile, showDeleteTagModal: boolean, deletingTag: boolean, + userInstitutions: DjangoInstitution[], + uploadingFile: boolean, } /** @@ -118,8 +119,6 @@ export class BiomarkersPanel extends React.Component { + response.json().then((userInstitutions) => { + this.setState({ userInstitutions }) + }).catch((err) => { + console.log('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.log("Error getting user's tags ->", err) + }) + } + + /** + * Prevents users from closing browser tag when upload is in process. + * @param e Event + */ + onUnload = e => { // the method that will be used for both add and remove event + if (this.state.uploadingFile) { + e.preventDefault() + e.returnValue = 'A file is being uploaded. If you close the tab the upload will be canceled.' + } + } + /** * Generates default feature selection creation structure * @returns Default the default Alert @@ -1420,46 +1455,6 @@ export class BiomarkersPanel extends React.Component { - return !this.state.addingOrEditingBiomarker && - this.state.newBiomarker.name.trim().length > 0 - } - - /** - * Handles Biomarker form changes - * @param name Name of the state field to modify - * @param value Value to set to the state field - */ - handleFormChanges = (name: string, value) => { - const newBiomarker = this.state.newBiomarker - newBiomarker[name] = value - this.setState({ newBiomarker }) - } - - /** - * TODO: Check if needed - * Adds a Survival data tuple for a CGDSDataset - * @param datasetName Name of the edited CGDS dataset - */ - addSurvivalFormTuple = (datasetName: NameOfCGDSDataset) => { - const newBiomarker = this.state.newBiomarker - const dataset = newBiomarker[datasetName] - - if (dataset !== null) { - const newElement: DjangoSurvivalColumnsTupleSimple = { event_column: '', time_column: '' } - - if (dataset.survival_columns === undefined) { - dataset.survival_columns = [] - } - - dataset.survival_columns.push(newElement) - this.setState({ newBiomarker }) - } - } - /** * Removes a Survival data tuple for a CGDSDataset * @param idxSurvivalTuple Index in survival tuple diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index a7cb8319..f6dead15 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -47,7 +47,6 @@ export const ManualForm = (props: ManualFormProps) => { handleChangeInputForm={props.handleChangeInputForm} biomarkerForm={props.biomarkerForm} cleanForm={props.cleanForm} - isFormEmpty={props.isFormEmpty} checkedIgnoreProposedAlias={props.checkedIgnoreProposedAlias} handleChangeIgnoreProposedAlias={props.handleChangeIgnoreProposedAlias} handleChangeMoleculeSelected={props.handleChangeMoleculeSelected} @@ -60,9 +59,6 @@ export const ManualForm = (props: ManualFormProps) => { handleSendForm={props.handleSendForm} handleChangeCheckBox={props.handleChangeCheckBox} tagOptions={props.tagOptions} - newFile={props.newFile} - uploadingFile={props.uploadingFile} - handleAddFileInputsChange={props.handleAddFileInputsChange} tags={props.tags} /> From 9af6d06c4f5e108aa5578bca87d4898b3c3999c5 Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Fri, 5 Jun 2026 11:46:40 -0300 Subject: [PATCH 6/9] remove unused method --- .../components/biomarkers/BiomarkersPanel.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 70e0f99a..6b39dd63 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -232,22 +232,6 @@ export class BiomarkersPanel extends React.Component { - response.json().then((userInstitutions) => { - this.setState({ userInstitutions }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - console.log("Error getting user's tags ->", err) - }) } /** From 149af80c5b5c8c3aa7c95b3eadec521c6b78687a Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Fri, 5 Jun 2026 11:47:21 -0300 Subject: [PATCH 7/9] fix unused const --- .../frontend/src/components/biomarkers/BiomarkersPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 6b39dd63..4921d5cc 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -44,7 +44,6 @@ declare const maxFeaturesBlindSearch: number declare const minFeaturesMetaheuristics: number declare const urlCloneBiomarker: string declare const urlStopFSExperiment: string -declare const urlUserInstitutions: string const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds const FILE_INPUT_LABEL = 'Add a new file' From a0d5a3d4ecaa8a243cd4fc70225b7428484e84d7 Mon Sep 17 00:00:00 2001 From: GonzaGomez Date: Thu, 11 Jun 2026 22:19:56 -0300 Subject: [PATCH 8/9] Remove unused methods and imports --- .../components/biomarkers/BiomarkersPanel.tsx | 20 +------------------ .../manualForm/ManualForm.tsx | 3 --- .../BiomarkerFromCorrelationModal.tsx | 2 -- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 4921d5cc..421d27a3 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Base } from '../Base' import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoInstitution, DjangoMethylationPlatform, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' +import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject } from '../../utils/util_functions' import { Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' @@ -105,8 +105,6 @@ interface BiomarkersPanelState { newFile: NewFile, showDeleteTagModal: boolean, deletingTag: boolean, - userInstitutions: DjangoInstitution[], - uploadingFile: boolean, } /** @@ -126,7 +124,6 @@ export class BiomarkersPanel extends React.Component { // the method that will be used for both add and remove event - if (this.state.uploadingFile) { - e.preventDefault() - e.returnValue = 'A file is being uploaded. If you close the tab the upload will be canceled.' - } - } - /** * Generates default feature selection creation structure * @returns Default the default Alert @@ -2231,9 +2215,7 @@ export class BiomarkersPanel extends React.Component diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index f6dead15..0854124d 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -4,15 +4,12 @@ import { BiomarkerType, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOf import { NewBiomarkerForm } from './newBiomarkerForm/NewBiomarkerForm' import { MoleculesSectionsContainer } from './MoleculeSectionContainer' import { DjangoTag } from '../../../../utils/django_interfaces' -import { NewFile } from '../../../files-manager/FilesManager' /** ManualForm's props. */ interface ManualFormProps { tagOptions: DropdownItemProps[] tags: DjangoTag[] - uploadingFile: boolean handleAddFileInputsChange: (name: string, value: any) => void - newFile: NewFile biomarkerForm: FormBiomarkerData, /** Value for Checkbox. */ checkedIgnoreProposedAlias: boolean, diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx index aa442a86..0454879c 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx @@ -1587,9 +1587,7 @@ export class BiomarkerFromCorrelationModal extends React.Component Date: Mon, 15 Jun 2026 11:35:32 -0300 Subject: [PATCH 9/9] Removed unused functions and state variables --- .../components/biomarkers/BiomarkersPanel.tsx | 71 +------------------ .../manualForm/ManualForm.tsx | 3 - .../BiomarkerFromCorrelationModal.tsx | 71 +------------------ 3 files changed, 6 insertions(+), 139 deletions(-) diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 421d27a3..1dc764cc 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -1,10 +1,10 @@ import React from 'react' import { Base } from '../Base' import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' +import { DjangoCGDSStudy, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject } from '../../utils/util_functions' -import { Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal, FileType } from '../../utils/interfaces' +import { Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' @@ -26,7 +26,6 @@ import { EditBiomarkerIcon } from './EditBiomarkerIcon' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { PopupIcons } from '../common/PopupIcons' import { TagsPanel } from '../files-manager/TagsPanel' -import { NewFile } from '../files-manager/FilesManager' // URLs defined in biomarkers.html declare const urlBiomarkersCRUD: string @@ -46,7 +45,7 @@ declare const urlCloneBiomarker: string declare const urlStopFSExperiment: string const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds -const FILE_INPUT_LABEL = 'Add a new file' + /** A matched molecule with the search query and the validated alias. */ type MoleculeFinderResult = { molecule: string, standard: string } @@ -102,7 +101,6 @@ interface BiomarkersPanelState { modalInstitutions: SharedInstitutionsBiomarkerPropsExtend, /** modal to handle shared users */ modalUsers: SharedUsersBiomarkerPropsExtend, - newFile: NewFile, showDeleteTagModal: boolean, deletingTag: boolean, } @@ -141,7 +139,6 @@ export class BiomarkersPanel extends React.Component { - const newFileForm = this.state.newFile - newFileForm[name] = value - this.setState({ newFile: newFileForm }) - } - /** * Cleans the new/edit biomarker form */ @@ -1475,19 +1443,6 @@ export class BiomarkersPanel extends React.Component { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), - }, - })) - } - /** * Generates the modal to confirm a biomarker deletion * @returns Modal component. Null if no Tag was selected to delete @@ -1564,23 +1519,6 @@ export class BiomarkersPanel extends React.Component { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.map((t, i) => - i === idxSurvivalTuple ? { ...t, [name]: value } : t - ), - }, - })) - } - /** * Function to go back to step 1 */ @@ -2215,9 +2153,6 @@ export class BiomarkersPanel extends React.Component )} diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index 0854124d..6fe5765c 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -9,14 +9,11 @@ import { DjangoTag } from '../../../../utils/django_interfaces' interface ManualFormProps { tagOptions: DropdownItemProps[] tags: DjangoTag[] - handleAddFileInputsChange: (name: string, value: any) => void biomarkerForm: FormBiomarkerData, /** Value for Checkbox. */ checkedIgnoreProposedAlias: boolean, /** Handle change for Checkbox. */ handleChangeIgnoreProposedAlias: (value: boolean) => void, - removeSurvivalFormTuple: (idx: number) => void, - handleSurvivalFormDatasetChanges: (idx: number, name: string, value) => void, cleanForm: () => void, isFormEmpty: () => boolean, handleChangeMoleculeSelected: (value: BiomarkerType) => void, diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx index 0454879c..c8e47821 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx @@ -1,10 +1,10 @@ import React from 'react' // Update the import path to the correct location of Base component import { Modal, DropdownItemProps, Icon, Form, Button, Confirm } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoMethylationPlatform, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces' +import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource } from '../../../utils/util_functions' -import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, ConfirmModal, ExperimentInfo, ExperimentResultTableControl, FileType } from '../../../utils/interfaces' +import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, ConfirmModal, ExperimentInfo, ExperimentResultTableControl } from '../../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from '../../biomarkers/types' import { ManualForm } from '../../biomarkers/modalContentBiomarker/manualForm/ManualForm' import { PaginationCustomFilter } from '../../common/PaginatedTable' @@ -12,7 +12,6 @@ import { isEqual } from 'lodash' import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters } from '../../biomarkers/utils' import { BiomarkerDetailsModal } from '../../biomarkers/BiomarkerDetailsModal' import { Alert } from '../../common/Alert' -import { NewFile } from '../../files-manager/FilesManager' // URLs defined in gem.html declare const urlBiomarkersCRUD: string @@ -26,7 +25,7 @@ declare const urlMethylationSitesFinder: string declare const urlGeneSymbolsFinder: string const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds -const FILE_INPUT_LABEL = 'Add a new file' + type SelectedOption = 'selectAll' | 'selectWithFilters' /** A matched molecule with the search query and the validated alias. */ @@ -87,7 +86,6 @@ interface BiomarkerFromCorrelationModalState { openSelectOptionModal: boolean, experimentInfoWithoutFilters: ExperimentInfo, modalReady: boolean, - newFile: NewFile, } /** @@ -128,7 +126,6 @@ export class BiomarkerFromCorrelationModal extends React.Component { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.filter((_, i) => i !== idxSurvivalTuple), - }, - })) - } - - /** - * Handles CGDS Dataset form changes in fields of Survival data tuples - * @param idxSurvivalTuple Index in survival tuple - * @param name Field of the CGDS dataset to change - * @param value Value to assign to the specified field - */ - handleSurvivalFormDatasetChanges = (idxSurvivalTuple: number, name: string, value: any) => { - this.setState(prevState => ({ - newFile: { - ...prevState.newFile, - survivalColumns: prevState.newFile.survivalColumns.map((t, i) => - i === idxSurvivalTuple ? { ...t, [name]: value } : t - ), - }, - })) - } - /** * Checks if the form is entirely empty. Useful to enable 'Cancel' button * @returns True is any of the form's field contains any data. False otherwise @@ -1433,17 +1382,6 @@ export class BiomarkerFromCorrelationModal extends React.Component { - const newFileForm = this.state.newFile - newFileForm[name] = value - this.setState({ newFile: newFileForm }) - } - /** * Generates default table's Filters. * @returns Default object for table's Filters @@ -1587,9 +1525,6 @@ export class BiomarkerFromCorrelationModal extends React.Component