Skip to content

Commit

Permalink
improve select and select multi
Browse files Browse the repository at this point in the history
  • Loading branch information
nicokant committed Jul 2, 2024
1 parent 03d4041 commit 6e341a1
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 47 deletions.
23 changes: 23 additions & 0 deletions src/frontend/src/samples/components/Cell/ActionsCell.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex">
<button
disabled={running}
className="btn bg-red-500 text-white"
onClick={runDelete}
>
<i className={`fas ${running ? "fa-spin fa-spinner" : "fa-trash"}`}></i>
</button>
</div>
);
}
51 changes: 51 additions & 0 deletions src/frontend/src/samples/components/Cell/MultiSelectCell.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<AsyncSelect
isMulti
defaultOptions
loadOptions={load}
getOptionLabel={(o) => o[labelField]}
getOptionValue={(o) => o[idField]}
onBlur={handleBlur}
classNamePrefix="react-select"
className=""
value={value}
onChange={setValue}
menuPortalTarget={datePortal}
/>
);
}
47 changes: 47 additions & 0 deletions src/frontend/src/samples/components/Cell/SelectCell.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<AsyncSelect
defaultOptions
loadOptions={load}
getOptionLabel={(o) => o.name}
getOptionValue={(o) => o.id}
onBlur={handleBlur}
classNamePrefix="react-select"
className=""
value={value}
onChange={setValue}
menuPortalTarget={datePortal}
/>
);
}
25 changes: 20 additions & 5 deletions src/frontend/src/samples/components/SampleForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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,
});
},
Expand Down Expand Up @@ -52,11 +62,16 @@ export default function SampleForm() {
{({ state, handleChange, handleBlur }) => (
<HUIField>
<Label className="block">Species</Label>
<input
className="mt-1 block"
value={state.value || ""}
onChange={(e) => handleChange(e.target.value)}
<AsyncSelect
defaultOptions
loadOptions={speciesOptions}
getOptionLabel={(o) => o.name}
getOptionValue={(o) => o.id}
onBlur={handleBlur}
classNamePrefix="react-select"
className=""
value={state.value}
onChange={handleChange}
/>
</HUIField>
)}
Expand Down
61 changes: 24 additions & 37 deletions src/frontend/src/samples/components/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
keepPreviousData,
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { config, client } from "../config";
Expand All @@ -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}`;
Expand All @@ -27,17 +28,26 @@ 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",
cell: SimpleCellInput,
}),
columnHelper.accessor("species", {
header: "Species",
cell: (props) => {
const v = props.getValue();
return v ? v.name : null;
},
cell: (props) => <SelectCell {...props} loadOptions={speciesOptions} queryKey={'species'} />,
}),
columnHelper.accessor("date", {
header: "Date",
Expand All @@ -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) => <SelectCell {...props} loadOptions={sampleTypesOptions} queryKey={'sampleTypes'} />,
}),
columnHelper.accessor("markers", {
header: "Markers",
cell: (props) => {
const v = props.getValue();
return v ? v.join(", ") : null;
},
cell: (props) => <MultiSelectCell {...props} loadOptions={markersOptions} queryKey={'markers'} idField="name" />,
}),
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 (
<div className="flex">
<button
disabled={running}
className="btn bg-red-500 text-white"
onClick={runDelete}
>
<i className={`fas ${running ? 'fa-spin fa-spinner' : 'fa-trash'}`}></i>
</button>
</div>
);
},
cell: ActionsCell,
}),
];

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/samples/index.css
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
48 changes: 45 additions & 3 deletions src/genlab_bestilling/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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=[
Expand Down
6 changes: 6 additions & 0 deletions src/genlab_bestilling/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
OperationStatusSerializer,
SampleBulkSerializer,
SampleSerializer,
SampleUpdateSerializer,
)


Expand All @@ -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}
)
Expand Down
1 change: 0 additions & 1 deletion src/genlab_bestilling/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down

0 comments on commit 6e341a1

Please sign in to comment.