Skip to content

Commit

Permalink
allow to save date
Browse files Browse the repository at this point in the history
  • Loading branch information
nicokant committed Jul 1, 2024
1 parent bda75b4 commit 03d4041
Show file tree
Hide file tree
Showing 11 changed files with 604 additions and 142 deletions.
439 changes: 385 additions & 54 deletions src/frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"axios": "^1.7.2",
"django-vite-plugin": "^4.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-datepicker": "^7.2.0",
"react-dom": "^18.3.1",
"react-select": "^5.8.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
Expand Down
34 changes: 34 additions & 0 deletions src/frontend/src/samples/components/Cell/DateCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useMemo, useState } from "react";
import DatePicker from "react-datepicker";
import { createPortal } from "react-dom";

const datePortal = document.getElementById('date-portal');

export default function DateCell({ getValue, row: { original }, column: { id }, table }) {
const initialValue = getValue() || "";
// We need to keep and update the state of the cell normally
const initialDate = useMemo(() => new Date(initialValue), [initialValue]);
const [value, setValue] = useState(initialDate);

// When the input is blurred, we'll call our table meta's updateData function
const onBlur = () => {
if (value !== initialValue) {
table.options.meta?.updateData({ id: original.id, [id]: value.toLocaleDateString("en-US") });
}
};

// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue || "");
}, [initialValue]);

return (
<DatePicker
selected={value}
onChange={(e) => setValue(e)}
onBlur={onBlur}
dateFormat="dd/MM/YYYY"
popperContainer={({children}) => createPortal(children, datePortal)}
/>
);
}
29 changes: 29 additions & 0 deletions src/frontend/src/samples/components/Cell/SimpleCellInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";


export default function SimpleCellInput({ getValue, row: { original }, column: { id }, table }) {
const initialValue = getValue() || "";
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);

// When the input is blurred, we'll call our table meta's updateData function
const onBlur = () => {
if (value !== initialValue) {
table.options.meta?.updateData({ id: original.id, [id]: value });
}
};

// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue || "");
}, [initialValue]);

return (
<input
className="border-0 w-full"
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
/>
);
}
107 changes: 60 additions & 47 deletions src/frontend/src/samples/components/SampleForm.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import { useForm } from "@tanstack/react-form";
import {
Field as HUIField,
Input,
Button,
Label,
} from "@headlessui/react";
import { Field as HUIField, Input, Button, Label } from "@headlessui/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { client, config } from '../config';
import { client, config } from "../config";
import DatePicker from 'react-datepicker';

import "react-datepicker/dist/react-datepicker.css";

export default function SampleForm() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();

const bulkCreate = useMutation({
mutationFn: (value) => {
return client.post('/api/samples/bulk/', { ...value, order: config.order })
return client.post("/api/samples/bulk/", {
...value,
date: value.date.toLocaleDateString("en-US"),
order: config.order,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['samples']})
}
})
queryClient.invalidateQueries({ queryKey: ["samples"] });
},
});

const { handleSubmit, Field } = useForm({
onSubmit: async({ value, formApi }) => {
const { handleSubmit, Field, Subscribe } = useForm({
onSubmit: async ({ value, formApi }) => {
try {
await bulkCreate.mutateAsync(value)
formApi.reset()
} catch(e) {
await bulkCreate.mutateAsync(value);
formApi.reset();
} catch (e) {
console.log(e);
}
},
Expand All @@ -34,7 +36,7 @@ export default function SampleForm() {
species: null,
pop_id: null,
date: null,
}
},
});

return (
Expand All @@ -44,14 +46,15 @@ export default function SampleForm() {
handleSubmit();
}}
className="flex gap-4 mb-5 items-end"
id="add-rows"
>
<Field name="species">
{({ state, handleChange, handleBlur }) => (
<HUIField>
<Label className="block">Species</Label>
<input
className="mt-1 block"
value={state.value || ''}
value={state.value || ""}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
Expand All @@ -61,46 +64,56 @@ export default function SampleForm() {
<Field name="pop_id">
{({ state, handleChange, handleBlur }) => (
<HUIField>
<Label className="block">Pop ID</Label>
<input
className="mt-1 block"
value={state.value || ''}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
</HUIField>
<Label className="block">Pop ID</Label>
<input
className="mt-1 block"
value={state.value || ""}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
</HUIField>
)}
</Field>
<Field name="date">
{({ state, handleChange, handleBlur }) => (
<HUIField>
<Label className="block">Date</Label>
<input
className="mt-1 block"
value={state.value || ''}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
</HUIField>
<Label className="block">Date</Label>
<DatePicker
showIcon
className="mt-1 block"
selected={state.value}
onChange={(e) => handleChange(e)}
onBlur={handleBlur}
dateFormat="dd/MM/YYYY"
/>
</HUIField>
)}
</Field>
<Field name="quantity">
{({ state, handleChange, handleBlur }) => (
<HUIField>
<Label className="block">Quantity</Label>
<input
type="number"
className="mt-1 block"
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
</HUIField>
<Label className="block">Quantity</Label>
<input
type="number"
className="mt-1 block"
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>
</HUIField>
)}
</Field>
<Button type="submit" className="btn bg-primary">
Add
</Button>
<Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
{([canSubmit, isSubmitting]) => (
<Button
type="submit"
className="btn bg-primary"
disabled={!canSubmit}
>
{isSubmitting ? <i className="fas fa-spin fa-spinner"></i> : "Add"}
</Button>
)}
</Subscribe>
</form>
);
}
64 changes: 24 additions & 40 deletions src/frontend/src/samples/components/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { Button, Input } from "@headlessui/react";

import { useVirtualizer } from "@tanstack/react-virtual";
import SimpleCellInput from "./Cell/SimpleCellInput";
import DateCell from "./Cell/DateCell";

async function getSamples({ pageParam }) {
const url = pageParam || `/api/samples/?order=${config.order}`;
Expand All @@ -28,6 +30,7 @@ const columnHelper = createColumnHelper();
const COLUMNS = [
columnHelper.accessor("guid", {
header: "GUID",
cell: SimpleCellInput,
}),
columnHelper.accessor("species", {
header: "Species",
Expand All @@ -38,12 +41,15 @@ const COLUMNS = [
}),
columnHelper.accessor("date", {
header: "Date",
cell: DateCell,
}),
columnHelper.accessor("pop_id", {
header: "Pop ID",
cell: SimpleCellInput,
}),
columnHelper.accessor("notes", {
header: "Notes",
cell: SimpleCellInput,
}),
columnHelper.accessor("type", {
header: "Type",
Expand All @@ -58,51 +64,30 @@ const COLUMNS = [
}),
columnHelper.display({
header: "Actions",
cell: ({ table, row: { original }}) => {
cell: ({ table, row: { original } }) => {
const [running, setRunning] = useState(false);

const runDelete = useCallback(() =>
table.options.meta?.deleteRow({ id: original.id }))
const runDelete = useCallback(async () => {
setRunning(true);
await table.options.meta?.deleteRow({ id: original.id });
setRunning(false);
}, []);

return (
<div className="flex">
<button className="btn bg-red-500 text-white" onClick={runDelete}><i className="fas fa-trash"></i></button>
<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>
);
},
}),
];

const fallbackData = [];

const defaultColumn = {
cell: ({ getValue, row: { original }, column: { id }, table }) => {
const initialValue = getValue() || "";
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);

// When the input is blurred, we'll call our table meta's updateData function
const onBlur = () => {
if (value !== initialValue) {
table.options.meta?.updateData({ id: original.id, [id]: value });
}
};

// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue || "");
}, [initialValue]);

return (
<Input
className="border-0 w-full"
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
/>
);
},
};

export default function Table() {
const tableContainerRef = useRef(null);
const queryClient = useQueryClient();
Expand Down Expand Up @@ -165,14 +150,13 @@ export default function Table() {
return client.delete(`/api/samples/${id}/`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['samples']})
queryClient.invalidateQueries({ queryKey: ["samples"] });
},
});

const table = useReactTable({
data: flatData ?? fallbackData,
data: flatData,
columns: COLUMNS,
defaultColumn,
getCoreRowModel: getCoreRowModel(),
meta: {
updateData: updateCell.mutateAsync,
Expand Down Expand Up @@ -209,7 +193,7 @@ export default function Table() {
}}
>
<table className="grid w-full">
<thead className="grid sticky top-0 bg-white border-b z-[100]">
<thead className="grid sticky top-0 bg-white border-b z-[10]">
{table.getHeaderGroups().map((headerGroup) => (
<tr
className="bg-gray-2 text-left dark:bg-meta-4 flex w-full"
Expand Down Expand Up @@ -280,7 +264,7 @@ export default function Table() {
{(isLoading || isFetching) && (
<p className="font-bold text-center">Loading...</p>
)}
{(updateCell.isPending) && (
{updateCell.isPending && (
<p className="font-bold text-center">Saving...</p>
)}
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/src/samples/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

table .react-datepicker-popper, form .react-datepicker-popper {
z-index: 999;
}

.react-datepicker__input-container {
height: 100%;
}

.react-datepicker__input-container input {
width: 100%;
height: 100%;
border: 0px;
}
1 change: 1 addition & 0 deletions src/frontend/src/samples/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client'

import 'vite/modulepreload-polyfill';
import './index.css';

import App from './App.jsx'

Expand Down
Loading

0 comments on commit 03d4041

Please sign in to comment.