diff --git a/src/frontend/src/samples/components/Cell/ActionsCell.jsx b/src/frontend/src/samples/components/Cell/ActionsCell.jsx
new file mode 100644
index 0000000..3832cac
--- /dev/null
+++ b/src/frontend/src/samples/components/Cell/ActionsCell.jsx
@@ -0,0 +1,23 @@
+import { useCallback, useState } from "react";
+
+export default function ActionsCell({ table, row: { original } }) {
+ const [running, setRunning] = useState(false);
+
+ const runDelete = useCallback(async () => {
+ setRunning(true);
+ await table.options.meta?.deleteRow({ id: original.id });
+ setRunning(false);
+ }, [original.id, table.options]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/frontend/src/samples/components/Cell/MultiSelectCell.jsx b/src/frontend/src/samples/components/Cell/MultiSelectCell.jsx
new file mode 100644
index 0000000..a6ef11e
--- /dev/null
+++ b/src/frontend/src/samples/components/Cell/MultiSelectCell.jsx
@@ -0,0 +1,51 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useMemo, useState } from "react";
+import AsyncSelect from "react-select/async";
+
+const datePortal = document.getElementById('date-portal');
+
+export default function MultiSelectCell({
+ getValue,
+ row: { original },
+ column: { id },
+ table,
+ loadOptions,
+ queryKey,
+ idField = 'id',
+ labelField = 'name',
+}) {
+ const queryClient = useQueryClient();
+ const initialValue = getValue();
+ const [value, setValue] = useState(initialValue);
+
+ // When the input is blurred, we'll call our table meta's updateData function
+ const handleBlur = () => {
+ if (value !== initialValue) {
+ console.log(value)
+ table.options.meta?.updateData({ id: original.id, [id]: value.map(v => v[idField]) });
+ }
+ };
+
+ // If the initialValue is changed external, sync it up with our state
+ useEffect(() => {
+ setValue(initialValue || "");
+ }, [initialValue]);
+
+ const load = async (input) => await queryClient.fetchQuery({ queryKey: [queryKey, input], queryFn: () => loadOptions(input) });
+
+ return (
+ o[labelField]}
+ getOptionValue={(o) => o[idField]}
+ onBlur={handleBlur}
+ classNamePrefix="react-select"
+ className=""
+ value={value}
+ onChange={setValue}
+ menuPortalTarget={datePortal}
+ />
+ );
+}
diff --git a/src/frontend/src/samples/components/Cell/SelectCell.jsx b/src/frontend/src/samples/components/Cell/SelectCell.jsx
new file mode 100644
index 0000000..6b46b5e
--- /dev/null
+++ b/src/frontend/src/samples/components/Cell/SelectCell.jsx
@@ -0,0 +1,47 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useMemo, useState } from "react";
+import AsyncSelect from "react-select/async";
+
+const datePortal = document.getElementById('date-portal');
+
+export default function SelectCell({
+ getValue,
+ row: { original },
+ column: { id },
+ table,
+ loadOptions,
+ queryKey,
+}) {
+ const queryClient = useQueryClient();
+ const initialValue = getValue();
+ const [value, setValue] = useState(initialValue);
+
+ // When the input is blurred, we'll call our table meta's updateData function
+ const handleBlur = () => {
+ if (value !== initialValue) {
+ table.options.meta?.updateData({ id: original.id, [id]: value.id });
+ }
+ };
+
+ // If the initialValue is changed external, sync it up with our state
+ useEffect(() => {
+ setValue(initialValue || "");
+ }, [initialValue]);
+
+ const load = async (input) => await queryClient.fetchQuery({ queryKey: [queryKey, input], queryFn: () => loadOptions(input) });
+
+ return (
+ o.name}
+ getOptionValue={(o) => o.id}
+ onBlur={handleBlur}
+ classNamePrefix="react-select"
+ className=""
+ value={value}
+ onChange={setValue}
+ menuPortalTarget={datePortal}
+ />
+ );
+}
diff --git a/src/frontend/src/samples/components/SampleForm.jsx b/src/frontend/src/samples/components/SampleForm.jsx
index 53ac45c..8dfc0fa 100644
--- a/src/frontend/src/samples/components/SampleForm.jsx
+++ b/src/frontend/src/samples/components/SampleForm.jsx
@@ -2,10 +2,19 @@ import { useForm } from "@tanstack/react-form";
import { Field as HUIField, Input, Button, Label } from "@headlessui/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { client, config } from "../config";
-import DatePicker from 'react-datepicker';
+import DatePicker from "react-datepicker";
+import AsyncSelect from "react-select/async";
import "react-datepicker/dist/react-datepicker.css";
+const speciesOptions = async (input) => {
+ let base = `/api/species/?order=${config.order}`
+ if (input) {
+ base += `&name__icontains=${input}`
+ }
+ return (await client.get(base)).data;
+};
+
export default function SampleForm() {
const queryClient = useQueryClient();
@@ -14,6 +23,7 @@ export default function SampleForm() {
return client.post("/api/samples/bulk/", {
...value,
date: value.date.toLocaleDateString("en-US"),
+ species: value.species.id,
order: config.order,
});
},
@@ -52,11 +62,16 @@ export default function SampleForm() {
{({ state, handleChange, handleBlur }) => (
- handleChange(e.target.value)}
+ o.name}
+ getOptionValue={(o) => o.id}
onBlur={handleBlur}
+ classNamePrefix="react-select"
+ className=""
+ value={state.value}
+ onChange={handleChange}
/>
)}
diff --git a/src/frontend/src/samples/components/Table.jsx b/src/frontend/src/samples/components/Table.jsx
index e5ca88a..e96704d 100644
--- a/src/frontend/src/samples/components/Table.jsx
+++ b/src/frontend/src/samples/components/Table.jsx
@@ -2,7 +2,6 @@ import {
keepPreviousData,
useInfiniteQuery,
useMutation,
- useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { config, client } from "../config";
@@ -12,12 +11,14 @@ import {
getCoreRowModel,
createColumnHelper,
} from "@tanstack/react-table";
-import { useEffect, useRef, useState, useCallback, useMemo } from "react";
-import { Button, Input } from "@headlessui/react";
+import { useEffect, useRef, useCallback, useMemo } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import SimpleCellInput from "./Cell/SimpleCellInput";
import DateCell from "./Cell/DateCell";
+import SelectCell from "./Cell/SelectCell";
+import ActionsCell from "./Cell/ActionsCell";
+import MultiSelectCell from "./Cell/MultiSelectCell";
async function getSamples({ pageParam }) {
const url = pageParam || `/api/samples/?order=${config.order}`;
@@ -27,6 +28,18 @@ async function getSamples({ pageParam }) {
const columnHelper = createColumnHelper();
+const speciesOptions = async (input) => {
+ return (await client.get(`/api/species/?order=${config.order}&name__icontains=${input}`)).data;
+};
+
+const sampleTypesOptions = async (input) => {
+ return (await client.get(`/api/sample-types/?order=${config.order}&name__icontains=${input}`)).data;
+};
+
+const markersOptions = async (input) => {
+ return (await client.get(`/api/markers/?order=${config.order}&name__icontains=${input}`)).data;
+};
+
const COLUMNS = [
columnHelper.accessor("guid", {
header: "GUID",
@@ -34,10 +47,7 @@ const COLUMNS = [
}),
columnHelper.accessor("species", {
header: "Species",
- cell: (props) => {
- const v = props.getValue();
- return v ? v.name : null;
- },
+ cell: (props) => ,
}),
columnHelper.accessor("date", {
header: "Date",
@@ -47,44 +57,21 @@ const COLUMNS = [
header: "Pop ID",
cell: SimpleCellInput,
}),
- columnHelper.accessor("notes", {
- header: "Notes",
- cell: SimpleCellInput,
- }),
columnHelper.accessor("type", {
header: "Type",
- cell: (props) => (props.getValue() ? props.getValue().name : null),
+ cell: (props) => ,
}),
columnHelper.accessor("markers", {
header: "Markers",
- cell: (props) => {
- const v = props.getValue();
- return v ? v.join(", ") : null;
- },
+ cell: (props) => ,
+ }),
+ columnHelper.accessor("notes", {
+ header: "Notes",
+ cell: SimpleCellInput,
}),
columnHelper.display({
header: "Actions",
- cell: ({ table, row: { original } }) => {
- const [running, setRunning] = useState(false);
-
- const runDelete = useCallback(async () => {
- setRunning(true);
- await table.options.meta?.deleteRow({ id: original.id });
- setRunning(false);
- }, []);
-
- return (
-
-
-
- );
- },
+ cell: ActionsCell,
}),
];
diff --git a/src/frontend/src/samples/index.css b/src/frontend/src/samples/index.css
index 94be6b0..7b86bfe 100644
--- a/src/frontend/src/samples/index.css
+++ b/src/frontend/src/samples/index.css
@@ -1,5 +1,5 @@
-table .react-datepicker-popper, form .react-datepicker-popper {
+table .react-datepicker-popper, form .react-datepicker-popper, form .react-select__menu, table .react-select__menu {
z-index: 999;
}
diff --git a/src/genlab_bestilling/api/serializers.py b/src/genlab_bestilling/api/serializers.py
index 78473c1..927d435 100644
--- a/src/genlab_bestilling/api/serializers.py
+++ b/src/genlab_bestilling/api/serializers.py
@@ -1,6 +1,6 @@
from rest_framework import serializers
-from ..models import Marker, Sample
+from ..models import Marker, Sample, SampleType, Species
class OperationStatusSerializer(serializers.Serializer):
@@ -19,10 +19,52 @@ class Meta:
fields = ("name",)
+class SampleTypeSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SampleType
+ fields = ("id", "name")
+
+
+class SpeciesSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Species
+ fields = ("id", "name")
+
+
class SampleSerializer(serializers.ModelSerializer):
- type = EnumSerializer()
- species = EnumSerializer()
+ type = SampleTypeSerializer()
+ species = SpeciesSerializer()
location = EnumSerializer(allow_null=True, required=False)
+ markers = MarkerSerializer(many=True)
+ date = serializers.DateField(
+ required=False,
+ input_formats=[
+ "iso-8601",
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d",
+ "%Y-%m-%dT%H:%M:%SZ",
+ "%m/%d/%Y",
+ ],
+ )
+
+ class Meta:
+ model = Sample
+ fields = (
+ "id",
+ "order",
+ "guid",
+ "species",
+ "markers",
+ "date",
+ "notes",
+ "pop_id",
+ "location",
+ "volume",
+ "type",
+ )
+
+
+class SampleUpdateSerializer(serializers.ModelSerializer):
date = serializers.DateField(
required=False,
input_formats=[
diff --git a/src/genlab_bestilling/api/views.py b/src/genlab_bestilling/api/views.py
index 9343bfe..abe2f8e 100644
--- a/src/genlab_bestilling/api/views.py
+++ b/src/genlab_bestilling/api/views.py
@@ -13,6 +13,7 @@
OperationStatusSerializer,
SampleBulkSerializer,
SampleSerializer,
+ SampleUpdateSerializer,
)
@@ -27,6 +28,11 @@ class SampleViewset(ModelViewSet):
filterset_class = SampleFilter
pagination_class = IDCursorPagination
+ def get_serializer_class(self):
+ if self.action in ["update", "partial_update"]:
+ return SampleUpdateSerializer
+ return super().get_serializer_class()
+
@extend_schema(
request=SampleBulkSerializer, responses={200: OperationStatusSerializer}
)
diff --git a/src/genlab_bestilling/filters.py b/src/genlab_bestilling/filters.py
index d6d8ef8..609c706 100644
--- a/src/genlab_bestilling/filters.py
+++ b/src/genlab_bestilling/filters.py
@@ -13,7 +13,6 @@ class BaseOrderFilter(filters.FilterSet):
order = filters.NumberFilter(field_name="order", method="filter_order")
def filter_order(self, queryset, name, value):
- print(value, name)
return queryset.filter(orders__id=value)