[ARVADOS-WORKBENCH2] updated: 2.1.0-260-g7328f1f2

Git user git at public.arvados.org
Thu Mar 18 21:35:22 UTC 2021


Summary of changes:
 src/plugins/sample-tracker/batch.tsx       | 152 +++++++++++++++++++++++
 src/plugins/sample-tracker/batchList.tsx   | 188 +++++++++++++++++++++++++++++
 src/plugins/sample-tracker/extraction.tsx  | 101 +++++++---------
 src/plugins/sample-tracker/index.tsx       |  45 ++++---
 src/plugins/sample-tracker/patient.tsx     |  79 +++++++++++-
 src/plugins/sample-tracker/patientList.tsx | 108 +++--------------
 src/plugins/sample-tracker/sample.tsx      |  21 +++-
 src/plugins/sample-tracker/sampleList.tsx  | 130 +++++++++++---------
 src/plugins/sample-tracker/study.tsx       | 129 +++++++++++++++++---
 src/plugins/sample-tracker/studyList.tsx   |  85 ++-----------
 10 files changed, 720 insertions(+), 318 deletions(-)
 create mode 100644 src/plugins/sample-tracker/batch.tsx
 create mode 100644 src/plugins/sample-tracker/batchList.tsx

       via  7328f1f28fd792781e32c06232042a58186ea733 (commit)
       via  784f4b9b6e2cbf98a364ae7cb0a2545b4dbdc981 (commit)
       via  49d9698047be5174952d1d6a7f38b6ec1cbfba06 (commit)
      from  91b752bb64edab61fede6668e4bada1ffc112861 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 7328f1f28fd792781e32c06232042a58186ea733
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Mar 18 17:35:12 2021 -0400

    Editing batches WIP
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/plugins/sample-tracker/batch.tsx b/src/plugins/sample-tracker/batch.tsx
index c965aedd..851c07b0 100644
--- a/src/plugins/sample-tracker/batch.tsx
+++ b/src/plugins/sample-tracker/batch.tsx
@@ -10,7 +10,7 @@ import { getProperty } from '~/store/properties/properties';
 import { RootState } from '~/store/store';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
-import { InjectedFormProps, Field, WrappedFieldProps, reduxForm, initialize, startSubmit, reset } from 'redux-form';
+import { InjectedFormProps, Field, WrappedFieldProps, reduxForm, startSubmit, reset } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField } from '~/views-components/form-fields/project-form-fields';
@@ -22,25 +22,17 @@ import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import Checkbox from '@material-ui/core/Checkbox';
 import { FormControl, InputLabel } from '@material-ui/core';
-import { FilterBuilder } from "~/services/api/filter-builder";
-import { GroupClass, GroupResource } from "~/models/group";
+import { GroupClass } from "~/models/group";
 
-import { sampleTrackerBatchType, batchListPanelActions, BATCH_LIST_PANEL_ID } from './batchList';
-import { AnalysisState, SampleStateSelect } from './extraction';
+import {
+    sampleTrackerBatchType, batchListPanelActions,
+    BATCH_LIST_PANEL_ID, BATCH_CREATE_FORM_NAME,
+    openBatchCreateDialog, BatchCreateFormDialogData
+} from './batchList';
+import { SampleStateSelect } from './extraction';
 
-const BATCH_CREATE_FORM_NAME = "batchCreateFormName";
-export const BATCH_PANEL_CURRENT_UUID = "BatchPanelCurrentUUID";
 
-export interface BatchCreateFormDialogData {
-    ownerUuid: string;
-    name: string;
-    state: AnalysisState;
-    selections: {
-        extraction: GroupResource,
-        value: boolean,
-        startingValue: boolean,
-    }[];
-}
+export const BATCH_PANEL_CURRENT_UUID = "BatchPanelCurrentUUID";
 
 type DialogProjectProps = WithDialogProps<BatchCreateFormDialogData> & InjectedFormProps<BatchCreateFormDialogData>;
 
@@ -71,9 +63,9 @@ const BatchAddFields = (props: DialogProjectProps) => <span>
 
 const DialogBatchCreate = (props: DialogProjectProps) =>
     <FormDialog
-        dialogTitle='New batch'
+        dialogTitle={props.data.selfUuid ? 'Edit batch' : 'New batch'}
         formFields={BatchAddFields}
-        submitLabel='Create a Batch'
+        submitLabel={props.data.selfUuid ? 'Update batch' : 'Create a Batch'}
         {...props}
     />;
 
@@ -82,13 +74,19 @@ const createBatch = (data: BatchCreateFormDialogData) =>
         dispatch(startSubmit(BATCH_CREATE_FORM_NAME));
         const p = {
             name: data.name,
+            ownerUuid: data.ownerUuid,
             groupClass: GroupClass.PROJECT,
             properties: {
                 "type": sampleTrackerBatchType,
                 "sample_tracker:state": data.state,
             }
         };
-        const newBatch = await services.projectService.create(p);
+        let newBatch;
+        if (data.selfUuid) {
+            newBatch = await services.projectService.update(data.selfUuid, p);
+        } else {
+            newBatch = await services.projectService.create(p);
+        }
 
         for (const s of data.selections) {
             if (s.value && !s.startingValue) {
@@ -103,6 +101,7 @@ const createBatch = (data: BatchCreateFormDialogData) =>
 
         dispatch(dialogActions.CLOSE_DIALOG({ id: BATCH_CREATE_FORM_NAME }));
         dispatch(reset(BATCH_CREATE_FORM_NAME));
+        dispatch(batchListPanelActions.REQUEST_ITEMS());
     };
 
 export const CreateBatchDialog = compose(
@@ -115,29 +114,6 @@ export const CreateBatchDialog = compose(
     })
 )(DialogBatchCreate);
 
-const openBatchCreateDialog = () =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const results = await services.groupsService.list({
-            filters: new FilterBuilder()
-                .addEqual("properties.type", "sample_tracker:extraction")
-                .addEqual("properties.sample_tracker:state", "NEW")
-                .getFilters()
-        });
-        const samples = await services.linkService.list({
-            filters: new FilterBuilder()
-                .addIn("uuid", results.items.map((item) => item.properties["sample_tracker:sample_uuid"]))
-                .getFilters()
-        });
-        console.log(samples);
-        const selections = results.items.map((item) => ({
-            extraction: item,
-            value: false,
-            startingValue: false
-        }));
-        dispatch(initialize(BATCH_CREATE_FORM_NAME, { selections }));
-        dispatch(dialogActions.OPEN_DIALOG({ id: BATCH_CREATE_FORM_NAME, data: { selections } }));
-    };
-
 interface TrackerProps {
     className?: string;
 }
diff --git a/src/plugins/sample-tracker/batchList.tsx b/src/plugins/sample-tracker/batchList.tsx
index ecf7d9b1..169583ee 100644
--- a/src/plugins/sample-tracker/batchList.tsx
+++ b/src/plugins/sample-tracker/batchList.tsx
@@ -23,19 +23,94 @@ import { progressIndicatorActions } from '~/store/progress-indicator/progress-in
 import { DataExplorer as DataExplorerState, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
 import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 import { updateResources } from "~/store/resources/resources-actions";
-import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { connect, DispatchProp } from 'react-redux';
+import { getResource } from "~/store/resources/resources";
+import { Typography } from '@material-ui/core';
+import { initialize } from 'redux-form';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { AnalysisState } from './extraction';
+import { StudyPathId } from './patient';
+import { matchPath } from "react-router";
+import { studyRoutePath } from './studyList';
 
 export const BATCH_LIST_PANEL_ID = "batchPanel";
 export const batchListPanelActions = bindDataExplorerActions(BATCH_LIST_PANEL_ID);
 export const sampleTrackerBatchType = "sample_tracker:batch";
 export const batchListRoutePath = "/sampleTracker/Batches";
 export const batchRoutePath = batchListRoutePath + "/:uuid";
-
+export const BATCH_CREATE_FORM_NAME = "batchCreateFormName";
 
 enum BatchPanelColumnNames {
     NAME = "Name"
 }
 
+export interface BatchCreateFormDialogData {
+    ownerUuid: string;
+    selfUuid?: string;
+    name: string;
+    state: AnalysisState;
+    selections: {
+        extraction: GroupResource,
+        value: boolean,
+        startingValue: boolean,
+    }[];
+}
+
+export const BatchNameComponent = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupResource>(props.uuid)(state.resources);
+        return resource;
+    })((resource: GroupResource & DispatchProp<any>) =>
+        <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }}
+            onClick={() => resource.dispatch<any>(openBatchCreateDialog(resource))}
+        >{resource.name}</Typography>);
+
+export const openBatchCreateDialog = (editExisting?: GroupResource) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const filters = new FilterBuilder()
+            .addEqual("properties.type", "sample_tracker:extraction")
+            .addEqual("properties.sample_tracker:state", "NEW")
+            .addEqual("properties.sample_tracker:batch_uuid", "");
+        const results = await services.groupsService.list({
+            filters: filters.getFilters()
+        });
+        const samples = await services.linkService.list({
+            filters: new FilterBuilder()
+                .addIn("uuid", results.items.map((item) => item.properties["sample_tracker:sample_uuid"]))
+                .getFilters()
+        });
+        console.log(samples);
+        const selections = results.items.map((item) => ({
+            extraction: item,
+            value: false,
+            startingValue: false
+        }));
+
+        const studyid = matchPath<StudyPathId>(getState().router.location!.pathname, { path: studyRoutePath, exact: true });
+
+        const formup: Partial<BatchCreateFormDialogData> = { selections, ownerUuid: studyid!.params.uuid };
+        if (editExisting) {
+            formup.selfUuid = editExisting.uuid;
+            formup.name = editExisting.name;
+            formup.state = editExisting.properties["sample_tracker:state"];
+            const results2 = await services.groupsService.list({
+                filters: new FilterBuilder()
+                    .addEqual("properties.type", "sample_tracker:extraction")
+                    .addEqual("properties.sample_tracker:batch_uuid", editExisting.uuid)
+                    .getFilters()
+            });
+            for (const item of results2.items) {
+                selections.push({
+                    extraction: item,
+                    value: true,
+                    startingValue: true
+                });
+            }
+        }
+        dispatch(initialize(BATCH_CREATE_FORM_NAME, formup));
+        dispatch(dialogActions.OPEN_DIALOG({ id: BATCH_CREATE_FORM_NAME, data: formup }));
+    };
+
 export const batchListPanelColumns: DataColumns<string> = [
     {
         name: BatchPanelColumnNames.NAME,
@@ -43,15 +118,10 @@ export const batchListPanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ResourceName uuid={uuid} />
+        render: uuid => <BatchNameComponent uuid={uuid} />
     }
 ];
 
-export const openBatchListPanel = (dispatch: Dispatch) => {
-    // dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
-    dispatch(batchListPanelActions.REQUEST_ITEMS());
-};
-
 export const BatchListMainPanel = () =>
     <DataExplorer
         id={BATCH_LIST_PANEL_ID}
@@ -71,12 +141,6 @@ const setItems = (listResults: ListResults<GroupResource>) =>
     });
 
 const getFilters = (dataExplorer: DataExplorerState) => {
-    //    const columns = dataExplorer.columns as DataColumns<string>;
-    //    const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
-    //    const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
-    //    const activeStatusFilter = Object.keys(statusColumnFilters).find(
-    //        filterName => statusColumnFilters[filterName].selected
-    //    );
     const fb = new FilterBuilder();
     fb.addEqual("properties.type", sampleTrackerBatchType);
 
@@ -108,9 +172,6 @@ export class BatchListPanelMiddlewareService extends DataExplorerMiddlewareServi
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const response = await this.services.groupsService.list(getParams(dataExplorer));
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
-            for (const i of response.items) {
-                i.uuid = batchListRoutePath + "/" + i.uuid;
-            }
             api.dispatch(updateResources(response.items));
             api.dispatch(setItems(response));
         } catch (e) {
diff --git a/src/plugins/sample-tracker/extraction.tsx b/src/plugins/sample-tracker/extraction.tsx
index 7afae1b8..0335a2c1 100644
--- a/src/plugins/sample-tracker/extraction.tsx
+++ b/src/plugins/sample-tracker/extraction.tsx
@@ -182,6 +182,7 @@ const createExtraction = (data: ExtractionCreateFormDialogData) =>
                 "sample_tracker:additional_id": data.additionalId,
                 "sample_tracker:state": data.state,
                 "sample_tracker:sample_uuid": data.sampleUuid,
+                "sample_tracker:batch_uuid": "",
             }
         };
         // const newProject =
diff --git a/src/plugins/sample-tracker/index.tsx b/src/plugins/sample-tracker/index.tsx
index 3d378335..cbc305df 100644
--- a/src/plugins/sample-tracker/index.tsx
+++ b/src/plugins/sample-tracker/index.tsx
@@ -23,7 +23,7 @@ import { addMenuActionSet } from '~/views-components/context-menu/context-menu';
 
 import {
     StudyListMainPanel,
-    studyListPanelColumns, studyListPanelActions, openStudyListPanel,
+    openStudyListPanel,
     StudyListPanelMiddlewareService, STUDY_LIST_PANEL_ID,
     studyListRoutePath, studyRoutePath
 } from './studyList';
@@ -34,7 +34,7 @@ import {
 import {
     PATIENT_LIST_PANEL_ID,
     PatientListPanelMiddlewareService,
-    patientListPanelColumns, patientListPanelActions, patientRoutePath, patientBaseRoutePath
+    patientRoutePath, patientBaseRoutePath
 } from './patientList';
 import {
     openPatientPanel, PatientMainPanel, PATIENT_SAMPLE_MENU, patientSampleActionSet,
@@ -43,7 +43,7 @@ import {
 
 import {
     SampleListPanelMiddlewareService,
-    SAMPLE_LIST_PANEL_ID, sampleListPanelColumns, sampleListPanelActions
+    SAMPLE_LIST_PANEL_ID,
 } from './sampleList';
 import {
     AddSampleMenuComponent, CreateSampleDialog
@@ -52,6 +52,10 @@ import {
 import {
     AddBatchMenuComponent, CreateBatchDialog
 } from './batch';
+import {
+    BatchListPanelMiddlewareService, BATCH_LIST_PANEL_ID,
+} from './batchList';
+
 
 import { CreateExtractionDialog } from './extraction';
 
@@ -96,7 +100,6 @@ export const register = (pluginConfig: PluginConfig) => {
         if (matchPath(pathname, { path: studyListRoutePath, exact: true })) {
             store.dispatch(handleFirstTimeLoad(
                 (dispatch: Dispatch) => {
-                    dispatch(studyListPanelActions.SET_COLUMNS({ columns: studyListPanelColumns }));
                     dispatch<any>(openStudyListPanel);
                     dispatch<any>(activateSidePanelTreeItem(categoryName));
                     dispatch<any>(setSidePanelBreadcrumbs(categoryName));
@@ -107,7 +110,6 @@ export const register = (pluginConfig: PluginConfig) => {
         if (studyid) {
             store.dispatch(handleFirstTimeLoad(
                 (dispatch: Dispatch) => {
-                    dispatch(patientListPanelActions.SET_COLUMNS({ columns: patientListPanelColumns }));
                     dispatch<any>(openStudyPanel(studyid.params.uuid));
                     // dispatch<any>(activateSidePanelTreeItem(categoryName));
                     // const name = getProperty(PATIENT_PANEL_CURRENT_UUID)(state.properties),
@@ -122,7 +124,6 @@ export const register = (pluginConfig: PluginConfig) => {
         if (patientid) {
             store.dispatch(handleFirstTimeLoad(
                 async (dispatch: Dispatch) => {
-                    dispatch(sampleListPanelActions.SET_COLUMNS({ columns: sampleListPanelColumns }));
                     dispatch<any>(openPatientPanel(patientid.params.uuid));
                     // dispatch<any>(activateSidePanelTreeItem(categoryName));
                     const patientrsc = await dispatch<any>(loadResource(patientid.params.uuid));
@@ -161,6 +162,9 @@ export const register = (pluginConfig: PluginConfig) => {
         elms.push(dataExplorerMiddleware(
             new SampleListPanelMiddlewareService(services, SAMPLE_LIST_PANEL_ID)
         ));
+        elms.push(dataExplorerMiddleware(
+            new BatchListPanelMiddlewareService(services, BATCH_LIST_PANEL_ID)
+        ));
 
         return elms;
     });
diff --git a/src/plugins/sample-tracker/patient.tsx b/src/plugins/sample-tracker/patient.tsx
index a35d311b..4700c734 100644
--- a/src/plugins/sample-tracker/patient.tsx
+++ b/src/plugins/sample-tracker/patient.tsx
@@ -27,7 +27,10 @@ import { matchPath } from "react-router";
 import { ServiceRepository } from "~/services/services";
 
 import { PATIENT_PANEL_CURRENT_UUID, sampleTrackerPatientType } from './patientList';
-import { SAMPLE_LIST_PANEL_ID, sampleListPanelActions, sampleBaseRoutePath } from './sampleList';
+import {
+    SAMPLE_LIST_PANEL_ID, sampleListPanelActions,
+    sampleBaseRoutePath, sampleListPanelColumns
+} from './sampleList';
 import { studyRoutePath } from './studyList';
 
 export const PATIENT_SAMPLE_MENU = "Sample Tracker - Patient Sample menu";
@@ -89,6 +92,7 @@ export const AddPatientMenuComponent = connect<{}, {}, MenuItemProps>(patientsMa
 
 export const openPatientPanel = (projectUuid: string) =>
     (dispatch: Dispatch) => {
+        dispatch(sampleListPanelActions.SET_COLUMNS({ columns: sampleListPanelColumns }));
         dispatch(propertiesActions.SET_PROPERTY({ key: PATIENT_PANEL_CURRENT_UUID, value: projectUuid }));
         dispatch(sampleListPanelActions.REQUEST_ITEMS());
     };
diff --git a/src/plugins/sample-tracker/study.tsx b/src/plugins/sample-tracker/study.tsx
index 58cfcd07..c46f2d6b 100644
--- a/src/plugins/sample-tracker/study.tsx
+++ b/src/plugins/sample-tracker/study.tsx
@@ -16,7 +16,7 @@ import { ProjectCreateFormDialogData } from '~/store/projects/project-create-act
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
 import { dialogActions } from "~/store/dialog/dialog-actions";
-import { MenuItem } from "@material-ui/core";
+import { MenuItem, Tabs, Tab } from "@material-ui/core";
 import { createProject } from "~/store/workbench/workbench-actions";
 import { reduxForm, initialize } from 'redux-form';
 import { withDialog } from "~/store/dialog/with-dialog";
@@ -24,9 +24,17 @@ import { ServiceRepository } from "~/services/services";
 
 import { sampleTrackerStudyType } from './studyList';
 
-import { PATIENT_LIST_PANEL_ID, STUDY_PANEL_CURRENT_UUID, patientListPanelActions } from './patientList';
+import {
+    PATIENT_LIST_PANEL_ID, STUDY_PANEL_CURRENT_UUID,
+    patientListPanelActions, patientListPanelColumns
+} from './patientList';
+
+import {
+    BATCH_LIST_PANEL_ID, batchListPanelActions, batchListPanelColumns
+} from './batchList';
 
 const STUDY_CREATE_FORM_NAME = "studyCreateFormName";
+const STUDY_PANEL_CURRENT_TAB = "studyPanelCurrentTab";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -77,28 +85,59 @@ export const AddStudyMenuComponent = connect()(
 
 export const openStudyPanel = (projectUuid: string) =>
     (dispatch: Dispatch) => {
+        dispatch(patientListPanelActions.SET_COLUMNS({ columns: patientListPanelColumns }));
+        dispatch(batchListPanelActions.SET_COLUMNS({ columns: batchListPanelColumns }));
         dispatch(propertiesActions.SET_PROPERTY({ key: STUDY_PANEL_CURRENT_UUID, value: projectUuid }));
         dispatch(patientListPanelActions.REQUEST_ITEMS());
+        dispatch(batchListPanelActions.REQUEST_ITEMS());
     };
 
 interface StudyProps {
     studyUuid: string;
+    currentTab: number;
+    changeTab: (event: any, value: number) => void;
 }
 
 export const studyMapStateToProps = (state: RootState) => ({
     studyUuid: getProperty(STUDY_PANEL_CURRENT_UUID)(state.properties),
+    currentTab: getProperty(STUDY_PANEL_CURRENT_TAB)(state.properties) || 0,
+});
+
+export const studyDispatchToProps = (dispatch: Dispatch) => ({
+    changeTab: (event: any, value: number) => {
+        dispatch(propertiesActions.SET_PROPERTY({ key: STUDY_PANEL_CURRENT_TAB, value }));
+    }
 });
 
-export const StudyMainPanel = connect(studyMapStateToProps)(
-    ({ studyUuid }: StudyProps) =>
+// onChange={}
+export const StudyMainPanel = connect(studyMapStateToProps, studyDispatchToProps)(
+    ({ studyUuid, currentTab, changeTab }: StudyProps) =>
         <div>
-            <DataExplorer
-                id={PATIENT_LIST_PANEL_ID}
-                onRowClick={(uuid: string) => { }}
-                onRowDoubleClick={(uuid: string) => { }}
-                onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
-                contextMenuColumn={true}
-                dataTableDefaultView={
-                    <DataTableDefaultView />
-                } />
+            <Tabs
+                onChange={changeTab}
+                value={currentTab}>
+                <Tab key={`tab-label-patients`} disableRipple label="Patients" />
+                <Tab key={`tab-label-batches`} disableRipple label="Batches" />
+            </Tabs>
+
+            {currentTab === 0 &&
+                <DataExplorer
+                    id={PATIENT_LIST_PANEL_ID}
+                    onRowClick={(uuid: string) => { }}
+                    onRowDoubleClick={(uuid: string) => { }}
+                    onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+                    contextMenuColumn={true}
+                    dataTableDefaultView={
+                        <DataTableDefaultView />
+                    } />}
+            {currentTab === 1 &&
+                <DataExplorer
+                    id={BATCH_LIST_PANEL_ID}
+                    onRowClick={(uuid: string) => { }}
+                    onRowDoubleClick={(uuid: string) => { }}
+                    onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+                    contextMenuColumn={true}
+                    dataTableDefaultView={
+                        <DataTableDefaultView />
+                    } />}
         </div>);
diff --git a/src/plugins/sample-tracker/studyList.tsx b/src/plugins/sample-tracker/studyList.tsx
index 543a8b62..b8857249 100644
--- a/src/plugins/sample-tracker/studyList.tsx
+++ b/src/plugins/sample-tracker/studyList.tsx
@@ -49,6 +49,7 @@ export const studyListPanelColumns: DataColumns<string> = [
 
 export const openStudyListPanel = (dispatch: Dispatch) => {
     // dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+    dispatch(studyListPanelActions.SET_COLUMNS({ columns: studyListPanelColumns }));
     dispatch(studyListPanelActions.REQUEST_ITEMS());
 };
 

commit 784f4b9b6e2cbf98a364ae7cb0a2545b4dbdc981
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Mar 18 15:35:51 2021 -0400

    Can create batches
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/plugins/sample-tracker/batch.tsx b/src/plugins/sample-tracker/batch.tsx
new file mode 100644
index 00000000..c965aedd
--- /dev/null
+++ b/src/plugins/sample-tracker/batch.tsx
@@ -0,0 +1,176 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { compose, Dispatch } from "redux";
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { getProperty } from '~/store/properties/properties';
+import { RootState } from '~/store/store';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { InjectedFormProps, Field, WrappedFieldProps, reduxForm, initialize, startSubmit, reset } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField } from '~/views-components/form-fields/project-form-fields';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { MenuItem } from "@material-ui/core";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { ServiceRepository } from "~/services/services";
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import Checkbox from '@material-ui/core/Checkbox';
+import { FormControl, InputLabel } from '@material-ui/core';
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { GroupClass, GroupResource } from "~/models/group";
+
+import { sampleTrackerBatchType, batchListPanelActions, BATCH_LIST_PANEL_ID } from './batchList';
+import { AnalysisState, SampleStateSelect } from './extraction';
+
+const BATCH_CREATE_FORM_NAME = "batchCreateFormName";
+export const BATCH_PANEL_CURRENT_UUID = "BatchPanelCurrentUUID";
+
+export interface BatchCreateFormDialogData {
+    ownerUuid: string;
+    name: string;
+    state: AnalysisState;
+    selections: {
+        extraction: GroupResource,
+        value: boolean,
+        startingValue: boolean,
+    }[];
+}
+
+type DialogProjectProps = WithDialogProps<BatchCreateFormDialogData> & InjectedFormProps<BatchCreateFormDialogData>;
+
+export const FormCheckbox =
+    ({ input }: WrappedFieldProps) =>
+        <FormControl >
+            <Checkbox {...input} />
+        </FormControl>;
+
+const mustBeDefined = (value: any) => value === undefined ? "Must be defined" : undefined;
+
+const BatchAddFields = (props: DialogProjectProps) => <span>
+    <ProjectNameField label="External batch id" />
+    <InputLabel>State</InputLabel>
+    <div><Field
+        name='state'
+        component={SampleStateSelect}
+        validate={mustBeDefined}
+    /></div>
+    <List>
+        {props.data.selections.map((val, idx) =>
+            <ListItem key={val.extraction.uuid}>
+                <Field name={"selections[" + idx + "].value"} component={FormCheckbox} type="checkbox" />
+                <span>{val.extraction.name}</span>
+            </ListItem>)}
+    </List>
+</span>;
+
+const DialogBatchCreate = (props: DialogProjectProps) =>
+    <FormDialog
+        dialogTitle='New batch'
+        formFields={BatchAddFields}
+        submitLabel='Create a Batch'
+        {...props}
+    />;
+
+const createBatch = (data: BatchCreateFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(BATCH_CREATE_FORM_NAME));
+        const p = {
+            name: data.name,
+            groupClass: GroupClass.PROJECT,
+            properties: {
+                "type": sampleTrackerBatchType,
+                "sample_tracker:state": data.state,
+            }
+        };
+        const newBatch = await services.projectService.create(p);
+
+        for (const s of data.selections) {
+            if (s.value && !s.startingValue) {
+                s.extraction.properties["sample_tracker:batch_uuid"] = newBatch.uuid;
+                s.extraction.properties["sample_tracker:state"] = data.state;
+                await services.groupsService.update(s.extraction.uuid, { properties: s.extraction.properties });
+            } else if (!s.value && s.startingValue) {
+                delete s.extraction.properties["sample_tracker:batch_uuid"];
+                await services.groupsService.update(s.extraction.uuid, { properties: s.extraction.properties });
+            }
+        }
+
+        dispatch(dialogActions.CLOSE_DIALOG({ id: BATCH_CREATE_FORM_NAME }));
+        dispatch(reset(BATCH_CREATE_FORM_NAME));
+    };
+
+export const CreateBatchDialog = compose(
+    withDialog(BATCH_CREATE_FORM_NAME),
+    reduxForm<BatchCreateFormDialogData>({
+        form: BATCH_CREATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(createBatch(data));
+        }
+    })
+)(DialogBatchCreate);
+
+const openBatchCreateDialog = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const results = await services.groupsService.list({
+            filters: new FilterBuilder()
+                .addEqual("properties.type", "sample_tracker:extraction")
+                .addEqual("properties.sample_tracker:state", "NEW")
+                .getFilters()
+        });
+        const samples = await services.linkService.list({
+            filters: new FilterBuilder()
+                .addIn("uuid", results.items.map((item) => item.properties["sample_tracker:sample_uuid"]))
+                .getFilters()
+        });
+        console.log(samples);
+        const selections = results.items.map((item) => ({
+            extraction: item,
+            value: false,
+            startingValue: false
+        }));
+        dispatch(initialize(BATCH_CREATE_FORM_NAME, { selections }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: BATCH_CREATE_FORM_NAME, data: { selections } }));
+    };
+
+interface TrackerProps {
+    className?: string;
+}
+
+export const AddBatchMenuComponent = connect()(
+    ({ dispatch, className }: TrackerProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openBatchCreateDialog())}>Add Batch</MenuItem >
+);
+
+export const openBatchPanel = (projectUuid: string) =>
+    (dispatch: Dispatch) => {
+        dispatch(propertiesActions.SET_PROPERTY({ key: BATCH_PANEL_CURRENT_UUID, value: projectUuid }));
+        dispatch(batchListPanelActions.REQUEST_ITEMS());
+    };
+
+interface BatchProps {
+    batchUuid: string;
+}
+
+export const batchMapStateToProps = (state: RootState) => ({
+    batchUuid: getProperty(BATCH_PANEL_CURRENT_UUID)(state.properties),
+});
+
+export const BatchMainPanel = connect(batchMapStateToProps)(
+    ({ batchUuid }: BatchProps) =>
+        <div>
+            <DataExplorer
+                id={BATCH_LIST_PANEL_ID}
+                onRowClick={(uuid: string) => { }}
+                onRowDoubleClick={(uuid: string) => { }}
+                onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+                contextMenuColumn={true}
+                dataTableDefaultView={
+                    <DataTableDefaultView />
+                } />
+        </div>);
diff --git a/src/plugins/sample-tracker/studyList.tsx b/src/plugins/sample-tracker/batchList.tsx
similarity index 51%
copy from src/plugins/sample-tracker/studyList.tsx
copy to src/plugins/sample-tracker/batchList.tsx
index 4180a9ee..ecf7d9b1 100644
--- a/src/plugins/sample-tracker/studyList.tsx
+++ b/src/plugins/sample-tracker/batchList.tsx
@@ -3,20 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { InjectedFormProps } from 'redux-form';
-import { WithDialogProps } from '~/store/dialog/with-dialog';
-import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
-import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
-import { dialogActions } from "~/store/dialog/dialog-actions";
 import { ServiceRepository } from "~/services/services";
-import { compose, MiddlewareAPI, Dispatch } from "redux";
-import { reduxForm, initialize } from 'redux-form';
-import { withDialog } from "~/store/dialog/with-dialog";
+import { MiddlewareAPI, Dispatch } from "redux";
 import { RootState } from '~/store/store';
-import { DispatchProp, connect } from 'react-redux';
-import { MenuItem } from "@material-ui/core";
-import { createProject } from "~/store/workbench/workbench-actions";
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { DataColumns } from '~/components/data-table/data-table';
@@ -36,70 +25,20 @@ import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 import { updateResources } from "~/store/resources/resources-actions";
 import { ResourceName } from '~/views-components/data-explorer/renderers';
 
-const STUDY_CREATE_FORM_NAME = "studyCreateFormName";
-export const STUDY_LIST_PANEL_ID = "studyPanel";
-export const studyListPanelActions = bindDataExplorerActions(STUDY_LIST_PANEL_ID);
-export const sampleTrackerStudyType = "sample_tracker:study";
-export const studyListRoutePath = "/sampleTracker/Studies";
-export const studyRoutePath = studyListRoutePath + "/:uuid";
-
-export interface ProjectCreateFormDialogData {
-    ownerUuid: string;
-    name: string;
-    description: string;
-}
-
-type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
-
-const StudyAddFields = () => <span>
-    <ProjectNameField label="Study name" />
-    <ProjectDescriptionField />
-</span>;
-
-const DialogStudyCreate = (props: DialogProjectProps) =>
-    <FormDialog
-        dialogTitle='New study'
-        formFields={StudyAddFields}
-        submitLabel='Create a Study'
-        {...props}
-    />;
-
-export const CreateStudyDialog = compose(
-    withDialog(STUDY_CREATE_FORM_NAME),
-    reduxForm<ProjectCreateFormDialogData>({
-        form: STUDY_CREATE_FORM_NAME,
-        onSubmit: (data, dispatch) => {
-            data.properties = { type: sampleTrackerStudyType };
-            dispatch(createProject(data));
-        }
-    })
-)(DialogStudyCreate);
-
-
-interface TrackerProps {
-    className?: string;
-}
-
-const studiesMapStateToProps = (state: RootState) => ({});
-
-const openStudyCreateDialog = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(initialize(STUDY_CREATE_FORM_NAME, {}));
-        dispatch(dialogActions.OPEN_DIALOG({ id: STUDY_CREATE_FORM_NAME, data: {} }));
-    };
+export const BATCH_LIST_PANEL_ID = "batchPanel";
+export const batchListPanelActions = bindDataExplorerActions(BATCH_LIST_PANEL_ID);
+export const sampleTrackerBatchType = "sample_tracker:batch";
+export const batchListRoutePath = "/sampleTracker/Batches";
+export const batchRoutePath = batchListRoutePath + "/:uuid";
 
-export const AddStudyMenuComponent = connect(studiesMapStateToProps)(
-    ({ dispatch, className }: TrackerProps & DispatchProp<any>) =>
-        <MenuItem className={className} onClick={() => dispatch(openStudyCreateDialog())}>Add Study</MenuItem >
-);
 
-enum StudyPanelColumnNames {
+enum BatchPanelColumnNames {
     NAME = "Name"
 }
 
-export const studyListPanelColumns: DataColumns<string> = [
+export const batchListPanelColumns: DataColumns<string> = [
     {
-        name: StudyPanelColumnNames.NAME,
+        name: BatchPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
@@ -108,26 +47,25 @@ export const studyListPanelColumns: DataColumns<string> = [
     }
 ];
 
-export const openStudyListPanel = (dispatch: Dispatch) => {
+export const openBatchListPanel = (dispatch: Dispatch) => {
     // dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
-    dispatch(studyListPanelActions.REQUEST_ITEMS());
+    dispatch(batchListPanelActions.REQUEST_ITEMS());
 };
 
-export const StudyListMainPanel = connect(studiesMapStateToProps)(
-    ({ }: TrackerProps) =>
-        <DataExplorer
-            id={STUDY_LIST_PANEL_ID}
-            onRowClick={(uuid: string) => { }}
-            onRowDoubleClick={(uuid: string) => { }}
-            onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
-            contextMenuColumn={true}
-            dataTableDefaultView={
-                <DataTableDefaultView />
-            } />);
+export const BatchListMainPanel = () =>
+    <DataExplorer
+        id={BATCH_LIST_PANEL_ID}
+        onRowClick={(uuid: string) => { }}
+        onRowDoubleClick={(uuid: string) => { }}
+        onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+        contextMenuColumn={true}
+        dataTableDefaultView={
+            <DataTableDefaultView />
+        } />;
 
 
 const setItems = (listResults: ListResults<GroupResource>) =>
-    studyListPanelActions.SET_ITEMS({
+    batchListPanelActions.SET_ITEMS({
         ...listResultsToDataExplorerItemsMeta(listResults),
         items: listResults.items.map(resource => resource.uuid),
     });
@@ -140,7 +78,7 @@ const getFilters = (dataExplorer: DataExplorerState) => {
     //        filterName => statusColumnFilters[filterName].selected
     //    );
     const fb = new FilterBuilder();
-    fb.addEqual("properties.type", sampleTrackerStudyType);
+    fb.addEqual("properties.type", sampleTrackerBatchType);
 
     const nameFilters = new FilterBuilder()
         .addILike("name", dataExplorer.searchValue)
@@ -157,7 +95,7 @@ const getParams = (dataExplorer: DataExplorerState) => ({
     filters: getFilters(dataExplorer),
 });
 
-export class StudyListPanelMiddlewareService extends DataExplorerMiddlewareService {
+export class BatchListPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
         super(id);
     }
@@ -171,13 +109,13 @@ export class StudyListPanelMiddlewareService extends DataExplorerMiddlewareServi
             const response = await this.services.groupsService.list(getParams(dataExplorer));
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
             for (const i of response.items) {
-                i.uuid = studyListRoutePath + "/" + i.uuid;
+                i.uuid = batchListRoutePath + "/" + i.uuid;
             }
             api.dispatch(updateResources(response.items));
             api.dispatch(setItems(response));
         } catch (e) {
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
-            api.dispatch(studyListPanelActions.SET_ITEMS({
+            api.dispatch(batchListPanelActions.SET_ITEMS({
                 items: [],
                 itemsAvailable: 0,
                 page: 0,
diff --git a/src/plugins/sample-tracker/extraction.tsx b/src/plugins/sample-tracker/extraction.tsx
index 4dc38d9c..7afae1b8 100644
--- a/src/plugins/sample-tracker/extraction.tsx
+++ b/src/plugins/sample-tracker/extraction.tsx
@@ -28,10 +28,11 @@ enum ExtractionType {
     RNA = "RNA",
 }
 
-enum AnalysisState {
+export enum AnalysisState {
     NEW = "NEW",
     AT_SEQUENCING = "AT_SEQUENCING",
     SEQUENCED = "SEQUENCED",
+    SEQ_FAILED = "SEQ_FAILED",
     ANALYSIS_COMPLETE = "ANALYSIS_COMPLETE"
 }
 
@@ -69,6 +70,9 @@ export const SampleStateSelect = styles(
                 <MenuItem value={AnalysisState.SEQUENCED}>
                     SEQUENCED
 		</MenuItem>
+                <MenuItem value={AnalysisState.SEQ_FAILED}>
+                    SEQ_FAILED
+		</MenuItem>
                 <MenuItem value={AnalysisState.ANALYSIS_COMPLETE}>
                     ANALYSIS_COMPLETE
 		</MenuItem>
diff --git a/src/plugins/sample-tracker/index.tsx b/src/plugins/sample-tracker/index.tsx
index 001889dd..3d378335 100644
--- a/src/plugins/sample-tracker/index.tsx
+++ b/src/plugins/sample-tracker/index.tsx
@@ -22,16 +22,25 @@ import { GroupResource } from "~/models/group";
 import { addMenuActionSet } from '~/views-components/context-menu/context-menu';
 
 import {
-    AddStudyMenuComponent, StudyListMainPanel, CreateStudyDialog,
+    StudyListMainPanel,
     studyListPanelColumns, studyListPanelActions, openStudyListPanel,
     StudyListPanelMiddlewareService, STUDY_LIST_PANEL_ID,
     studyListRoutePath, studyRoutePath
 } from './studyList';
 import {
-    AddPatientMenuComponent, CreatePatientDialog, PATIENT_LIST_PANEL_ID, StudyPathId,
+    openStudyPanel, StudyMainPanel, CreateStudyDialog, AddStudyMenuComponent
+} from './study';
+
+import {
+    PATIENT_LIST_PANEL_ID,
     PatientListPanelMiddlewareService,
     patientListPanelColumns, patientListPanelActions, patientRoutePath, patientBaseRoutePath
 } from './patientList';
+import {
+    openPatientPanel, PatientMainPanel, PATIENT_SAMPLE_MENU, patientSampleActionSet,
+    CreatePatientDialog, AddPatientMenuComponent, StudyPathId,
+} from './patient';
+
 import {
     SampleListPanelMiddlewareService,
     SAMPLE_LIST_PANEL_ID, sampleListPanelColumns, sampleListPanelActions
@@ -39,12 +48,11 @@ import {
 import {
     AddSampleMenuComponent, CreateSampleDialog
 } from './sample';
+
 import {
-    openStudyPanel, StudyMainPanel
-} from './study';
-import {
-    openPatientPanel, PatientMainPanel, PATIENT_SAMPLE_MENU, patientSampleActionSet
-} from './patient';
+    AddBatchMenuComponent, CreateBatchDialog
+} from './batch';
+
 import { CreateExtractionDialog } from './extraction';
 
 const categoryName = "Studies";
@@ -62,6 +70,7 @@ export const register = (pluginConfig: PluginConfig) => {
         elms.push(<AddStudyMenuComponent className={menuItemClass} />);
         elms.push(<AddPatientMenuComponent className={menuItemClass} />);
         elms.push(<AddSampleMenuComponent className={menuItemClass} />);
+        elms.push(<AddBatchMenuComponent className={menuItemClass} />);
         return elms;
     });
 
@@ -140,6 +149,7 @@ export const register = (pluginConfig: PluginConfig) => {
     pluginConfig.dialogs.push(<CreatePatientDialog />);
     pluginConfig.dialogs.push(<CreateSampleDialog />);
     pluginConfig.dialogs.push(<CreateExtractionDialog />);
+    pluginConfig.dialogs.push(<CreateBatchDialog />);
 
     pluginConfig.middlewares.push((elms, services) => {
         elms.push(dataExplorerMiddleware(
diff --git a/src/plugins/sample-tracker/patient.tsx b/src/plugins/sample-tracker/patient.tsx
index c92e7188..a35d311b 100644
--- a/src/plugins/sample-tracker/patient.tsx
+++ b/src/plugins/sample-tracker/patient.tsx
@@ -4,7 +4,7 @@
 
 import * as React from 'react';
 import { DispatchProp, connect } from 'react-redux';
-import { Dispatch } from "redux";
+import { compose, Dispatch } from "redux";
 import { propertiesActions } from "~/store/properties/properties-actions";
 import { getProperty } from '~/store/properties/properties';
 import { RootState } from '~/store/store';
@@ -14,11 +14,78 @@ import { openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ResourceKind } from '~/models/resource';
 import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
 import { openExtractionCreateDialog } from "./extraction";
+import { InjectedFormProps, reduxForm, initialize } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField } from '~/views-components/form-fields/project-form-fields';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { MenuItem } from "@material-ui/core";
+import { createProject } from "~/store/workbench/workbench-actions";
+import { matchPath } from "react-router";
+import { ServiceRepository } from "~/services/services";
 
-import { PATIENT_PANEL_CURRENT_UUID } from './patientList';
+import { PATIENT_PANEL_CURRENT_UUID, sampleTrackerPatientType } from './patientList';
 import { SAMPLE_LIST_PANEL_ID, sampleListPanelActions, sampleBaseRoutePath } from './sampleList';
+import { studyRoutePath } from './studyList';
 
 export const PATIENT_SAMPLE_MENU = "Sample Tracker - Patient Sample menu";
+const PATIENT_CREATE_FORM_NAME = "patientCreateFormName";
+
+type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
+
+const PatientAddFields = () => <span>
+    <ProjectNameField label="Patient anonymized identifier" />
+</span>;
+
+const DialogPatientCreate = (props: DialogProjectProps) =>
+    <FormDialog
+        dialogTitle='Add patient'
+        formFields={PatientAddFields}
+        submitLabel='Add a patient'
+        {...props}
+    />;
+
+export const CreatePatientDialog = compose(
+    withDialog(PATIENT_CREATE_FORM_NAME),
+    reduxForm<ProjectCreateFormDialogData>({
+        form: PATIENT_CREATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            data.properties = { type: sampleTrackerPatientType };
+            dispatch(createProject(data));
+        }
+    })
+)(DialogPatientCreate);
+
+export interface MenuItemProps {
+    className?: string;
+    studyUuid?: string;
+}
+
+export interface StudyPathId {
+    uuid: string;
+}
+
+export const patientsMapStateToProps = (state: RootState) => {
+    const props: MenuItemProps = {};
+    const studyid = matchPath<StudyPathId>(state.router.location!.pathname, { path: studyRoutePath, exact: true });
+    if (studyid) {
+        props.studyUuid = studyid.params.uuid;
+    }
+    return props;
+};
+
+const openPatientCreateDialog = (studyUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(initialize(PATIENT_CREATE_FORM_NAME, { ownerUuid: studyUuid }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: PATIENT_CREATE_FORM_NAME, data: {} }));
+    };
+
+export const AddPatientMenuComponent = connect<{}, {}, MenuItemProps>(patientsMapStateToProps)(
+    ({ studyUuid, dispatch, className }: MenuItemProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openPatientCreateDialog(studyUuid!))} disabled={!studyUuid}>Add Patient</MenuItem >
+);
 
 export const openPatientPanel = (projectUuid: string) =>
     (dispatch: Dispatch) => {
diff --git a/src/plugins/sample-tracker/patientList.tsx b/src/plugins/sample-tracker/patientList.tsx
index 86cbd369..4b0adca2 100644
--- a/src/plugins/sample-tracker/patientList.tsx
+++ b/src/plugins/sample-tracker/patientList.tsx
@@ -3,20 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { InjectedFormProps } from 'redux-form';
-import { WithDialogProps } from '~/store/dialog/with-dialog';
-import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
-import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectNameField } from '~/views-components/form-fields/project-form-fields';
-import { dialogActions } from "~/store/dialog/dialog-actions";
+
 import { ServiceRepository } from "~/services/services";
-import { compose, MiddlewareAPI, Dispatch } from "redux";
-import { reduxForm, initialize } from 'redux-form';
-import { withDialog } from "~/store/dialog/with-dialog";
+import { MiddlewareAPI, Dispatch } from "redux";
 import { RootState } from '~/store/store';
-import { DispatchProp, connect } from 'react-redux';
-import { MenuItem } from "@material-ui/core";
-import { createProject } from "~/store/workbench/workbench-actions";
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { DataColumns } from '~/components/data-table/data-table';
@@ -34,14 +24,9 @@ import { ListResults } from '~/services/common-service/common-service';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { DataExplorer as DataExplorerState, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
 import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
-import { updateResources } from "~/store/resources/resources-actions";
-import {
-    studyRoutePath
-} from './studyList';
-import { matchPath } from "react-router";
 import { getProperty } from '~/store/properties/properties';
+import { updateResources } from "~/store/resources/resources-actions";
 
-const PATIENT_CREATE_FORM_NAME = "patientCreateFormName";
 export const PATIENT_LIST_PANEL_ID = "patientListPanel";
 export const patientListPanelActions = bindDataExplorerActions(PATIENT_LIST_PANEL_ID);
 export const sampleTrackerPatientType = "sample_tracker:patient";
@@ -50,67 +35,6 @@ export const PATIENT_PANEL_CURRENT_UUID = "PatientPanelCurrentUUID";
 export const patientBaseRoutePath = "/SampleTracker/Patient";
 export const patientRoutePath = patientBaseRoutePath + "/:uuid";
 
-export interface ProjectCreateFormDialogData {
-    ownerUuid: string;
-    name: string;
-    description: string;
-}
-
-type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
-
-const PatientAddFields = () => <span>
-    <ProjectNameField label="Patient anonymized identifier" />
-</span>;
-
-const DialogPatientCreate = (props: DialogProjectProps) =>
-    <FormDialog
-        dialogTitle='Add patient'
-        formFields={PatientAddFields}
-        submitLabel='Add a patient'
-        {...props}
-    />;
-
-export const CreatePatientDialog = compose(
-    withDialog(PATIENT_CREATE_FORM_NAME),
-    reduxForm<ProjectCreateFormDialogData>({
-        form: PATIENT_CREATE_FORM_NAME,
-        onSubmit: (data, dispatch) => {
-            data.properties = { type: sampleTrackerPatientType };
-            dispatch(createProject(data));
-        }
-    })
-)(DialogPatientCreate);
-
-export interface MenuItemProps {
-    className?: string;
-    studyUuid?: string;
-}
-
-export interface StudyPathId {
-    uuid: string;
-}
-
-export const patientsMapStateToProps = (state: RootState) => {
-    const props: MenuItemProps = {};
-    const studyid = matchPath<StudyPathId>(state.router.location!.pathname, { path: studyRoutePath, exact: true });
-    if (studyid) {
-        props.studyUuid = studyid.params.uuid;
-    }
-    return props;
-};
-
-const openPatientCreateDialog = (studyUuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(initialize(PATIENT_CREATE_FORM_NAME, { ownerUuid: studyUuid }));
-        dispatch(dialogActions.OPEN_DIALOG({ id: PATIENT_CREATE_FORM_NAME, data: {} }));
-    };
-
-export const AddPatientMenuComponent = connect<{}, {}, MenuItemProps>(patientsMapStateToProps)(
-    ({ studyUuid, dispatch, className }: MenuItemProps & DispatchProp<any>) =>
-        <MenuItem className={className} onClick={() => dispatch(openPatientCreateDialog(studyUuid!))} disabled={!studyUuid}>Add Patient</MenuItem >
-);
-
-
 enum PatientPanelColumnNames {
     NAME = "Name"
 }
@@ -126,21 +50,16 @@ export const patientListPanelColumns: DataColumns<string> = [
     }
 ];
 
-export interface TrackerProps {
-    className?: string;
-}
-
-export const PatientListPanel = connect(patientsMapStateToProps)(
-    ({ }: TrackerProps) =>
-        <DataExplorer
-            id={PATIENT_LIST_PANEL_ID}
-            onRowClick={(uuid: string) => { }}
-            onRowDoubleClick={(uuid: string) => { }}
-            onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
-            contextMenuColumn={true}
-            dataTableDefaultView={
-                <DataTableDefaultView />
-            } />);
+export const PatientListPanel = () =>
+    <DataExplorer
+        id={PATIENT_LIST_PANEL_ID}
+        onRowClick={(uuid: string) => { }}
+        onRowDoubleClick={(uuid: string) => { }}
+        onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+        contextMenuColumn={true}
+        dataTableDefaultView={
+            <DataTableDefaultView />
+        } />;
 
 const setItems = (listResults: ListResults<GroupResource>) =>
     patientListPanelActions.SET_ITEMS({
diff --git a/src/plugins/sample-tracker/sampleList.tsx b/src/plugins/sample-tracker/sampleList.tsx
index ec0390ad..a31a0b8d 100644
--- a/src/plugins/sample-tracker/sampleList.tsx
+++ b/src/plugins/sample-tracker/sampleList.tsx
@@ -165,7 +165,7 @@ export const sampleListPanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <span />
+        render: uuid => <MultiCellComponent uuid={uuid.substr(sampleBaseRoutePath.length + 1)} propertyname="sample_tracker:batch_uuid" />
     },
     {
         name: SamplePanelColumnNames.TRACKER_STATE,
diff --git a/src/plugins/sample-tracker/study.tsx b/src/plugins/sample-tracker/study.tsx
index 117a26bf..58cfcd07 100644
--- a/src/plugins/sample-tracker/study.tsx
+++ b/src/plugins/sample-tracker/study.tsx
@@ -3,16 +3,78 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { connect } from 'react-redux';
-import { Dispatch } from "redux";
+import { connect, DispatchProp } from 'react-redux';
+import { compose, Dispatch } from "redux";
 import { propertiesActions } from "~/store/properties/properties-actions";
 import { getProperty } from '~/store/properties/properties';
 import { RootState } from '~/store/store';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { MenuItem } from "@material-ui/core";
+import { createProject } from "~/store/workbench/workbench-actions";
+import { reduxForm, initialize } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { ServiceRepository } from "~/services/services";
+
+import { sampleTrackerStudyType } from './studyList';
 
 import { PATIENT_LIST_PANEL_ID, STUDY_PANEL_CURRENT_UUID, patientListPanelActions } from './patientList';
 
+const STUDY_CREATE_FORM_NAME = "studyCreateFormName";
+
+export interface ProjectCreateFormDialogData {
+    ownerUuid: string;
+    name: string;
+    description: string;
+}
+
+type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
+
+const StudyAddFields = () => <span>
+    <ProjectNameField label="Study name" />
+    <ProjectDescriptionField />
+</span>;
+
+const DialogStudyCreate = (props: DialogProjectProps) =>
+    <FormDialog
+        dialogTitle='New study'
+        formFields={StudyAddFields}
+        submitLabel='Create a Study'
+        {...props}
+    />;
+
+export const CreateStudyDialog = compose(
+    withDialog(STUDY_CREATE_FORM_NAME),
+    reduxForm<ProjectCreateFormDialogData>({
+        form: STUDY_CREATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            data.properties = { type: sampleTrackerStudyType };
+            dispatch(createProject(data));
+        }
+    })
+)(DialogStudyCreate);
+
+const openStudyCreateDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(initialize(STUDY_CREATE_FORM_NAME, {}));
+        dispatch(dialogActions.OPEN_DIALOG({ id: STUDY_CREATE_FORM_NAME, data: {} }));
+    };
+
+interface TrackerProps {
+    className?: string;
+}
+
+export const AddStudyMenuComponent = connect()(
+    ({ dispatch, className }: TrackerProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openStudyCreateDialog())}>Add Study</MenuItem >
+);
+
 export const openStudyPanel = (projectUuid: string) =>
     (dispatch: Dispatch) => {
         dispatch(propertiesActions.SET_PROPERTY({ key: STUDY_PANEL_CURRENT_UUID, value: projectUuid }));
diff --git a/src/plugins/sample-tracker/studyList.tsx b/src/plugins/sample-tracker/studyList.tsx
index 4180a9ee..543a8b62 100644
--- a/src/plugins/sample-tracker/studyList.tsx
+++ b/src/plugins/sample-tracker/studyList.tsx
@@ -3,20 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { InjectedFormProps } from 'redux-form';
-import { WithDialogProps } from '~/store/dialog/with-dialog';
-import { ProjectCreateFormDialogData } from '~/store/projects/project-create-actions';
-import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
-import { dialogActions } from "~/store/dialog/dialog-actions";
 import { ServiceRepository } from "~/services/services";
-import { compose, MiddlewareAPI, Dispatch } from "redux";
-import { reduxForm, initialize } from 'redux-form';
-import { withDialog } from "~/store/dialog/with-dialog";
+import { MiddlewareAPI, Dispatch } from "redux";
 import { RootState } from '~/store/store';
-import { DispatchProp, connect } from 'react-redux';
-import { MenuItem } from "@material-ui/core";
-import { createProject } from "~/store/workbench/workbench-actions";
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { DataColumns } from '~/components/data-table/data-table';
@@ -36,62 +25,12 @@ import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 import { updateResources } from "~/store/resources/resources-actions";
 import { ResourceName } from '~/views-components/data-explorer/renderers';
 
-const STUDY_CREATE_FORM_NAME = "studyCreateFormName";
 export const STUDY_LIST_PANEL_ID = "studyPanel";
 export const studyListPanelActions = bindDataExplorerActions(STUDY_LIST_PANEL_ID);
 export const sampleTrackerStudyType = "sample_tracker:study";
 export const studyListRoutePath = "/sampleTracker/Studies";
 export const studyRoutePath = studyListRoutePath + "/:uuid";
 
-export interface ProjectCreateFormDialogData {
-    ownerUuid: string;
-    name: string;
-    description: string;
-}
-
-type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
-
-const StudyAddFields = () => <span>
-    <ProjectNameField label="Study name" />
-    <ProjectDescriptionField />
-</span>;
-
-const DialogStudyCreate = (props: DialogProjectProps) =>
-    <FormDialog
-        dialogTitle='New study'
-        formFields={StudyAddFields}
-        submitLabel='Create a Study'
-        {...props}
-    />;
-
-export const CreateStudyDialog = compose(
-    withDialog(STUDY_CREATE_FORM_NAME),
-    reduxForm<ProjectCreateFormDialogData>({
-        form: STUDY_CREATE_FORM_NAME,
-        onSubmit: (data, dispatch) => {
-            data.properties = { type: sampleTrackerStudyType };
-            dispatch(createProject(data));
-        }
-    })
-)(DialogStudyCreate);
-
-
-interface TrackerProps {
-    className?: string;
-}
-
-const studiesMapStateToProps = (state: RootState) => ({});
-
-const openStudyCreateDialog = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(initialize(STUDY_CREATE_FORM_NAME, {}));
-        dispatch(dialogActions.OPEN_DIALOG({ id: STUDY_CREATE_FORM_NAME, data: {} }));
-    };
-
-export const AddStudyMenuComponent = connect(studiesMapStateToProps)(
-    ({ dispatch, className }: TrackerProps & DispatchProp<any>) =>
-        <MenuItem className={className} onClick={() => dispatch(openStudyCreateDialog())}>Add Study</MenuItem >
-);
 
 enum StudyPanelColumnNames {
     NAME = "Name"
@@ -113,17 +52,16 @@ export const openStudyListPanel = (dispatch: Dispatch) => {
     dispatch(studyListPanelActions.REQUEST_ITEMS());
 };
 
-export const StudyListMainPanel = connect(studiesMapStateToProps)(
-    ({ }: TrackerProps) =>
-        <DataExplorer
-            id={STUDY_LIST_PANEL_ID}
-            onRowClick={(uuid: string) => { }}
-            onRowDoubleClick={(uuid: string) => { }}
-            onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
-            contextMenuColumn={true}
-            dataTableDefaultView={
-                <DataTableDefaultView />
-            } />);
+export const StudyListMainPanel = () =>
+    <DataExplorer
+        id={STUDY_LIST_PANEL_ID}
+        onRowClick={(uuid: string) => { }}
+        onRowDoubleClick={(uuid: string) => { }}
+        onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
+        contextMenuColumn={true}
+        dataTableDefaultView={
+            <DataTableDefaultView />
+        } />;
 
 
 const setItems = (listResults: ListResults<GroupResource>) =>

commit 49d9698047be5174952d1d6a7f38b6ec1cbfba06
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Mar 18 10:40:56 2021 -0400

    Displays extractions
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/plugins/sample-tracker/extraction.tsx b/src/plugins/sample-tracker/extraction.tsx
index af99f095..4dc38d9c 100644
--- a/src/plugins/sample-tracker/extraction.tsx
+++ b/src/plugins/sample-tracker/extraction.tsx
@@ -13,20 +13,13 @@ import { RootState } from '~/store/store';
 import { TextField } from "~/components/text-field/text-field";
 import { getResource } from "~/store/resources/resources";
 import { FormControl, InputLabel } from '@material-ui/core';
-import {
-    patientBaseRoutePath, patientRoutePath
-} from './patientList';
-import {
-    sampleBaseRoutePath
-} from './sampleList';
-import { matchPath } from "react-router";
 import { MenuItem, Select } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { DispatchProp, connect } from 'react-redux';
 import { withStyles, WithStyles } from '@material-ui/core/styles';
 import { LinkResource } from "~/models/link";
-import { GroupResource } from "~/models/group";
+import { GroupClass, GroupResource } from "~/models/group";
 import { withDialog } from "~/store/dialog/with-dialog";
+import { sampleTrackerExtractionType } from "./sampleList";
 
 const EXTRACTION_CREATE_FORM_NAME = "extractionCreateFormName";
 
@@ -96,18 +89,22 @@ export const ExtractionTypeSelect = styles(
             </Select>
         </FormControl>);
 
+const mustBeDefined = (value: any) => value === undefined ? "Must be defined" : undefined;
+
 const ExtractionAddFields = () => <span>
 
     <InputLabel>Extraction type</InputLabel>
     <div>
         <Field
             name='extractionType'
-            component={ExtractionTypeSelect} />
+            component={ExtractionTypeSelect}
+            validate={mustBeDefined}
+        />
     </div>
 
     <InputLabel>Additional id</InputLabel>
     <Field
-        name='timePoint'
+        name='additionalId'
         component={TextField}
         type="number" />
 
@@ -129,6 +126,7 @@ const ExtractionAddFields = () => <span>
     <div><Field
         name='state'
         component={SampleStateSelect}
+        validate={mustBeDefined}
     /></div>
 
 </span>;
@@ -143,10 +141,16 @@ const DialogExtractionCreate = (props: DialogExtractionProps) =>
     />;
 
 const makeExtractionId = (data: ExtractionCreateFormDialogData, state: RootState): string => {
-    const rscSamp = getResource<LinkResource>(sampleBaseRoutePath + "/" + data.sampleUuid)(state.resources);
-    const rscPat = getResource<GroupResource>(patientBaseRoutePath + "/" + rscSamp!.ownerUuid)(state.resources);
+    const rscSamp = getResource<LinkResource>(data.sampleUuid)(state.resources);
+    const rscPat = getResource<GroupResource>(rscSamp!.ownerUuid)(state.resources);
     let id = rscPat!.name + "_" + data.extractionType + "_";
 
+    if (rscSamp!.properties["sample_tracker:sample_type"] === "tumor") {
+        id = id + "T";
+    } else {
+        id = id + "N";
+    }
+
     if (rscSamp!.properties["sample_tracker:time_point"] < 10) {
         id = id + "_0" + rscSamp!.properties["sample_tracker:time_point"];
     } else {
@@ -157,29 +161,28 @@ const makeExtractionId = (data: ExtractionCreateFormDialogData, state: RootState
     } else {
         id = id + "_" + data.additionalId;
     }
-
     return id;
 };
 
 const createExtraction = (data: ExtractionCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-
+        const rscSamp = getResource<LinkResource>(data.sampleUuid)(getState().resources);
         dispatch(startSubmit(EXTRACTION_CREATE_FORM_NAME));
-        // const extractionId =
-        makeExtractionId(data, getState());
-        /*await services.linkService.create({
-            ownerUuid: data.patientUuid,
-            name: extractionId,
-            linkClass: extractionTrackerExtractionType,
+        const p = {
+            name: makeExtractionId(data, getState()),
+            ownerUuid: rscSamp!.ownerUuid,
+            groupClass: GroupClass.PROJECT,
             properties: {
-                "sample_tracker:collection_type": data.collectionType,
+                "type": sampleTrackerExtractionType,
                 "sample_tracker:extraction_type": data.extractionType,
-                "sample_tracker:collected_at": data.collectedAt,
-                "sample_tracker:time_point": data.timePoint,
-                "sample_tracker:flow_started_at": data.flowStartedAt,
-                "sample_tracker:flow_completed_at": data.flowCompletedAt,
-            },
-        });*/
+                "sample_tracker:additional_id": data.additionalId,
+                "sample_tracker:state": data.state,
+                "sample_tracker:sample_uuid": data.sampleUuid,
+            }
+        };
+        // const newProject =
+        await services.projectService.create(p);
+
         dispatch(dialogActions.CLOSE_DIALOG({ id: EXTRACTION_CREATE_FORM_NAME }));
         dispatch(reset(EXTRACTION_CREATE_FORM_NAME));
     };
@@ -198,30 +201,13 @@ export const CreateExtractionDialog = compose(
 
 export const openExtractionCreateDialog = (sampleUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(initialize(EXTRACTION_CREATE_FORM_NAME, { sampleUuid }));
-        dispatch(dialogActions.OPEN_DIALOG({ id: EXTRACTION_CREATE_FORM_NAME, data: {} }));
+        dispatch(initialize(EXTRACTION_CREATE_FORM_NAME,
+            {
+                sampleUuid,
+                additionalId: 1,
+                state: AnalysisState.NEW
+            }));
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: EXTRACTION_CREATE_FORM_NAME, data: {}
+        }));
     };
-
-
-export interface MenuItemProps {
-    className?: string;
-    patientUuid?: string;
-}
-
-export interface PatientPathId {
-    uuid: string;
-}
-
-export const extractionsMapStateToProps = (state: RootState) => {
-    const props: MenuItemProps = {};
-    const patientid = matchPath<PatientPathId>(state.router.location!.pathname, { path: patientRoutePath, exact: true });
-    if (patientid) {
-        props.patientUuid = patientid.params.uuid;
-    }
-    return props;
-};
-
-export const AddExtractionMenuComponent = connect<{}, {}, MenuItemProps>(extractionsMapStateToProps)(
-    ({ patientUuid, dispatch, className }: MenuItemProps & DispatchProp<any>) =>
-        <MenuItem className={className} onClick={() => dispatch(openExtractionCreateDialog(patientUuid!))} disabled={!patientUuid}>Add Extraction</MenuItem >
-);
diff --git a/src/plugins/sample-tracker/index.tsx b/src/plugins/sample-tracker/index.tsx
index faadae13..001889dd 100644
--- a/src/plugins/sample-tracker/index.tsx
+++ b/src/plugins/sample-tracker/index.tsx
@@ -17,6 +17,7 @@ import { Location } from 'history';
 import { handleFirstTimeLoad } from '~/store/workbench/workbench-actions';
 import { dataExplorerMiddleware } from "~/store/data-explorer/data-explorer-middleware";
 import { getResource } from "~/store/resources/resources";
+import { loadResource } from "~/store/resources/resources-actions";
 import { GroupResource } from "~/models/group";
 import { addMenuActionSet } from '~/views-components/context-menu/context-menu';
 
@@ -111,14 +112,14 @@ export const register = (pluginConfig: PluginConfig) => {
         const patientid = matchPath<StudyPathId>(pathname, { path: patientRoutePath, exact: true });
         if (patientid) {
             store.dispatch(handleFirstTimeLoad(
-                (dispatch: Dispatch) => {
+                async (dispatch: Dispatch) => {
                     dispatch(sampleListPanelActions.SET_COLUMNS({ columns: sampleListPanelColumns }));
                     dispatch<any>(openPatientPanel(patientid.params.uuid));
                     // dispatch<any>(activateSidePanelTreeItem(categoryName));
-                    const patientrsc = getResource<GroupResource>(pathname)(store.getState().resources);
+                    const patientrsc = await dispatch<any>(loadResource(patientid.params.uuid));
                     if (patientrsc) {
                         const studyid = studyListRoutePath + "/" + patientrsc.ownerUuid;
-                        const studyrsc = getResource<GroupResource>(studyid)(store.getState().resources);
+                        const studyrsc = await dispatch<any>(loadResource(patientrsc.ownerUuid));
                         if (studyrsc) {
                             dispatch<any>(setBreadcrumbs([{ label: categoryName, uuid: categoryName },
                             { label: studyrsc.name, uuid: studyid },
diff --git a/src/plugins/sample-tracker/patient.tsx b/src/plugins/sample-tracker/patient.tsx
index 2384cbac..c92e7188 100644
--- a/src/plugins/sample-tracker/patient.tsx
+++ b/src/plugins/sample-tracker/patient.tsx
@@ -16,7 +16,7 @@ import { ContextMenuActionSet } from "~/views-components/context-menu/context-me
 import { openExtractionCreateDialog } from "./extraction";
 
 import { PATIENT_PANEL_CURRENT_UUID } from './patientList';
-import { SAMPLE_LIST_PANEL_ID, sampleListPanelActions } from './sampleList';
+import { SAMPLE_LIST_PANEL_ID, sampleListPanelActions, sampleBaseRoutePath } from './sampleList';
 
 export const PATIENT_SAMPLE_MENU = "Sample Tracker - Patient Sample menu";
 
@@ -54,7 +54,7 @@ export const patientSampleActionSet: ContextMenuActionSet = [[
     {
         name: "Add extraction",
         execute: (dispatch, resource) => {
-            dispatch<any>(openExtractionCreateDialog(resource.uuid));
+            dispatch<any>(openExtractionCreateDialog(resource.uuid.substr(sampleBaseRoutePath.length + 1)));
         }
     },
 ]];
diff --git a/src/plugins/sample-tracker/patientList.tsx b/src/plugins/sample-tracker/patientList.tsx
index 51e3c38e..86cbd369 100644
--- a/src/plugins/sample-tracker/patientList.tsx
+++ b/src/plugins/sample-tracker/patientList.tsx
@@ -187,6 +187,7 @@ export class PatientListPanelMiddlewareService extends DataExplorerMiddlewareSer
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const response = await this.services.groupsService.list(getParams(dataExplorer, studyUuid));
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(updateResources(response.items));
             for (const i of response.items) {
                 i.uuid = patientBaseRoutePath + "/" + i.uuid;
             }
diff --git a/src/plugins/sample-tracker/sample.tsx b/src/plugins/sample-tracker/sample.tsx
index 51b91ac1..e8e2309d 100644
--- a/src/plugins/sample-tracker/sample.tsx
+++ b/src/plugins/sample-tracker/sample.tsx
@@ -20,7 +20,7 @@ import { matchPath } from "react-router";
 import { MenuItem, Select } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { getResource } from "~/store/resources/resources";
-import { sampleTrackerSampleType } from "./sampleList";
+import { sampleTrackerSampleType, sampleListPanelActions } from "./sampleList";
 import { DispatchProp, connect } from 'react-redux';
 import { withStyles, WithStyles } from '@material-ui/core/styles';
 import { GroupResource } from "~/models/group";
@@ -85,32 +85,42 @@ export const SampleTypeSelect = styles(
             </Select>
         </FormControl>);
 
+const mustBeDefined = (value: any) => value === undefined ? "Must be defined" : undefined;
+
 const SampleAddFields = () => <span>
 
     <InputLabel>Patient time point</InputLabel>
     <Field
         name='timePoint'
         component={TextField}
-        type="number" />
+        type="number"
+        validate={mustBeDefined}
+    />
 
     <InputLabel>Collection date</InputLabel>
     <Field
         name='collectedAt'
         component={TextField}
-        type="date" />
+        type="date"
+        validate={mustBeDefined}
+    />
 
     <InputLabel>Collection type</InputLabel>
     <div>
         <Field
             name='collectionType'
-            component={CollectionTypeSelect} />
+            component={CollectionTypeSelect}
+            validate={mustBeDefined}
+        />
     </div>
 
     <InputLabel>Sample type</InputLabel>
     <div>
         <Field
             name='sampleType'
-            component={SampleTypeSelect} />
+            component={SampleTypeSelect}
+            validate={mustBeDefined}
+        />
     </div>
     <InputLabel>Flow started at</InputLabel>
     <Field
@@ -172,6 +182,7 @@ const createSample = (data: SampleCreateFormDialogData) =>
         });
         dispatch(dialogActions.CLOSE_DIALOG({ id: SAMPLE_CREATE_FORM_NAME }));
         dispatch(reset(SAMPLE_CREATE_FORM_NAME));
+        dispatch(sampleListPanelActions.REQUEST_ITEMS());
     };
 
 export const CreateSampleDialog = compose(
diff --git a/src/plugins/sample-tracker/sampleList.tsx b/src/plugins/sample-tracker/sampleList.tsx
index 4c94e6b8..ec0390ad 100644
--- a/src/plugins/sample-tracker/sampleList.tsx
+++ b/src/plugins/sample-tracker/sampleList.tsx
@@ -7,11 +7,9 @@ import { ServiceRepository } from "~/services/services";
 import { MiddlewareAPI, Dispatch } from "redux";
 import { RootState } from '~/store/store';
 import { DispatchProp, connect } from 'react-redux';
-import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
-import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { DataColumns } from '~/components/data-table/data-table';
 import { createTree } from '~/models/tree';
-import { ResourceName } from '~/views-components/data-explorer/renderers';
+// import { ResourceName } from '~/views-components/data-explorer/renderers';
 import { SortDirection } from '~/components/data-table/data-column';
 import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action";
 
@@ -21,6 +19,7 @@ import {
     dataExplorerToListParams
 } from '~/store/data-explorer/data-explorer-middleware-service';
 import { LinkResource } from "~/models/link";
+import { GroupResource } from "~/models/group";
 import { ListResults } from '~/services/common-service/common-service';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { DataExplorer as DataExplorerState, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
@@ -28,6 +27,7 @@ import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from '~/store/properties/properties';
 import { getResource } from "~/store/resources/resources";
+import { propertiesActions } from "~/store/properties/properties-actions";
 
 export const SAMPLE_LIST_PANEL_ID = "sampleListPanel";
 export const sampleListPanelActions = bindDataExplorerActions(SAMPLE_LIST_PANEL_ID);
@@ -38,6 +38,7 @@ export const SAMPLE_PANEL_CURRENT_UUID = "SamplePanelCurrentUUID";
 export const sampleBaseRoutePath = "/SampleTracker/Sample";
 export const sampleRoutePath = sampleBaseRoutePath + "/:uuid";
 
+const PATIENT_PANEL_SAMPLES = "PATIENT_PANEL_SAMPLES";
 
 enum SamplePanelColumnNames {
     NAME = "Name",
@@ -48,8 +49,6 @@ enum SamplePanelColumnNames {
     FLOW_COMPLETED_AT = "Flow completed",
     COLLECTED_AT = "Collected",
     EXTRACTION_TYPE = "Extraction",
-    SEQUENCING_SENT = "Sent for sequencing",
-    SEQUENCING_COMPLETE = "Sequencing completed",
     TRACKER_STATE = "State",
     BATCH_ID = "Batch",
 }
@@ -78,22 +77,33 @@ export const TimestampComponent = connect(
         return { resource, propertyname: props.propertyname };
     })((props: { resource: LinkResource, propertyname: string } & DispatchProp<any>) => <span>{props.resource.properties[props.propertyname]}</span>);
 
-export const TextComponent = connect(
+export const MultiCellComponent = connect(
     (state: RootState, props: { uuid: string, propertyname: string }) => {
-        const resource = getResource<LinkResource>(props.uuid)(state.resources);
-        return { resource, propertyname: props.propertyname };
-    })((props: { resource: LinkResource, propertyname: string } & DispatchProp<any>) => <span>{props.resource.properties[props.propertyname]}</span>);
+        const reverse = getProperty<{ [key: string]: any[] }>(PATIENT_PANEL_SAMPLES)(state.properties);
+        let items = (reverse && reverse[props.uuid]) || [];
+        items = items.map(item => {
+            const rsc = getResource<GroupResource>(item)(state.resources);
+            return rsc || { uuid: "", properties: {} };
+        });
+        return { items, propertyname: props.propertyname };
+    })((props: { items: any[], propertyname: string } & DispatchProp<any>) => <>
+        {props.items.map(item =>
+            <div key={item.uuid} > {item.properties[props.propertyname]}</div>
+        )}
+    </>
+    );
+
 
 export const sampleListPanelColumns: DataColumns<string> = [
-    {
+    /*{
         name: SamplePanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
         render: uuid => <ResourceName uuid={uuid} />
-    },
-    /*{
+    },*/
+    {
         name: SamplePanelColumnNames.TIME_POINT,
         selected: true,
         configurable: true,
@@ -102,28 +112,28 @@ export const sampleListPanelColumns: DataColumns<string> = [
         render: uuid => <TimePointComponent uuid={uuid} />
     },
     {
-        name: SamplePanelColumnNames.COLLECTION_TYPE,
+        name: SamplePanelColumnNames.COLLECTED_AT,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <CollectionTypeComponent uuid={uuid} />
-    },*/
+        render: uuid => <TimestampComponent uuid={uuid} propertyname="sample_tracker:collected_at" />
+    },
     {
-        name: SamplePanelColumnNames.SAMPLE_TYPE,
+        name: SamplePanelColumnNames.COLLECTION_TYPE,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <SampleTypeComponent uuid={uuid} />
+        render: uuid => <CollectionTypeComponent uuid={uuid} />
     },
     {
-        name: SamplePanelColumnNames.COLLECTED_AT,
+        name: SamplePanelColumnNames.SAMPLE_TYPE,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <TimestampComponent uuid={uuid} propertyname="sample_tracker:collected_at" />
+        render: uuid => <SampleTypeComponent uuid={uuid} />
     },
     {
         name: SamplePanelColumnNames.FLOW_STARTED_AT,
@@ -147,18 +157,10 @@ export const sampleListPanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <span />
+        render: uuid => <MultiCellComponent uuid={uuid.substr(sampleBaseRoutePath.length + 1)} propertyname="sample_tracker:extraction_type" />
     },
     {
-        name: SamplePanelColumnNames.SEQUENCING_SENT,
-        selected: true,
-        configurable: true,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: uuid => <span />
-    },
-    {
-        name: SamplePanelColumnNames.SEQUENCING_COMPLETE,
+        name: SamplePanelColumnNames.BATCH_ID,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
@@ -171,40 +173,17 @@ export const sampleListPanelColumns: DataColumns<string> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <span />
-    },
-    {
-        name: SamplePanelColumnNames.BATCH_ID,
-        selected: true,
-        configurable: true,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: uuid => <span />
+        render: uuid => <MultiCellComponent uuid={uuid.substr(sampleBaseRoutePath.length + 1)} propertyname="sample_tracker:state" />
     }
 ];
 
-export interface TrackerProps {
-    className?: string;
-}
-
-export const SampleListPanel = ({ }: TrackerProps) =>
-    <DataExplorer
-        id={SAMPLE_LIST_PANEL_ID}
-        onRowClick={(uuid: string) => { }}
-        onRowDoubleClick={(uuid: string) => { }}
-        onContextMenu={(event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { }}
-        contextMenuColumn={true}
-        dataTableDefaultView={
-            <DataTableDefaultView />
-        } />;
-
 const setItems = (listResults: ListResults<LinkResource>) =>
     sampleListPanelActions.SET_ITEMS({
         ...listResultsToDataExplorerItemsMeta(listResults),
         items: listResults.items.map(resource => resource.uuid),
     });
 
-const getFilters = (dataExplorer: DataExplorerState, patientUuid: string) => {
+const getSampleFilters = (dataExplorer: DataExplorerState, patientUuid: string) => {
     const fb = new FilterBuilder();
     fb.addEqual("owner_uuid", patientUuid);
     fb.addEqual("link_class", sampleTrackerSampleType);
@@ -219,9 +198,30 @@ const getFilters = (dataExplorer: DataExplorerState, patientUuid: string) => {
     );
 };
 
-const getParams = (dataExplorer: DataExplorerState, patientUuid: string) => ({
+const getSampleParams = (dataExplorer: DataExplorerState, patientUuid: string) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    filters: getSampleFilters(dataExplorer, patientUuid),
+});
+
+
+const getExtractionFilters = (dataExplorer: DataExplorerState, patientUuid: string) => {
+    const fb = new FilterBuilder();
+    fb.addEqual("owner_uuid", patientUuid);
+    fb.addEqual("properties.type", sampleTrackerExtractionType);
+
+    const nameFilters = new FilterBuilder()
+        .addILike("name", dataExplorer.searchValue)
+        .getFilters();
+
+    return joinFilters(
+        fb.getFilters(),
+        nameFilters,
+    );
+};
+
+const getExtractionParams = (dataExplorer: DataExplorerState, patientUuid: string) => ({
     ...dataExplorerToListParams(dataExplorer),
-    filters: getFilters(dataExplorer, patientUuid),
+    filters: getExtractionFilters(dataExplorer, patientUuid),
 });
 
 export class SampleListPanelMiddlewareService extends DataExplorerMiddlewareService {
@@ -241,12 +241,26 @@ export class SampleListPanelMiddlewareService extends DataExplorerMiddlewareServ
 
         try {
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-            const response = await this.services.linkService.list(getParams(dataExplorer, patientUuid));
+            const response = await this.services.linkService.list(getSampleParams(dataExplorer, patientUuid));
+            const response2 = await this.services.groupsService.list(getExtractionParams(dataExplorer, patientUuid));
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+
+            const reverse = {};
+            for (const i of response2.items) {
+                const sid = i.properties["sample_tracker:sample_uuid"];
+                const lst = reverse[sid] || [];
+                lst.push(i.uuid);
+                reverse[sid] = lst;
+            }
+            api.dispatch(propertiesActions.SET_PROPERTY({ key: PATIENT_PANEL_SAMPLES, value: reverse }));
+
+            api.dispatch(updateResources(response.items));
+
             for (const i of response.items) {
                 i.uuid = sampleBaseRoutePath + "/" + i.uuid;
             }
             api.dispatch(updateResources(response.items));
+            api.dispatch(updateResources(response2.items));
             api.dispatch(setItems(response));
         } catch (e) {
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list