=> [
+ {
+ title: "ID",
+ dataIndex: "id",
+ key: "id",
+ //fixed: 'left',
+ width: 70,
+ onHeaderCell: compactHeaderCell,
+ onCell: () => nowrapCell,
+ render: (id: number) => {id},
+ },
+ {
+ title: "Readset",
+ dataIndex: "name",
+ key: "name",
+ //fixed: 'left',
+ width: 450,
+ onHeaderCell: compactHeaderCell,
+ render: (name: string) => {name},
+ },
+ {
+ title: "Sample",
+ dataIndex: "readset_sample_name",
+ key: "readset_sample_name",
+ width: 450,
+ onHeaderCell: compactHeaderCell,
+ filterIcon: (filtered) => (
+
+ ),
+ filterDropdown: ({
+ setSelectedKeys,
+ selectedKeys,
+ confirm,
+ clearFilters,
+ }: FilterDropdownProps) => (
+
+ {
+ setSelectedKeys(event.target.value ? [event.target.value] : [])
+ }}
+ onPressEnter={() => confirm()}
+ style={{
+ marginBottom: 8,
+ display: "block",
+ }}
+ />
+
+
+
+
+
+ ),
+ onFilter: (value, record) =>
+ String(record.readset_sample_name ?? "")
+ .toLowerCase()
+ .includes(String(value).toLowerCase()),
+ },
+ {
+ title: "Alias",
+ dataIndex: "alias",
+ key: "alias",
+ width: 450,
+ onHeaderCell: compactHeaderCell,
+ render: (alias: string | null) => alias || N/A,
+ },
+ {
+ title: "Cohort",
+ dataIndex: "cohort",
+ key: "cohort",
+ width: 120,
+ onHeaderCell: compactHeaderCell,
+ render: (cohort: string | null) => cohort || N/A,
+ },
+ {
+ title: "Library Type",
+ dataIndex: "library_type",
+ key: "library_type",
+ width: 140,
+ onHeaderCell: compactHeaderCell,
+ filters: libraryTypeFilters,
+ filterIcon: (filtered) => (
+
+ ),
+ onFilter: (value, record) => record.library_type === value,
+ render: (libraryType: string | null) =>
+ libraryType ? {libraryType} : N/A,
+ },
+ {
+ title: "Run",
+ dataIndex: "run_name",
+ key: "run_name",
+ width: 260,
+ onHeaderCell: compactHeaderCell,
+ filterIcon: (filtered) => (
+
+ ),
+ filterDropdown: ({
+ setSelectedKeys,
+ selectedKeys,
+ confirm,
+ clearFilters,
+ }: FilterDropdownProps) => (
+
+ {
+ setSelectedKeys(event.target.value ? [event.target.value] : [])
+ }}
+ onPressEnter={() => confirm()}
+ style={{ marginBottom: 8, display: "block" }}
+ />
+
+
+
+ ),
+
+ onFilter: (value, record) =>
+ String(record.run_name ?? "")
+ .toLowerCase()
+ .includes(String(value).toLowerCase()),
+ },
+ {
+ title: "Run Start",
+ dataIndex: "run_start_date",
+ key: "run_start_date",
+ width: 120,
+ onHeaderCell: compactHeaderCell,
+ filterIcon: (filtered) => (
+
+ ),
+ filterDropdown: ({
+ setSelectedKeys,
+ selectedKeys,
+ confirm,
+ clearFilters,
+ }: FilterDropdownProps) => (
+
+ {
+ const [startDate, endDate] = String(selectedKeys[0]).split("|")
+
+ return startDate && endDate
+ ? ([dayjs(startDate), dayjs(endDate)] as [Dayjs, Dayjs])
+ : null
+ })()
+ : null
+ }
+ onChange={(dates) => {
+ if (!dates || !dates[0] || !dates[1]) {
+ setSelectedKeys([])
+ return
+ }
+
+ setSelectedKeys([`${dates[0].format("YYYY-MM-DD")}|${dates[1].format("YYYY-MM-DD")}`])
+ }}
+ />
+
+
+
+ ),
+
+ onFilter: (value, record) => {
+ const [startDate, endDate] = String(value).split("|")
+
+ if (!startDate || !endDate) {
+ return true
+ }
+
+ return record.run_start_date >= startDate && record.run_start_date <= endDate
+ },
+ },
+ {
+ title: "Validation Status",
+ dataIndex: "run_validation_status",
+ key: "run_validation_status",
+ width: 170,
+ onHeaderCell: compactHeaderCell,
+ render: (validationStatus: ValidationStatus | null) =>
+ validationStatus === null ? (
+ N/A
+ ) : (
+
+ ),
+ },
+ // {
+ // title: 'Container Barcodes',
+ // dataIndex: 'barcodes',
+ // key: 'barcodes',
+ // width: 280,
+ // onHeaderCell: compactHeaderCell,
+ // render: (barcodes: string[]) =>
+ // barcodes?.length ? (
+ //
+ // {barcodes.map((barcode) => (
+ // {barcode}
+ // ))}
+ //
+ // ) : (
+ // N/A
+ // ),
+ // },
+ {
+ title: "Reads",
+ dataIndex: "number_of_reads",
+ key: "number_of_reads",
+ align: "right",
+ width: 180,
+ onHeaderCell: compactHeaderCell,
+ render: (reads: number | null) =>
+ reads !== null ? reads.toLocaleString("fr-CA") : N/A,
+ },
+ {
+ title: "Avg Quality",
+ dataIndex: "average_quality",
+ key: "average_quality",
+ align: "right",
+ width: 100,
+ onHeaderCell: compactHeaderCell,
+ render: (value: string | null) =>
+ value !== null ? Number(value).toFixed(2) : N/A,
+ },
+ {
+ title: "% PF Aligned",
+ dataIndex: "pf_reads_aligned",
+ key: "pf_reads_aligned",
+ align: "right",
+ width: 100,
+ onHeaderCell: compactHeaderCell,
+ render: (value: string | null) =>
+ value !== null ? `${(Number(value) * 100).toFixed(2)}` : N/A,
+ },
+ {
+ title: "% Duplicate",
+ dataIndex: "duplicate_aligned",
+ key: "duplicate_aligned",
+ align: "right",
+ width: 100,
+ onHeaderCell: compactHeaderCell,
+ render: (value: string | null) =>
+ value !== null ? `${(Number(value) * 100).toFixed(2)}` : N/A,
+ },
+ {
+ title: "Readset Files",
+ dataIndex: "readset_files",
+ key: "readset_files",
+ onHeaderCell: compactHeaderCell,
+ render: (files?: ProjectOverviewReadset["readset_files"] | null) =>
+ files?.length ? (
+
+ {files.map((file, index) =>
+ file.file_path ? (
+
+
+
+ {file.size !== null && file.size !== undefined
+ ? `${(Number(file.size) / 1024 / 1024).toFixed(2)} MB`
+ : "N/A"}
+
+
+ ) : null,
+ )}
+
+ ) : (
+ N/A
+ ),
+ },
+]
+
+const formatReadsetFilesForCsv = (files: ProjectOverviewReadset["readset_files"]): string => {
+ if (!files?.length) {
+ return ""
+ }
+
+ return files
+ .flatMap((file) => {
+ if (!file.file_path) {
+ return []
+ }
+
+ if (file.size === null || file.size === undefined) {
+ return [file.file_path]
+ }
+
+ const sizeInMb = (Number(file.size) / 1024 / 1024).toFixed(2)
+ return [`${file.file_path} (${sizeInMb} MB)`]
+ })
+ .join("; ")
+}
+
+function ProjectReadSetsTab({ parentProjectId, externalID, isActive }: ProjectReadSetsTabProps) {
+ const [projectOverviewReadsets, setProjectOverviewReadsets] = useState(
+ [],
+ )
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const dispatch = useAppDispatch()
+
+ // Charge les Read Sets associés au projet parent donné.
+ const fetchReadsetsByParentProjectID = useCallback(
+ async (parentProjectId: number): Promise => {
+ const response = await dispatch(
+ api.parentProjects.readsets(
+ parentProjectId,
+ {
+ limit: 100000,
+ },
+ true,
+ ),
+ )
+
+ return response.data.results
+ },
+ [dispatch],
+ )
+
+ // Charge les Read Sets du projet parent et met à jour l’état du composant.
+ const loadParentProjectReadsets = useCallback(
+ async (parentProjectId: number): Promise => {
+ try {
+ setIsLoading(true)
+ setError(null)
+
+ const fetchedReadsets = await fetchReadsetsByParentProjectID(parentProjectId)
+ setProjectOverviewReadsets(fetchedReadsets)
+ } catch (error) {
+ setProjectOverviewReadsets([])
+ setError(error instanceof Error ? error.message : "Failed to fetch read sets")
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ [fetchReadsetsByParentProjectID],
+ )
+
+ useEffect(() => {
+ if (!isActive) {
+ return
+ }
+
+ if (parentProjectId === null) {
+ setProjectOverviewReadsets([])
+ setError("Invalid parent project ID")
+ return
+ }
+
+ loadParentProjectReadsets(parentProjectId)
+ }, [isActive, parentProjectId, loadParentProjectReadsets])
+
+ const exportReadsets = useMemo(
+ () =>
+ projectOverviewReadsets.map((readset) => ({
+ ...readset,
+ readset_files: formatReadsetFilesForCsv(readset.readset_files),
+ })),
+ [projectOverviewReadsets],
+ )
+
+ const generateCsvContent = useCreateCsvExportFunction(exportReadsets)
+
+ const libraryTypeFilters = Array.from(
+ new Set(
+ projectOverviewReadsets
+ .map((readset) => readset.library_type)
+ .filter((libraryType): libraryType is string => Boolean(libraryType)),
+ ),
+ ).map((libraryType) => ({
+ text: libraryType,
+ value: libraryType,
+ }))
+
+ const projectOverviewReadsetColumns = useMemo(
+ () => getProjectOverviewReadsetColumns(libraryTypeFilters),
+ [libraryTypeFilters],
+ )
+
+ if (isLoading) {
+ return
+ }
+
+ if (error) {
+ return
+ }
+
+ const exportButtonData: ProjectOverviewExportButtonData = {
+ exportType: "Project Readsets",
+ exportFunction: generateCsvContent,
+ filename: "Project Readsets",
+ itemsCount: projectOverviewReadsets.length,
+ disabled: projectOverviewReadsets.length === 0,
+ }
+
+ return (
+ <>
+ {!isLoading && isActive && }
+ {!isLoading && projectOverviewReadsets.length > 0 && (
+
+ )}
+ {projectOverviewReadsets.length > 0 ? (
+ `${range[0]}-${range[1]} of ${total} readsets`,
+ }}
+ />
+ ) : (
+
+ )}
+ >
+ )
+}
+
+export default ProjectReadSetsTab
diff --git a/frontend/src/components/projectOverview/ProjectSubmissionsTab.tsx b/frontend/src/components/projectOverview/ProjectSubmissionsTab.tsx
new file mode 100644
index 0000000000..0daec1c15d
--- /dev/null
+++ b/frontend/src/components/projectOverview/ProjectSubmissionsTab.tsx
@@ -0,0 +1,129 @@
+import React, { useCallback, useMemo } from "react"
+
+import ExternalIDProjectsDashboard from "./ExternalIDProjectDashboard"
+import { Empty, Table } from "antd"
+import { Link } from "react-router-dom"
+
+import { FMSProject } from "../../models/fms_api_models"
+
+import { useCreateCsvExportFunction } from "./useCsvExport"
+import { ProjectOverviewExportButtonData } from "./types"
+import ProjectOverviewExportButton from "./ProjectOverviewExportButton"
+
+interface ProjectSubmissionsTabProps {
+ internalProjects: FMSProject[]
+ isLoading: boolean
+ externalID: string
+}
+
+const submissionColumns = [
+ {
+ title: "ID",
+ dataIndex: "id",
+ key: "id",
+ render: (id: number) => {id},
+ },
+ {
+ title: "Project Submissions Names",
+ dataIndex: "name",
+ key: "name",
+ render: (name: string, project: FMSProject) => (
+ {name}
+ ),
+ },
+ {
+ title: "External ID",
+ dataIndex: "external_id",
+ key: "external_id",
+ },
+ {
+ title: "Principal Investigator",
+ dataIndex: "principal_investigator",
+ key: "principal_investigator",
+ },
+ {
+ title: "Requestor Name",
+ dataIndex: "requestor_name",
+ key: "requestor_name",
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ key: "status",
+ },
+ {
+ title: "Created At",
+ dataIndex: "created_at",
+ key: "created_at",
+ render: (createdAt: string) =>
+ createdAt
+ ? new Date(createdAt).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })
+ : "",
+ },
+]
+
+const ProjectSubmissionsTab = ({
+ internalProjects,
+ isLoading,
+ externalID,
+}: ProjectSubmissionsTabProps) => {
+ const exportProjects = useMemo[]>(
+ () =>
+ internalProjects.map((project) => ({
+ id: project.id,
+ name: project.name,
+ external_id: project.external_id ?? "",
+ principal_investigator: project.principal_investigator,
+ requestor_name: project.requestor_name,
+ status: project.status,
+ created_at: project.created_at,
+ })),
+ [internalProjects],
+ )
+
+ const generateCsvContent = useCreateCsvExportFunction(exportProjects)
+
+ if (!isLoading && internalProjects.length === 0) {
+ return
+ }
+
+ const exportButtonData: ProjectOverviewExportButtonData = {
+ exportType: "Associated Projects",
+ exportFunction: generateCsvContent,
+ filename: "Associated Projects",
+ itemsCount: internalProjects.length,
+ disabled: internalProjects.length === 0,
+ }
+
+ return (
+ <>
+ {!isLoading && (
+
+ )}
+ {!isLoading && }
+
+ `${range[0]}-${range[1]} of ${total} items`,
+ }}
+ />
+ >
+ )
+}
+
+export default ProjectSubmissionsTab
diff --git a/frontend/src/components/projectOverview/types.ts b/frontend/src/components/projectOverview/types.ts
new file mode 100644
index 0000000000..d9f0093127
--- /dev/null
+++ b/frontend/src/components/projectOverview/types.ts
@@ -0,0 +1,103 @@
+import { FMSProject } from "../../models/fms_api_models"
+
+export type ExternalIDProjectSample = {
+ biosample_id: number
+ id: number
+ external_id: string
+ project_id: number
+ project_name: string
+ name: string
+ alias: string | null
+ container?: string | null
+ individual: string | null
+ creation_date?: string | null
+ collection_site: string | null
+ comment?: string | null
+ experimental_group: string[]
+ volume?: number | null
+ concentration?: number | null
+ quality_flag?: boolean | null
+ quantity_flag?: boolean | null
+ identity_flag?: boolean | null
+ number_of_reads: number
+ last_process_id?: number | null
+ last_process_name?: string | null
+ last_process_execution_date?: string | null
+}
+
+export type ExternalIDProjectSamplesSummary = {
+ total_samples: number
+
+ qc_passed_count: number
+ qc_review_count: number
+ missing_qc_count: number
+
+ samples_with_assigned_process_count: number
+ samples_without_assigned_process_count: number
+ samples_assigned_to_a_process_rate: number
+
+ total_quantity: number
+ avg_concentration: number | null
+
+ total_reads: number | null
+ avg_reads_per_sample: number | null
+}
+
+export type ExternalIDProjectSamplesResponse = {
+ external_id: string
+ count: number
+ summary: ExternalIDProjectSamplesSummary
+ samples: ExternalIDProjectSample[]
+}
+
+export type ProjectOverviewReadsetFile = {
+ file_path: string | null
+ size: number | null
+}
+
+export type ProjectOverviewReadset = {
+ id: number
+ name: string
+ readset_sample_name: string
+ biosample_id: number | null
+ external_id: string
+ run_name: string
+ run_start_date: string // YYYY-MM-DD
+ run_validation_status: number | null
+
+ alias: string | null
+ cohort: string | null
+ library_type: string | null
+
+ barcodes: string[]
+
+ number_of_reads: number | null
+ number_of_bases: number | null
+
+ average_quality: string | null
+ pf_reads_aligned: string | null
+ duplicate_aligned: string | null
+
+ lane: number
+ reference_genome_id: number | null
+ reference_genome_assembly_name: string | null
+ sequencing_index_name: string | null
+
+ readset_files?: ProjectOverviewReadsetFile[]
+}
+
+export interface ProjectOverviewExportButtonData {
+ exportType: string
+ exportFunction: () => Promise
+ filename: string
+ itemsCount: number
+ disabled: boolean
+}
+
+export type ProjectsByExternalIDGroup = {
+ external_id: string | null
+ external_id_number: number | null
+ external_project_name: string | null
+ project_count: number
+ projects: FMSProject[]
+}
diff --git a/frontend/src/components/projectOverview/useCsvExport.ts b/frontend/src/components/projectOverview/useCsvExport.ts
new file mode 100644
index 0000000000..c1970f46a6
--- /dev/null
+++ b/frontend/src/components/projectOverview/useCsvExport.ts
@@ -0,0 +1,128 @@
+import { useCallback, useMemo } from "react"
+import { csvEscape } from "./utils"
+
+// Custom hook that creates the final CSV export function.
+// Input: array of objects.
+// Output: function returning Promise, ready for ExportButton.
+export const useCreateCsvExportFunction = >(
+ items: T[],
+): (() => Promise) => {
+ //// FUNCTION DEFINITIONS
+
+ ///1-A
+
+ // Returns the object keys as strings.
+ // Example: { id: 1, name: "A" } -> ["id", "name"]
+ const getObjectKeys = useCallback((item: T): string[] => {
+ return Object.keys(item)
+ }, [])
+
+ // Gets CSV headers from the first item of the array.
+ // Headers are simple string keys.
+ // If there are no items, returns [].
+ const getHeadersFromItems = useCallback(
+ (items: T[]): string[] => {
+ if (items.length === 0) {
+ return []
+ }
+
+ return getObjectKeys(items[0])
+ },
+ [getObjectKeys],
+ )
+
+ // Public helper for getting headers.
+ // It wraps getHeadersFromItems so the rest of the code calls one clear function.
+ const getHeaders = useCallback(
+ (items: T[]): string[] => {
+ return getHeadersFromItems(items)
+ },
+ [getHeadersFromItems],
+ )
+
+ ///1-B
+ // Returns object keys, but typed as keyof T.
+ // Example: Array<"id" | "name"> instead of string[].
+ const getTypedObjectKeys = useCallback((item: T): Array => {
+ return Object.keys(item) as Array
+ }, [])
+
+ // Gets export fields from the first item.
+ // These fields are typed and are used to safely read values from each row.
+ const getTypedFieldsFromItems = useCallback(
+ (items: T[]): Array => {
+ if (items.length === 0) {
+ return []
+ }
+
+ return getTypedObjectKeys(items[0])
+ },
+ [getTypedObjectKeys],
+ )
+
+ // Public helper for getting typed export fields.
+ const getExportFields = useCallback(
+ (items: T[]): Array => {
+ return getTypedFieldsFromItems(items)
+ },
+ [getTypedFieldsFromItems],
+ )
+
+ // Converts the items into CSV rows.
+ // Each item becomes one row.
+ // Each field becomes one cell in that row.
+ const formatExportRows = >(
+ items: T[],
+ fields: Array,
+ ) => {
+ return items.map((item) =>
+ fields.map((field) => {
+ const value = item[field]
+
+ // Formats date-like fields.
+ // Note: field is normally a key, so this check only works if field itself is a Date.
+ if (field instanceof Date) {
+ return value
+ ? new Date(String(value)).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })
+ : ""
+ }
+
+ return value
+ }),
+ )
+ }
+
+ //// FUNCTION CALLS
+
+ // Builds CSV headers once when items change.
+ const headers = useMemo(() => {
+ return getHeaders(items)
+ }, [items])
+
+ // Builds typed fields once when items change.
+ const exportFields = useMemo(() => {
+ return getExportFields(items)
+ }, [items])
+
+ // Builds CSV rows once when items or fields change.
+ const rows = useMemo(() => {
+ return formatExportRows(items, exportFields)
+ }, [items, exportFields])
+
+ // Returns the function used by the export button.
+ // When called, it creates the final CSV string.
+ return useCallback(() => {
+ const csv = [
+ headers.map(csvEscape).join(","),
+ ...rows.map((row) => row.map(csvEscape).join(",")),
+ ].join("\n")
+
+ return Promise.resolve(csv)
+ }, [headers, rows])
+}
+
+//////////////////////////////////////////////////////////////
diff --git a/frontend/src/components/projectOverview/utils.ts b/frontend/src/components/projectOverview/utils.ts
new file mode 100644
index 0000000000..ec9b6736ab
--- /dev/null
+++ b/frontend/src/components/projectOverview/utils.ts
@@ -0,0 +1,32 @@
+import { Project } from "../../models/frontend_models"
+
+export const csvEscape = (value: unknown) => {
+ const stringValue = value == null ? "" : String(value)
+ return `"${stringValue.replace(/"/g, '""')}"`
+}
+
+/*
+ * This function formats the project submission rows for CSV export.
+ * It takes an array of projects and an array of fields to include in the export.
+ * It returns an array of arrays, where each inner array represents a row in the CSV.
+ */
+
+export const formatProjectSubmissionRows = (projects: Project[], fields: Array) => {
+ return projects.map((project) =>
+ fields.map((field) => {
+ const value = project[field]
+
+ if (field === "created_at") {
+ return value
+ ? new Date(String(value)).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })
+ : ""
+ }
+
+ return value
+ }),
+ )
+}
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
index 60900971d1..e32952e547 100644
--- a/frontend/src/utils/api.ts
+++ b/frontend/src/utils/api.ts
@@ -1,21 +1,54 @@
-import {stringify as qs} from "querystring";
-import {API_BASE_PATH} from "../config";
-import { FMSDataset, FMSId, FMSPagedResultsReponse, FMSParentProject, FMSProject, FMSProtocol, FMSReadset, FMSSample, FMSSampleNextStep, FMSSampleNextStepByStudy, FMSStep, FMSStepHistory, FMSStudy, FMSWorkflow, LabworkStepInfo, ReleaseStatus, FMSReportInformation, WorkflowStepOrder, FMSReportData, FMSPooledSample, FMSSampleIdentity, FMSSampleIdentityMatch, FMSBiosample, FMSUser, FMSProfile, FMSSampleLineageGraph, FMSTemplateAction, FMSTemplatePrefillOption, FMSVersion, FMSExperimentRun } from "../models/fms_api_models";
-import { AnyAction, Dispatch } from "redux";
-import { RootState } from "../store";
-import { notifyError } from "../modules/notification/actions";
+import { stringify as qs } from "querystring"
+import { API_BASE_PATH } from "../config"
+import {
+ FMSDataset,
+ FMSId,
+ FMSPagedResultsReponse,
+ FMSParentProject,
+ FMSProject,
+ FMSProtocol,
+ FMSReadset,
+ FMSSample,
+ FMSSampleNextStep,
+ FMSSampleNextStepByStudy,
+ FMSStep,
+ FMSStepHistory,
+ FMSStudy,
+ FMSWorkflow,
+ LabworkStepInfo,
+ ReleaseStatus,
+ FMSReportInformation,
+ WorkflowStepOrder,
+ FMSReportData,
+ FMSPooledSample,
+ FMSSampleIdentity,
+ FMSSampleIdentityMatch,
+ FMSBiosample,
+ FMSUser,
+ FMSProfile,
+ FMSSampleLineageGraph,
+ FMSTemplateAction,
+ FMSTemplatePrefillOption,
+ FMSVersion,
+ FMSExperimentRun,
+} from "../models/fms_api_models"
+import { AnyAction, Dispatch } from "redux"
+import { RootState } from "../store"
+import { notifyError } from "../modules/notification/actions"
+import { ProjectOverviewReadset } from "../components/projectOverview/types"
const api = {
auth: {
- token: credentials => post("/token/", credentials),
- tokenRefresh: tokens => post("/token/refresh/", tokens),
- resetPassword: email => post("/password_reset/", { email }),
+ token: (credentials) => post("/token/", credentials),
+ tokenRefresh: (tokens) => post("/token/refresh/", tokens),
+ resetPassword: (email) => post("/password_reset/", { email }),
changePassword: (token, password) => post("/password_reset/confirm/", { token, password }),
},
biosamples: {
get: (biosampleId: FMSId) => get>(`/biosamples/${biosampleId}/`),
- list: (options: QueryParams, abort?: boolean) => get>>(`/biosamples/`, options, { abort }),
+ list: (options: QueryParams, abort?: boolean) =>
+ get>>(`/biosamples/`, options, { abort }),
},
containerKinds: {
@@ -23,96 +56,116 @@ const api = {
},
containers: {
- get: id => get(`/containers/${id}/`),
- add: container => post("/containers/", container),
- update: container => patch(`/containers/${container.id}/`, container),
+ get: (id) => get(`/containers/${id}/`),
+ add: (container) => post("/containers/", container),
+ update: (container) => patch(`/containers/${container.id}/`, container),
list: (options, abort?: boolean) => get("/containers/", options, { abort }),
- listExport: options => get("/containers/list_export/", {format: "csv", ...options}),
- listParents: id => get(`/containers/${id}/list_parents/`),
- listChildren: id => get(`/containers/${id}/list_children/`),
- listChildrenRecursively: id => get(`/containers/${id}/list_children_recursively/`),
+ listExport: (options) => get("/containers/list_export/", { format: "csv", ...options }),
+ listParents: (id) => get(`/containers/${id}/list_parents/`),
+ listChildren: (id) => get(`/containers/${id}/list_children/`),
+ listChildrenRecursively: (id) => get(`/containers/${id}/list_children_recursively/`),
template: {
actions: () => get(`/containers/template_actions/`),
- check: (action, template) => post(`/containers/template_check/`, form({ action, template })),
- submit: (action, template) => post(`/containers/template_submit/`, form({ action, template })),
+ check: (action, template) => post(`/containers/template_check/`, form({ action, template })),
+ submit: (action, template) =>
+ post(`/containers/template_submit/`, form({ action, template })),
},
prefill: {
templates: () => get(`/containers/list_prefills/`),
- request: (options, template) => filteredpost(`/containers/prefill_template/`, {...options}, form({ template: template })),
+ request: (options, template) =>
+ filteredpost(`/containers/prefill_template/`, { ...options }, form({ template: template })),
},
search: (q, { parent, sample_holding, exact_match, except_kinds }) =>
get("/containers/search/", { q, parent, sample_holding, exact_match, except_kinds }),
},
coordinates: {
- get: coordinateId => get(`/coordinates/${coordinateId}/`),
+ get: (coordinateId) => get(`/coordinates/${coordinateId}/`),
list: (options, abort?: boolean) => get("/coordinates/", options, { abort }),
search: (q, options) => get("/coordinates/search/", { q, ...options }),
},
datasets: {
get: (id: FMSDataset["id"]) => get>(`/datasets/${id}/`),
- list: (options, abort?: boolean) => get>>("/datasets/", options, { abort }),
- setReleaseStatus: (
- id: FMSDataset["id"],
- updates: Record,
- ) => patch(`/datasets/${id}/set_release_status/`, updates),
+ list: (options, abort?: boolean) =>
+ get>>("/datasets/", options, { abort }),
+ setReleaseStatus: (id: FMSDataset["id"], updates: Record) =>
+ patch(`/datasets/${id}/set_release_status/`, updates),
addArchivedComment: (id, comment) => post(`/datasets/${id}/add_archived_comment/`, { comment }),
- getRootFolder: (id) => get(`/datasets/${id}/get_dataset_files_root_folder/`)
+ getRootFolder: (id) => get(`/datasets/${id}/get_dataset_files_root_folder/`),
},
datasetFiles: {
- get: id => get(`/dataset-files/${id}/`),
- update: dataset => patch(`/dataset-files/${dataset.id}/`, dataset),
+ get: (id) => get(`/dataset-files/${id}/`),
+ update: (dataset) => patch(`/dataset-files/${dataset.id}/`, dataset),
list: (options, abort?: boolean) => get("/dataset-files/", options, { abort }),
},
derivedSamples: {
- get: (derivedSampleId: FMSId) => get>(`/derivedsamples/${derivedSampleId}/`),
- list: (options: QueryParams, abort?: boolean) => get>>(`/derivedsamples/`, options, { abort }),
+ get: (derivedSampleId: FMSId) =>
+ get>(`/derivedsamples/${derivedSampleId}/`),
+ list: (options: QueryParams, abort?: boolean) =>
+ get>>(`/derivedsamples/`, options, {
+ abort,
+ }),
},
experimentRuns: {
- get: experimentRunId => get(`/experiment-runs/${experimentRunId}/`),
- list: (options, abort?: boolean, requestID?: string) => get>>("/experiment-runs/", options, {abort, requestID}),
- listExport: options => get("/experiment-runs/list_export/", {format: "csv", ...options}),
+ get: (experimentRunId) => get(`/experiment-runs/${experimentRunId}/`),
+ list: (options, abort?: boolean, requestID?: string) =>
+ get>>("/experiment-runs/", options, {
+ abort,
+ requestID,
+ }),
+ listExport: (options) => get("/experiment-runs/list_export/", { format: "csv", ...options }),
template: {
actions: () => get(`/experiment-runs/template_actions/`),
- check: (action, template) => post(`/experiment-runs/template_check/`, form({ action, template })),
- submit: (action, template) => post(`/experiment-runs/template_submit/`, form({ action, template })),
+ check: (action, template) =>
+ post(`/experiment-runs/template_check/`, form({ action, template })),
+ submit: (action, template) =>
+ post(`/experiment-runs/template_submit/`, form({ action, template })),
},
- launchRunProcessing: experimentRunId => patch(`/experiment-runs/${experimentRunId}/launch_run_processing/`, {}),
- relaunchRunProcessing: experimentRunId => patch(`/experiment-runs/${experimentRunId}/relaunch_run_processing/`, {}),
- fetchRunInfo: experimentRunId => get(`/experiment-runs/${experimentRunId}/run_info/`, {}),
- setLaneValidationStatus: (experimentRunId, lane, validation_status) => post(`/experiment-runs/${experimentRunId}/set_experiment_run_lane_validation_status/`, {lane, validation_status}),
- getLaneValidationStatus: (experimentRunId, lane) => get(`/experiment-runs/${experimentRunId}/get_experiment_run_lane_validation_status/`, {lane})
+ launchRunProcessing: (experimentRunId) =>
+ patch(`/experiment-runs/${experimentRunId}/launch_run_processing/`, {}),
+ relaunchRunProcessing: (experimentRunId) =>
+ patch(`/experiment-runs/${experimentRunId}/relaunch_run_processing/`, {}),
+ fetchRunInfo: (experimentRunId) => get(`/experiment-runs/${experimentRunId}/run_info/`, {}),
+ setLaneValidationStatus: (experimentRunId, lane, validation_status) =>
+ post(`/experiment-runs/${experimentRunId}/set_experiment_run_lane_validation_status/`, {
+ lane,
+ validation_status,
+ }),
+ getLaneValidationStatus: (experimentRunId, lane) =>
+ get(`/experiment-runs/${experimentRunId}/get_experiment_run_lane_validation_status/`, {
+ lane,
+ }),
},
importedFiles: {
- get: fileId => get(`/imported-files/${fileId}/`),
+ get: (fileId) => get(`/imported-files/${fileId}/`),
list: (options, abort?: boolean) => get("/imported-files/", options, { abort }),
- download: fileId => get(`/imported-files/${fileId}/download/`),
+ download: (fileId) => get(`/imported-files/${fileId}/download/`),
},
indices: {
- get: indexId => get(`/indices/${indexId}/`),
+ get: (indexId) => get(`/indices/${indexId}/`),
list: (options, abort?: boolean) => get("/indices/", options, { abort }),
- listExport: options => get("/indices/list_export/", {format: "csv", ...options}),
+ listExport: (options) => get("/indices/list_export/", { format: "csv", ...options }),
listSets: () => get("/indices/list_sets/"),
template: {
actions: () => get(`/indices/template_actions/`),
- check: (action, template) => post(`/indices/template_check/`, form({ action, template })),
+ check: (action, template) => post(`/indices/template_check/`, form({ action, template })),
submit: (action, template) => post(`/indices/template_submit/`, form({ action, template })),
},
validate: (options) => get("/indices/validate/", options),
},
individuals: {
- get: individualId => get(`/individuals/${individualId}/`),
- add: individual => post("/individuals/", individual),
- update: individual => patch(`/individuals/${individual.id}/`, individual),
+ get: (individualId) => get(`/individuals/${individualId}/`),
+ add: (individual) => post("/individuals/", individual),
+ update: (individual) => patch(`/individuals/${individual.id}/`, individual),
list: (options, abort?: boolean) => get("/individuals/", options, { abort }),
- listExport: options => get("/individuals/list_export/", {format: "csv", ...options}),
+ listExport: (options) => get("/individuals/list_export/", { format: "csv", ...options }),
search: (q, options) => get("/individuals/search/", { q, ...options }),
},
@@ -125,80 +178,119 @@ const api = {
},
libraries: {
- get: libraryId => get(`/libraries/${libraryId}/`),
+ get: (libraryId) => get(`/libraries/${libraryId}/`),
list: (options, abort?: boolean) => get("/libraries/", options, { abort }),
- listExport: options => get("/libraries/list_export/", {format: "csv", ...options}),
+ listExport: (options) => get("/libraries/list_export/", { format: "csv", ...options }),
template: {
actions: () => get(`/libraries/template_actions/`),
- check: (action, template) => post(`/libraries/template_check/`, form({ action, template })),
+ check: (action, template) => post(`/libraries/template_check/`, form({ action, template })),
submit: (action, template) => post(`/libraries/template_submit/`, form({ action, template })),
},
prefill: {
templates: () => get(`/libraries/list_prefills/`),
- request: (options, template) => filteredpost(`/libraries/prefill_template/`, {...options}, form({ template: template })),
+ request: (options, template) =>
+ filteredpost(`/libraries/prefill_template/`, { ...options }, form({ template: template })),
},
- search: q => get("/libraries/search/", { q }),
+ search: (q) => get("/libraries/search/", { q }),
},
libraryTypes: {
- get: libraryTypeId => get(`/library-types/${libraryTypeId}/`),
+ get: (libraryTypeId) => get(`/library-types/${libraryTypeId}/`),
list: (options, abort?: boolean) => get("/library-types/", options, { abort }),
},
metrics: {
- getReadsPerSampleForLane: (experimentRunId, lane) => get(`/metrics/`, {limit: 100000, name: 'nb_reads', metric_group: 'qc', readset__dataset__experiment_run_id: experimentRunId, readset__dataset__lane: lane})
+ getReadsPerSampleForLane: (experimentRunId, lane) =>
+ get(`/metrics/`, {
+ limit: 100000,
+ name: "nb_reads",
+ metric_group: "qc",
+ readset__dataset__experiment_run_id: experimentRunId,
+ readset__dataset__lane: lane,
+ }),
},
parentProjects: {
- get: (parentProjectId: FMSId) => get>(`/parent-projects/${parentProjectId}/`),
- list: (options: object, abort?: boolean, requestID?: string) => get>>("/parent-projects/", options, { abort, requestID }),
+ get: (parentProjectId: FMSId) =>
+ get>(`/parent-projects/${parentProjectId}/`),
+ list: (options: object, abort?: boolean, requestID?: string) =>
+ get>>("/parent-projects/", options, {
+ abort,
+ requestID,
+ }),
+ readsets: (parentProjectId: FMSId, options: QueryParams, abort?: boolean) =>
+ get>>(
+ `/parent-projects/${parentProjectId}/readsets/`,
+ options,
+ { abort },
+ ),
},
platforms: {
- get: platformId => get(`/platforms/${platformId}/`),
+ get: (platformId) => get(`/platforms/${platformId}/`),
list: (options, abort?: boolean) => get("/platforms/", options, { abort }),
},
pooledSamples: {
- list: (options: any, apiOptions?: APIFetchOptions) => get>>("/pooled-samples/", options, apiOptions),
- listExport: options => get("/pooled-samples/list_export/", {format: "csv", ...options}),
+ list: (options: any, apiOptions?: APIFetchOptions) =>
+ get>>(
+ "/pooled-samples/",
+ options,
+ apiOptions,
+ ),
+ listExport: (options) => get("/pooled-samples/list_export/", { format: "csv", ...options }),
template: {
actions: () => get>(`/pooled-samples/template_actions/`),
- check: (action, template) => post(`/pooled-samples/template_check/`, form({ action, template })),
- submit: (action, template) => post(`/pooled-samples/template_submit/`, form({ action, template })),
+ check: (action, template) =>
+ post(`/pooled-samples/template_check/`, form({ action, template })),
+ submit: (action, template) =>
+ post(`/pooled-samples/template_submit/`, form({ action, template })),
},
prefill: {
- templates: () => get>(`/pooled-samples/list_prefills/`),
- request: (options: any, template: number) => filteredpost(`/pooled-samples/prefill_template/`, {...options}, form({ template: template })),
+ templates: () =>
+ get>(`/pooled-samples/list_prefills/`),
+ request: (options: any, template: number) =>
+ filteredpost(
+ `/pooled-samples/prefill_template/`,
+ { ...options },
+ form({ template: template }),
+ ),
},
},
processes: {
- get: processId => get(`/processes/${processId}/`),
+ get: (processId) => get(`/processes/${processId}/`),
list: (options, abort?: boolean) => get("/processes/", options, { abort }),
},
processMeasurements: {
- get: processMeasurementId => get(`/process-measurements/${processMeasurementId}/`),
+ get: (processMeasurementId) => get(`/process-measurements/${processMeasurementId}/`),
list: (options, abort?: boolean) => get("/process-measurements/", options, { abort }),
- listExport: options => get("/process-measurements/list_export/", {format: "csv", ...options}),
- search: q => get("/process-measurements/search/", { q }),
+ listExport: (options) =>
+ get("/process-measurements/list_export/", { format: "csv", ...options }),
+ search: (q) => get("/process-measurements/search/", { q }),
template: {
actions: () => get(`/process-measurements/template_actions/`),
- check: (action, template) => post(`/process-measurements/template_check/`, form({ action, template })),
- submit: (action, template) => post(`/process-measurements/template_submit/`, form({ action, template })),
+ check: (action, template) =>
+ post(`/process-measurements/template_check/`, form({ action, template })),
+ submit: (action, template) =>
+ post(`/process-measurements/template_submit/`, form({ action, template })),
},
},
projects: {
- get: projectId => get(`/projects/${projectId}/`),
- add: project => post("/projects/", project),
- update: project => patch(`/projects/${project.id}/`, project),
- list: (options, abort?: boolean, requestID?: string) => get>>("/projects/", options, { abort, requestID }),
- listExport: options => get("/projects/list_export/", {format: "csv", ...options}),
+ get: (projectId) => get(`/projects/${projectId}/`),
+ add: (project) => post("/projects/", project),
+ update: (project) => patch(`/projects/${project.id}/`, project),
+ list: (options, abort?: boolean, requestID?: string) =>
+ get>>("/projects/", options, {
+ abort,
+ requestID,
+ }),
+ listExport: (options) => get("/projects/list_export/", { format: "csv", ...options }),
template: {
actions: () => get(`/projects/template_actions/`),
- check: (action, template) => post(`/projects/template_check/`, form({ action, template })),
+ check: (action, template) => post(`/projects/template_check/`, form({ action, template })),
submit: (action, template) => post(`/projects/template_submit/`, form({ action, template })),
},
},
@@ -208,21 +300,28 @@ const api = {
},
protocols: {
- list: (options, abort?: boolean) => get("/protocols/", options, { abort }),
- lastProtocols: (options, abort?: boolean) => get>("/protocols/last_protocols/", options, { abort }),
+ list: (options, abort?: boolean) => get("/protocols/", options, { abort }),
+ lastProtocols: (options, abort?: boolean) =>
+ get>(
+ "/protocols/last_protocols/",
+ options,
+ { abort },
+ ),
},
readsets: {
- get: id => get(`/readsets/${id}/`),
- list: (options: QueryParams, abort?: boolean) => get>>(`/readsets/`, options, { abort }),
+ get: (id) => get(`/readsets/${id}/`),
+ list: (options: QueryParams, abort?: boolean) =>
+ get>>(`/readsets/`, options, { abort }),
},
referenceGenomes: {
- get: referenceGenomeId => get(`/reference-genomes/${referenceGenomeId}`),
- add: referenceGenome => post(`/reference-genomes/`, referenceGenome),
- update: referenceGenome => patch(`/reference-genomes/${referenceGenome.id}/`, referenceGenome),
- list: (options, abort?: boolean) => get('/reference-genomes/', options, { abort }),
- search: q => get("/reference-genomes/search/", { q }),
+ get: (referenceGenomeId) => get(`/reference-genomes/${referenceGenomeId}`),
+ add: (referenceGenome) => post(`/reference-genomes/`, referenceGenome),
+ update: (referenceGenome) =>
+ patch(`/reference-genomes/${referenceGenome.id}/`, referenceGenome),
+ list: (options, abort?: boolean) => get("/reference-genomes/", options, { abort }),
+ search: (q) => get("/reference-genomes/search/", { q }),
},
runTypes: {
@@ -230,40 +329,66 @@ const api = {
},
samples: {
- get: sampleId => get>(`/samples/${sampleId}/`),
- add: sample => post("/samples/", sample),
- addSamplesToStudy: (exceptedSampleIDs: Array, defaultSelection: boolean, projectId: FMSProject['id'], studyLetter: FMSStudy['letter'], stepOrder: WorkflowStepOrder['order'], queryParams?: QueryParams) =>
- filteredpost(`/samples/add_samples_to_study/`, queryParams, { excepted_sample_ids: exceptedSampleIDs, default_selection: defaultSelection, project_id: projectId, study_letter: studyLetter, step_order: stepOrder }),
- update: sample => patch(`/samples/${sample.id}/`, sample),
- list: (options, abort?: boolean) => get>>("/samples/", options, { abort }),
- listExport: options => get("/samples/list_export/", {format: "csv", ...options}),
- listExportMetadata: options => get("/samples/list_export_metadata/", {format: "csv", ...options}),
+ get: (sampleId) => get>(`/samples/${sampleId}/`),
+ add: (sample) => post("/samples/", sample),
+ addSamplesToStudy: (
+ exceptedSampleIDs: Array,
+ defaultSelection: boolean,
+ projectId: FMSProject["id"],
+ studyLetter: FMSStudy["letter"],
+ stepOrder: WorkflowStepOrder["order"],
+ queryParams?: QueryParams,
+ ) =>
+ filteredpost(`/samples/add_samples_to_study/`, queryParams, {
+ excepted_sample_ids: exceptedSampleIDs,
+ default_selection: defaultSelection,
+ project_id: projectId,
+ study_letter: studyLetter,
+ step_order: stepOrder,
+ }),
+ update: (sample) => patch(`/samples/${sample.id}/`, sample),
+ list: (options, abort?: boolean) =>
+ get>>("/samples/", options, { abort }),
+ listExport: (options) => get("/samples/list_export/", { format: "csv", ...options }),
+ listExportMetadata: (options) =>
+ get("/samples/list_export_metadata/", { format: "csv", ...options }),
listCollectionSites: (filter) => get("/samples/list_collection_sites/", { filter }),
- listVersions: sampleId => get>(`/samples/${sampleId}/versions/`),
+ listVersions: (sampleId) => get>(`/samples/${sampleId}/versions/`),
template: {
actions: () => get(`/samples/template_actions/`),
- check: (action, template) => post(`/samples/template_check/`, form({ action, template })),
+ check: (action, template) => post(`/samples/template_check/`, form({ action, template })),
submit: (action, template) => post(`/samples/template_submit/`, form({ action, template })),
},
prefill: {
templates: () => get(`/samples/list_prefills/`),
- request: (options, template) => filteredpost(`/samples/prefill_template/`, {...options}, form({ template: template })),
+ request: (options, template) =>
+ filteredpost(`/samples/prefill_template/`, { ...options }, form({ template: template })),
},
- search: q => get("/samples/search/", { q }),
+ search: (q) => get("/samples/search/", { q }),
},
sampleIdentity: {
- get: (id: FMSSampleIdentity['id']) => get>(`/sample-identities/${id}/`),
- list: (options: any, abort?: boolean) => get>>(`/sample-identities/`, options, { abort }),
+ get: (id: FMSSampleIdentity["id"]) =>
+ get>(`/sample-identities/${id}/`),
+ list: (options: any, abort?: boolean) =>
+ get>>(`/sample-identities/`, options, {
+ abort,
+ }),
},
sampleIdentityMatch: {
- get: (id: FMSSampleIdentityMatch['id']) => get>(`/sample-identity-matches/${id}/`),
- list: (options: any, abort?: boolean) => get>>(`/sample-identity-matches/`, options, { abort }),
+ get: (id: FMSSampleIdentityMatch["id"]) =>
+ get>(`/sample-identity-matches/${id}/`),
+ list: (options: any, abort?: boolean) =>
+ get>>(
+ `/sample-identity-matches/`,
+ options,
+ { abort },
+ ),
},
sampleMetadata: {
- get: options => get(`/sample-metadata/`, options),
+ get: (options) => get(`/sample-metadata/`, options),
search: (q, options) => get("/sample-metadata/search/", { q, ...options }),
},
@@ -272,75 +397,148 @@ const api = {
},
sampleNextStep: {
- listSamples: (sampleIDs: FMSId[]) => get>>('/sample-next-step/', {sample__id__in: sampleIDs.join(','), limit: 100000}),
- getStudySamples: (studyId) => get('/sample-next-step/', {studies__id__in : studyId}),
- executeAutomation: (stepId, additionalData, options) => filteredpost(`/sample-next-step/execute_automation/`, {...options}, form({step_id: stepId, additional_data: additionalData, ...options}),),
- labworkSummary: () => get('/sample-next-step/labwork_info/'),
- labworkStepSummary: (stepId: FMSId, groupBy: string, options?: QueryParams, sample__id__in?: FMSId[]) => filteredpost>('/sample-next-step/labwork_step_info/', {...options, step__id__in: stepId, group_by: groupBy}, { sample__id__in }),
- listSamplesAtStep: (stepId: FMSId, options?: QueryParams, sample__id__in?: FMSId[]) => filteredpost>>('/sample-next-step/list_post/', {limit: 100000, ...options, step__id__in: stepId}, { sample__id__in }),
+ listSamples: (sampleIDs: FMSId[]) =>
+ get>>("/sample-next-step/", {
+ sample__id__in: sampleIDs.join(","),
+ limit: 100000,
+ }),
+ getStudySamples: (studyId) => get("/sample-next-step/", { studies__id__in: studyId }),
+ executeAutomation: (stepId, additionalData, options) =>
+ filteredpost(
+ `/sample-next-step/execute_automation/`,
+ { ...options },
+ form({ step_id: stepId, additional_data: additionalData, ...options }),
+ ),
+ labworkSummary: () => get("/sample-next-step/labwork_info/"),
+ labworkStepSummary: (
+ stepId: FMSId,
+ groupBy: string,
+ options?: QueryParams,
+ sample__id__in?: FMSId[],
+ ) =>
+ filteredpost>(
+ "/sample-next-step/labwork_step_info/",
+ { ...options, step__id__in: stepId, group_by: groupBy },
+ { sample__id__in },
+ ),
+ listSamplesAtStep: (stepId: FMSId, options?: QueryParams, sample__id__in?: FMSId[]) =>
+ filteredpost>>(
+ "/sample-next-step/list_post/",
+ { limit: 100000, ...options, step__id__in: stepId },
+ { sample__id__in },
+ ),
prefill: {
- templates: (protocolId) => get('/sample-next-step/list_prefills/', {protocol: protocolId}),
- request: (templateID: FMSId, user_prefill_data: string, placement_data: string, sample__id__in: string, options?: QueryParams) => filteredpost('/sample-next-step/prefill_template/',{...options}, form({user_prefill_data: user_prefill_data, placement_data: placement_data, template: templateID.toString(), sample__id__in }), { notifyError: true })
+ templates: (protocolId) => get("/sample-next-step/list_prefills/", { protocol: protocolId }),
+ request: (
+ templateID: FMSId,
+ user_prefill_data: string,
+ placement_data: string,
+ sample__id__in: string,
+ options?: QueryParams,
+ ) =>
+ filteredpost(
+ "/sample-next-step/prefill_template/",
+ { ...options },
+ form({
+ user_prefill_data: user_prefill_data,
+ placement_data: placement_data,
+ template: templateID.toString(),
+ sample__id__in,
+ }),
+ { notifyError: true },
+ ),
},
template: {
actions: () => get(`/sample-next-step/template_actions/`),
- check: (action, template) => post(`/sample-next-step/template_check/`, form({ action, template })),
- submit: (action, template) => post(`/sample-next-step/template_submit/`, form({ action, template })),
+ check: (action, template) =>
+ post(`/sample-next-step/template_check/`, form({ action, template })),
+ submit: (action, template) =>
+ post(`/sample-next-step/template_submit/`, form({ action, template })),
},
},
sampleNextStepByStudy: {
- getStudySamples: (options: any) => get>>('/sample-next-step-by-study/', {...options}),
- getStudySamplesForStepOrder: (studyId, stepOrderID, options) => get(`/sample-next-step-by-study/`, {...options, study__id__in : studyId, step_order__id__in : stepOrderID }),
- countStudySamples: (studyId, options) => get(`/sample-next-step-by-study/summary_by_study/`, {...options, study__id__in: studyId}),
- remove: sampleNextStepByStudyId => remove(`/sample-next-step-by-study/${sampleNextStepByStudyId}/`),
- removeList: (sampleIDs: FMSId[], study: FMSStudy['id'], stepOrder: number) => post>>(`/sample-next-step-by-study/destroy_list/`, { sample_ids: sampleIDs, study, step_order: stepOrder }),
- list: (options, abort?: boolean) => get("/sample-next-step-by-study/", { limit: 100000, ...options }, { abort }),
+ getStudySamples: (options: any) =>
+ get>>(
+ "/sample-next-step-by-study/",
+ { ...options },
+ ),
+ getStudySamplesForStepOrder: (studyId, stepOrderID, options) =>
+ get(`/sample-next-step-by-study/`, {
+ ...options,
+ study__id__in: studyId,
+ step_order__id__in: stepOrderID,
+ }),
+ countStudySamples: (studyId, options) =>
+ get(`/sample-next-step-by-study/summary_by_study/`, { ...options, study__id__in: studyId }),
+ remove: (sampleNextStepByStudyId) =>
+ remove(`/sample-next-step-by-study/${sampleNextStepByStudyId}/`),
+ removeList: (sampleIDs: FMSId[], study: FMSStudy["id"], stepOrder: number) =>
+ post>>(`/sample-next-step-by-study/destroy_list/`, {
+ sample_ids: sampleIDs,
+ study,
+ step_order: stepOrder,
+ }),
+ list: (options, abort?: boolean) =>
+ get("/sample-next-step-by-study/", { limit: 100000, ...options }, { abort }),
},
samplesheets: {
- getSamplesheet: (barcode, kind, placementData) => post('/samplesheets/get_samplesheet/', { container_barcode: barcode, container_kind: kind, placement: placementData }),
+ getSamplesheet: (barcode, kind, placementData) =>
+ post("/samplesheets/get_samplesheet/", {
+ container_barcode: barcode,
+ container_kind: kind,
+ placement: placementData,
+ }),
},
sequences: {
- get: sequenceId => get(`/sequences/${sequenceId}/`),
+ get: (sequenceId) => get(`/sequences/${sequenceId}/`),
list: (options, abort?: boolean) => get("/sequences/", options, { abort }),
},
stepHistory: {
- getCompletedSamplesForStudy: (studyId, options) => get>>('/step-histories/', {...options, study__id__in: studyId}),
- countStudySamples: (studyId) => get(`/step-histories/summary_by_study/`, {study__id__in: studyId})
+ getCompletedSamplesForStudy: (studyId, options) =>
+ get>>("/step-histories/", {
+ ...options,
+ study__id__in: studyId,
+ }),
+ countStudySamples: (studyId) =>
+ get(`/step-histories/summary_by_study/`, { study__id__in: studyId }),
},
steps: {
- list: (options, abort?: boolean) => get>>('/steps/', options, { abort} ),
+ list: (options, abort?: boolean) =>
+ get>>("/steps/", options, { abort }),
},
studies: {
- get: studyId => get>(`/studies/${studyId}/`),
- add: study => post("/studies/", study),
- update: study => patch(`/studies/${study.id}/`, study),
- list: (options, abort?: boolean) => get>>('/studies/', options, {abort}),
- listProjectStudies: projectId => get('/studies/', { project_id: projectId}),
- remove: (studyId) => remove(`/studies/${studyId}/`)
+ get: (studyId) => get>(`/studies/${studyId}/`),
+ add: (study) => post("/studies/", study),
+ update: (study) => patch(`/studies/${study.id}/`, study),
+ list: (options, abort?: boolean) =>
+ get>>("/studies/", options, { abort }),
+ listProjectStudies: (projectId) => get("/studies/", { project_id: projectId }),
+ remove: (studyId) => remove(`/studies/${studyId}/`),
},
taxons: {
- get: taxonId => get(`/taxons/${taxonId}/`),
- add: taxon => post(`/taxons/`, taxon),
- update: taxon => patch(`/taxons/${taxon.id}/`, taxon),
+ get: (taxonId) => get(`/taxons/${taxonId}/`),
+ add: (taxon) => post(`/taxons/`, taxon),
+ update: (taxon) => patch(`/taxons/${taxon.id}/`, taxon),
list: (options, abort?: boolean) => get("/taxons/", options, { abort }),
- search: q => get("/taxons/search/", { q }),
+ search: (q) => get("/taxons/search/", { q }),
},
users: {
- get: userId => get>(`/users/${userId}/`),
- add: user => post("/users/", user),
- update: user => patch(`/users/${user.id}/`, user),
- updateSelf: user => patch(`/users/update_self/`, user),
+ get: (userId) => get>(`/users/${userId}/`),
+ add: (user) => post("/users/", user),
+ update: (user) => patch(`/users/${user.id}/`, user),
+ updateSelf: (user) => patch(`/users/update_self/`, user),
list: (options, abort?: boolean) => get("/users/", options, { abort }),
listRevisions: (userId, options = {}) => get(`/revisions/`, { user_id: userId, ...options }),
- listVersions: (userId, options = {}) => get(`/versions/`, { revision__user: userId, ...options }),
+ listVersions: (userId, options = {}) =>
+ get(`/versions/`, { revision__user: userId, ...options }),
},
profiles: {
@@ -348,8 +546,10 @@ const api = {
},
workflows: {
- get: (workflowId: FMSWorkflow['id']) => get>(`/workflows/${workflowId}/`),
- list: (options, abort?: boolean) => get>>('/workflows/', options, { abort })
+ get: (workflowId: FMSWorkflow["id"]) =>
+ get>(`/workflows/${workflowId}/`),
+ list: (options, abort?: boolean) =>
+ get>>("/workflows/", options, { abort }),
},
groups: {
@@ -357,203 +557,269 @@ const api = {
},
query: {
- search: q => get("/query/search/", { q }, { abort: true }),
+ search: (q) => get("/query/search/", { q }, { abort: true }),
},
sample_lineage: {
- get: (sampleId: FMSId) => get>(`/sample-lineage/${sampleId}/graph/`)
+ get: (sampleId: FMSId) =>
+ get>(`/sample-lineage/${sampleId}/graph/`),
},
report: {
- listReports: () => get>("/reports/"),
- listReportInformation: (name: string) => get>(`/reports/${name}/`),
- getReport: (name: string, start_date: string, end_date: string, time_window = "month", group_by: string[] = []) => get>(`/reports/${name}/`, { group_by, time_window, start_date, end_date }),
- getReportAsExcel: (name: string, start_date: string, end_date: string, time_window = "month", group_by: string[] = []) => get (`/reports/${name}/`, { group_by, time_window, start_date, end_date, export: true }),
- }
+ listReports: () => get>("/reports/"),
+ listReportInformation: (name: string) =>
+ get>(`/reports/${name}/`),
+ getReport: (
+ name: string,
+ start_date: string,
+ end_date: string,
+ time_window = "month",
+ group_by: string[] = [],
+ ) =>
+ get>(`/reports/${name}/`, {
+ group_by,
+ time_window,
+ start_date,
+ end_date,
+ }),
+ getReportAsExcel: (
+ name: string,
+ start_date: string,
+ end_date: string,
+ time_window = "month",
+ group_by: string[] = [],
+ ) =>
+ get(`/reports/${name}/`, {
+ group_by,
+ time_window,
+ start_date,
+ end_date,
+ export: true,
+ }),
+ },
}
-
-export default api;
-
-type AuthTokensAccess = Partial> & Pick
-
-export function dispatchForApi(token: string | undefined, thunk: (_: Dispatch, getState: () => AuthTokensAccess) => T): T {
- return thunk(undefined as unknown as Dispatch, () => ({ auth: { isFetching: false, error: null, currentUserID: null, tokens: { access: token, refresh: null }, _persist: { version: 0, rehydrated: false } } }))
+export default api
+
+type AuthTokensAccess = Partial> & Pick
+
+export function dispatchForApi(
+ token: string | undefined,
+ thunk: (_: Dispatch, getState: () => AuthTokensAccess) => T,
+): T {
+ return thunk(undefined as unknown as Dispatch, () => ({
+ auth: {
+ isFetching: false,
+ error: null,
+ currentUserID: null,
+ tokens: { access: token, refresh: null },
+ _persist: { version: 0, rehydrated: false },
+ },
+ }))
}
-type WithTokenFn, Args extends any[]> = (...args: Args) => (dispatch: Dispatch, getState: () => AuthTokensAccess) => Promise
-export function withToken, Args extends any[]>(token: string | undefined, fn: WithTokenFn) {
- // dispatch is hopefully not used in the fn function
- return (...args: Parameters) => dispatchForApi(token, fn(...args))
+type WithTokenFn, Args extends any[]> = (
+ ...args: Args
+) => (dispatch: Dispatch, getState: () => AuthTokensAccess) => Promise
+export function withToken, Args extends any[]>(
+ token: string | undefined,
+ fn: WithTokenFn,
+) {
+ // dispatch is hopefully not used in the fn function
+ return (...args: Parameters) => dispatchForApi(token, fn(...args))
}
const ongoingRequests: Record = {}
-type HTTPMethod = 'GET' | 'POST' | 'DELETE' | 'PATCH'
+type HTTPMethod = "GET" | "POST" | "DELETE" | "PATCH"
export interface APIFetchOptions {
- abort?: boolean
- requestID?: string
- notifyError?: boolean
+ abort?: boolean
+ requestID?: string
+ notifyError?: boolean
}
-export const ABORT_ERROR_NAME = 'AbortError'
+export const ABORT_ERROR_NAME = "AbortError"
-function apiFetch>(method: HTTPMethod, route: string, body?: any, options: APIFetchOptions = { abort: false, notifyError: false }) {
- const baseRoute = getPathname(route)
+function apiFetch>(
+ method: HTTPMethod,
+ route: string,
+ body?: any,
+ options: APIFetchOptions = { abort: false, notifyError: false },
+) {
+ const baseRoute = getPathname(route)
- return (dispatch: Dispatch, getState: (() => AuthTokensAccess)) => {
+ return (dispatch: Dispatch, getState: () => AuthTokensAccess) => {
+ const accessToken = getState().auth.tokens.access
- const accessToken = getState().auth.tokens.access;
+ const headers = {}
- const headers = {}
+ if (accessToken) headers["authorization"] = `Bearer ${accessToken}`
- if (accessToken)
- headers["authorization"] = `Bearer ${accessToken}`
+ if (!isFormData(body) && isObject(body)) headers["content-type"] = "application/json"
- if (!isFormData(body) && isObject(body))
- headers["content-type"] = "application/json"
-
- const requestID = options.requestID ?? baseRoute
+ const requestID = options.requestID ?? baseRoute
- // For abortable requests
- let signal: AbortSignal | undefined
- if (options.abort) {
- const controller = new AbortController()
- signal = controller.signal
- if (ongoingRequests[requestID]) {
- ongoingRequests[requestID].abort({
- name: ABORT_ERROR_NAME,
- message: `Request aborted for request to ${requestID}`,
- })
- }
- ongoingRequests[requestID] = controller
- }
-
- const request = fetch(`${API_BASE_PATH}${route}`, {
- method,
- headers,
- credentials: 'omit',
- signal,
- body:
- isFormData(body) ?
- body :
- isObject(body) ?
- JSON.stringify(body) :
- undefined,
+ // For abortable requests
+ let signal: AbortSignal | undefined
+ if (options.abort) {
+ const controller = new AbortController()
+ signal = controller.signal
+ if (ongoingRequests[requestID]) {
+ ongoingRequests[requestID].abort({
+ name: ABORT_ERROR_NAME,
+ message: `Request aborted for request to ${requestID}`,
})
+ }
+ ongoingRequests[requestID] = controller
+ }
- return request
- .then(res => {
- if (options.abort) {
- delete ongoingRequests[requestID]
- }
- return res
- })
- .then((response) => attachData(response))
- .then(response => {
- if (response.ok) {
- return response;
- }
- if (options.notifyError) {
- let detail = response.data.detail
- if (Array.isArray(detail)) {
- detail = detail.join('; ')
- }
- dispatch(notifyError({
- id: requestID,
- title: detail || 'API request failed',
- }))
- }
- return Promise.reject(createAPIError(response));
- })
- };
+ const request = fetch(`${API_BASE_PATH}${route}`, {
+ method,
+ headers,
+ credentials: "omit",
+ signal,
+ body: isFormData(body) ? body : isObject(body) ? JSON.stringify(body) : undefined,
+ })
+
+ return request
+ .then((res) => {
+ if (options.abort) {
+ delete ongoingRequests[requestID]
+ }
+ return res
+ })
+ .then((response) => attachData(response))
+ .then((response) => {
+ if (response.ok) {
+ return response
+ }
+ if (options.notifyError) {
+ let detail = response.data.detail
+ if (Array.isArray(detail)) {
+ detail = detail.join("; ")
+ }
+ dispatch(
+ notifyError({
+ id: requestID,
+ title: detail || "API request failed",
+ }),
+ )
+ }
+ return Promise.reject(createAPIError(response))
+ })
+ }
}
export type QueryParams = Parameters[0]
-function get>(route: string, queryParams?: QueryParams, options?: APIFetchOptions) {
- const fullRoute = route + (queryParams ? '?' + qs(queryParams) : '')
- return apiFetch('GET', fullRoute, undefined, options);
+function get>(
+ route: string,
+ queryParams?: QueryParams,
+ options?: APIFetchOptions,
+) {
+ const fullRoute = route + (queryParams ? "?" + qs(queryParams) : "")
+ return apiFetch("GET", fullRoute, undefined, options)
}
-function filteredpost>(route: string, queryParams: QueryParams, body: any, options?: APIFetchOptions) {
- const fullRoute = route + (queryParams ? '?' + qs(queryParams) : '')
- return apiFetch('POST', fullRoute, body, options);
+function filteredpost>(
+ route: string,
+ queryParams: QueryParams,
+ body: any,
+ options?: APIFetchOptions,
+) {
+ const fullRoute = route + (queryParams ? "?" + qs(queryParams) : "")
+ return apiFetch("POST", fullRoute, body, options)
}
-function post>(route: string, body: any, options?: APIFetchOptions) {
- return apiFetch('POST', route, body, options);
+function post>(
+ route: string,
+ body: any,
+ options?: APIFetchOptions,
+) {
+ return apiFetch("POST", route, body, options)
}
-function patch>(route: string, body: any, options?: APIFetchOptions) {
- return apiFetch('PATCH', route, body, options);
+function patch>(
+ route: string,
+ body: any,
+ options?: APIFetchOptions,
+) {
+ return apiFetch("PATCH", route, body, options)
}
function remove>(route: string) {
- return apiFetch('DELETE', route);
+ return apiFetch("DELETE", route)
}
-interface ApiError extends Omit {
- name: 'APIError'
- message: string
- stack: string[]
- data: Record
- fromAPI: boolean
- status: number
- statusText: string
- url: string
+interface ApiError extends Omit {
+ name: "APIError"
+ message: string
+ stack: string[]
+ data: Record
+ fromAPI: boolean
+ status: number
+ statusText: string
+ url: string
}
function createAPIError>(response: R): ApiError {
- const data = response.data;
- let detail: any;
-
- // Server errors
- if (response.isJSON && response.status === 400) {
- detail = JSON.stringify(data, null, 2)
- }
- else {
- // API error as { ok: false, detail: ... }
- try {
- detail = data.detail ||
- (data.revision__user && ('User: ' + data.revision__user.join(', ')));
- } catch (_) { }
- }
+ const data = response.data
+ let detail: any
+
+ // Server errors
+ if (response.isJSON && response.status === 400) {
+ detail = JSON.stringify(data, null, 2)
+ } else {
+ // API error as { ok: false, detail: ... }
+ try {
+ detail = data.detail || (data.revision__user && "User: " + data.revision__user.join(", "))
+ } catch (_) {}
+ }
- const message = detail ?
- ('API error: ' + detail) :
- (`HTTP error ${response.status}: ` + response.statusText + ': ' + response.url)
+ const message = detail
+ ? "API error: " + detail
+ : `HTTP error ${response.status}: ` + response.statusText + ": " + response.url
- const error = new Error(message) as unknown as ApiError;
- error.name = 'APIError';
- error.fromAPI = Boolean(detail);
- error.data = data || {};
- error.url = response.url;
- error.status = response.status;
- error.statusText = response.statusText;
- error.stack = []
+ const error = new Error(message) as unknown as ApiError
+ error.name = "APIError"
+ error.fromAPI = Boolean(detail)
+ error.data = data || {}
+ error.url = response.url
+ error.status = response.status
+ error.statusText = response.statusText
+ error.stack = []
- return error;
+ return error
}
export interface FMSResponse extends Response {
- isJSON: boolean
- data: T
- filename?: string
+ isJSON: boolean
+ data: T
+ filename?: string
+}
+interface JsonResponse extends FMSResponse {
+ isJSON: true
+}
+interface ArrayBufferResponse extends FMSResponse {
+ isJSON: false
}
-interface JsonResponse extends FMSResponse { isJSON: true }
-interface ArrayBufferResponse extends FMSResponse { isJSON: false }
-interface StringResponse extends FMSResponse { isJSON: false }
-interface AttachDataErrorResponse extends FMSResponse> { isJSON: false }
-type ResponseWithData = JsonResponse | ArrayBufferResponse | StringResponse | AttachDataErrorResponse
-
-function attachData>(response: Response & Partial) {
- const contentType = response.headers.get('content-type') || '';
- const contentDispo = response.headers.get('content-disposition');
- const filename = getFilenameOrNull(contentDispo)
- if (filename)
- response.filename = filename
-
- /*
+interface StringResponse extends FMSResponse {
+ isJSON: false
+}
+interface AttachDataErrorResponse extends FMSResponse> {
+ isJSON: false
+}
+type ResponseWithData =
+ JsonResponse | ArrayBufferResponse | StringResponse | AttachDataErrorResponse
+
+function attachData>(
+ response: Response & Partial,
+) {
+ const contentType = response.headers.get("content-type") || ""
+ const contentDispo = response.headers.get("content-disposition")
+ const filename = getFilenameOrNull(contentDispo)
+ if (filename) response.filename = filename
+
+ /*
TODO: This code was causing downloaded excel templates to become corrupted because
the backend was sending "None" as a Content-Type, due to a problem with mime types.
We tried to fix that by hard-coding the content-type as 'application/octet-stream' but
@@ -565,49 +831,50 @@ function attachData>(response: Response & Partia
This was a difficult problem to figure out. This code needs to be improved to avoid
the same problem in the future if we transer other binary data types.
*/
- const isJSON = contentType.includes('/json')
- const isExcel = contentType.includes('/ms-excel') || contentType.includes('/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
- const isZip = contentType.includes('/zip')
-
- response.isJSON = isJSON
- return (isJSON ? response.json() : isExcel || isZip ? response.arrayBuffer() : response.text())
- .then(data => {
- response.data = data;
- return response as R
- })
- .catch(() => {
- response.data = {};
- return response as R // as AttachDataErrorResponse (ideally)
- })
+ const isJSON = contentType.includes("/json")
+ const isExcel =
+ contentType.includes("/ms-excel") ||
+ contentType.includes("/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ const isZip = contentType.includes("/zip")
+
+ response.isJSON = isJSON
+ return (isJSON ? response.json() : isExcel || isZip ? response.arrayBuffer() : response.text())
+ .then((data) => {
+ response.data = data
+ return response as R
+ })
+ .catch(() => {
+ response.data = {}
+ return response as R // as AttachDataErrorResponse (ideally)
+ })
}
function getFilenameOrNull(contentDispo: string | null) {
- if (contentDispo)
- return contentDispo.split('filename=').length > 1
- // eslint-disable-next-line no-useless-escape
- ? contentDispo.split('filename=')[1].replace(/^.*[\\\/]/, '')
- : null
- else
- return null
+ if (contentDispo)
+ return contentDispo.split("filename=").length > 1
+ ? // eslint-disable-next-line no-useless-escape
+ contentDispo.split("filename=")[1].replace(/^.*[\\\/]/, "")
+ : null
+ else return null
}
function form(params: Record) {
- const formData = new FormData()
- for (const key in params) {
- const value = params[key]
- formData.append(key, value)
- }
- return formData
+ const formData = new FormData()
+ for (const key in params) {
+ const value = params[key]
+ formData.append(key, value)
+ }
+ return formData
}
function isObject(object: any): object is object {
- return object !== null && typeof object === 'object'
+ return object !== null && typeof object === "object"
}
function isFormData(object: any): object is FormData {
- return object instanceof FormData
+ return object instanceof FormData
}
function getPathname(route: string) {
- return route.replace(/\?.*$/, '')
-}
\ No newline at end of file
+ return route.replace(/\?.*$/, "")
+}