Skip to content

Commit

Permalink
feat: Basic CRUD for userData structure, setup ListContext
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed May 27, 2024
1 parent 0243169 commit 238211f
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 37 deletions.
76 changes: 76 additions & 0 deletions editor.planx.uk/src/@planx/components/List/Public/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { createContext, ReactNode,useContext, useState } from "react";

import { generateNewItem,Schema, UserData } from "../model";

interface ListContextValue {
schema: Schema;
activeIndex: number | undefined;
userData: UserData;
addNewItem: () => void;
saveItem: (index: number, updatedItem: UserData[0]) => void;
removeItem: (index: number) => void;
editItem: (index: number) => void;
cancelEditItem: () => void;
}

interface ListProviderProps {
children: ReactNode;
schema: Schema;
}

const ListContext = createContext<ListContextValue | undefined>(undefined);

export const ListProvider: React.FC<ListProviderProps> = ({
children,
schema,
}) => {
const [activeIndex, setActiveIndex] = useState<number | undefined>(0);
const [userData, setUserData] = useState<UserData>(
schema.min === 0 ? [] : [generateNewItem(schema)],
);

const addNewItem = () => {
setUserData([...userData, generateNewItem(schema)]);
setActiveIndex((prev) => (prev === undefined ? 0 : prev + 1));
};

const saveItem = (index: number, updatedItem: UserData[0]) => {
setUserData((prev) =>
prev.map((item, i) => (i === index ? updatedItem : item)),
);
};

const editItem = (index: number) => setActiveIndex(index);

const removeItem = (index: number) => {
if (index === activeIndex || index === 0) cancelEditItem();
setUserData((prev) => prev.filter((_, i) => i !== index));
};

const cancelEditItem = () => setActiveIndex(undefined);

return (
<ListContext.Provider
value={{
activeIndex,
userData,
addNewItem,
saveItem,
schema,
editItem,
removeItem,
cancelEditItem,
}}
>
{children}
</ListContext.Provider>
);
};

export const useListContext = (): ListContextValue => {
const context = useContext(ListContext);
if (!context) {
throw new Error("useListContext must be used within a ListProvider");
}
return context;
};
133 changes: 101 additions & 32 deletions editor.planx.uk/src/@planx/components/List/Public/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import { PublicProps } from "@planx/components/ui";
import React from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import InputRow from "ui/shared/InputRow";

import Card from "../../shared/Preview/Card";
import CardHeader from "../../shared/Preview/CardHeader";
import type { Field, List } from "../model";
import { ListProvider, useListContext } from "./Context";
import {
NumberFieldInput,
RadioFieldInput,
Expand All @@ -28,6 +35,11 @@ const ListCard = styled(Box)(({ theme }) => ({
marginBottom: theme.spacing(2),
}));

const CardButton = styled(Button)(({ theme }) => ({
fontWeight: FONT_WEIGHT_SEMI_BOLD,
gap: theme.spacing(2),
}));

/**
* Controller to return correct user input for field in schema
*/
Expand All @@ -47,51 +59,108 @@ const InputField: React.FC<Field & { index: number }> = (props) => {
}
};

function ListComponent({
info,
policyRef,
howMeasured,
schema,
handleSubmit,
title,
description,
}: Props) {
// TODO: Track user state, allow items to be added and removed
// TODO: Track "active" index
// TODO: Validate min / max
// TODO: Validate user input against schema fields, track errors
// TODO: On submit generate a payload
const ActiveListCard: React.FC<{
index: number;
}> = ({ index }) => {
const { schema, saveItem, cancelEditItem } = useListContext();

return (
// TODO: This should be a HTML form
<ListCard>
<Typography component="h2" variant="h3">
{schema.type} {index + 1}
</Typography>
{schema.fields.map((field, i) => (
<InputRow key={i}>
<InputField {...field} index={i} />
</InputRow>
))}
<Box display="flex" gap={2}>
<Button
variant="contained"
color="primary"
onClick={() => saveItem(index, [])}
>
Save
</Button>
<Button onClick={cancelEditItem}>Cancel</Button>
</Box>
</ListCard>
);
};

const InactiveListCard: React.FC<{
index: number;
}> = ({ index }) => {
const { schema, userData, removeItem, editItem } = useListContext();

return (
<ListCard>
<Typography component="h2" variant="h3">
{schema.type} {index + 1}
</Typography>
<Table>
{schema.fields.map((field, i) => (
<TableRow>
<TableCell sx={{ fontWeight: FONT_WEIGHT_SEMI_BOLD }}>
{field.data.title}
</TableCell>
<TableCell>{userData[index][i]?.val}</TableCell>
</TableRow>
))}
</Table>
<Box display="flex" gap={2}>
<CardButton onClick={() => removeItem(index)}>
<DeleteIcon titleAccess="Remove" color="warning" fontSize="medium" />
Remove
</CardButton>
<CardButton onClick={() => editItem(index)}>
{/* TODO: Is primary colour really right here? */}
<EditIcon titleAccess="Edit" color="primary" fontSize="medium" />
Edit
</CardButton>
</Box>
</ListCard>
);
};

const Root = ({ title, description, info, policyRef, howMeasured }: Props) => {
const { userData, activeIndex, schema, addNewItem } = useListContext();

return (
<Card handleSubmit={handleSubmit} isValid>
<Card handleSubmit={() => console.log({ userData })} isValid>
<CardHeader
title={title}
description={description}
info={info}
policyRef={policyRef}
howMeasured={howMeasured}
/>
<ListCard>
<Typography component="h2" variant="h3">
{schema.type} index
</Typography>
{schema.fields.map((field, i) => (
<InputRow key={i}>
<InputField {...field} index={i} />
</InputRow>
))}
<Box display="flex" gap={2}>
<Button variant="contained" color="primary">
Save
</Button>
<Button>Cancel</Button>
</Box>
</ListCard>
<Button variant="contained" color="secondary">
activeIndex = {activeIndex}
{userData.map((_, i) =>
i === activeIndex ? (
<ActiveListCard key={`card-${i}`} index={i} />
) : (
<InactiveListCard key={`card-${i}`} index={i} />
),
)}
<Button variant="contained" color="secondary" onClick={addNewItem}>
+ Add a new {schema.type.toLowerCase()} type
</Button>
</Card>
);
};

function ListComponent(props: Props) {
// TODO: Validate min / max
// TODO: Validate user input against schema fields, track errors
// TODO: On submit generate a payload

return (
<ListProvider schema={props.schema}>
<Root {...props} />
</ListProvider>
);
}

export default ListComponent;
32 changes: 27 additions & 5 deletions editor.planx.uk/src/@planx/components/List/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,24 @@ import { SCHEMAS } from "./Editor";
interface QuestionInput {
title: string;
description?: string;
fn?: string;
options: Option[];
}

export type TextField = { type: "text"; required?: boolean; data: TextInput };
// TODO: Add summary fields for inactive view?
export type TextField = {
type: "text";
required?: boolean;
data: TextInput & { fn: string };
};
export type NumberField = {
type: "number";
required?: boolean;
data: NumberInput;
data: NumberInput & { fn: string };
};
export type QuestionField = {
type: "question";
required?: boolean;
data: QuestionInput;
data: QuestionInput & { fn: string };
};

/**
Expand All @@ -38,7 +42,7 @@ export type Field = TextField | NumberField | QuestionField;
export interface Schema {
type: string;
fields: Field[];
min?: number;
min: number;
max?: number;
}

Expand All @@ -58,3 +62,21 @@ export const parseContent = (data: Record<string, any> | undefined): List => ({
schema: data?.schema || SCHEMAS[0].schema,
...parseMoreInformation(data),
});

interface Response {
type: Field["type"];
val: string;
fn: string;
}

export type UserData = Response[][];

export const generateNewItem = (schema: Schema): Response[] => {
const item = schema.fields.map((field) => ({
type: field.type,
val: "",
fn: field.data.fn,
}));

return item;
};

0 comments on commit 238211f

Please sign in to comment.