From 6e341a1e05994adfa720c231dd9be41d335916f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Cant=C3=B9?= Date: Tue, 2 Jul 2024 09:39:51 +0200 Subject: [PATCH] improve select and select multi --- .../samples/components/Cell/ActionsCell.jsx | 23 +++++++ .../components/Cell/MultiSelectCell.jsx | 51 ++++++++++++++++ .../samples/components/Cell/SelectCell.jsx | 47 ++++++++++++++ .../src/samples/components/SampleForm.jsx | 25 ++++++-- src/frontend/src/samples/components/Table.jsx | 61 ++++++++----------- src/frontend/src/samples/index.css | 2 +- src/genlab_bestilling/api/serializers.py | 48 ++++++++++++++- src/genlab_bestilling/api/views.py | 6 ++ src/genlab_bestilling/filters.py | 1 - 9 files changed, 217 insertions(+), 47 deletions(-) create mode 100644 src/frontend/src/samples/components/Cell/ActionsCell.jsx create mode 100644 src/frontend/src/samples/components/Cell/MultiSelectCell.jsx create mode 100644 src/frontend/src/samples/components/Cell/SelectCell.jsx 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)