workshopping multi select

This commit is contained in:
Mikayla Dobson
2023-02-16 10:35:37 -06:00
parent 1daf3418ce
commit 99829533fd
12 changed files with 216 additions and 68 deletions

View File

@@ -5,11 +5,7 @@ import { useAuthContext } from "../context/AuthContext";
import { ICollection, IIngredient } from "../schemas"; import { ICollection, IIngredient } from "../schemas";
import { Button, Page } from "./ui"; import { Button, Page } from "./ui";
import API from "../util/API"; import API from "../util/API";
import { OptionType } from "../util/types";
interface OptionType {
value: number
label: string
}
export default function Sandbox() { export default function Sandbox() {
const [ingredients, setIngredients] = useState<Array<IIngredient>>([]); const [ingredients, setIngredients] = useState<Array<IIngredient>>([]);

View File

@@ -1,18 +1,105 @@
import { useCallback, useRef, useEffect, useState, createRef } from "react"; import { useCallback, useRef, useEffect, useState, createRef } from "react";
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../context/AuthContext";
import { Button, Card, Divider, Form, Page, Panel } from "../ui" import { Button, Card, Divider, Form, Page, Panel } from "../ui"
import { IRecipe } from "../../schemas"; import { IIngredient, IRecipe } from "../../schemas";
import API from "../../util/API"; import API from "../../util/API";
import Creatable from "react-select/creatable";
import { OptionType } from "../../util/types";
import { useSelectorContext } from "../../context/SelectorContext";
import { MultiValue } from "react-select";
const AddRecipe = () => { const AddRecipe = () => {
const { user, token } = useAuthContext(); const { user, token } = useAuthContext();
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] }) const {
const [toast, setToast] = useState(<></>) data, setData, selector, setSelector,
options, setOptions, selected, setSelected,
onChange, onCreateOption
} = useSelectorContext();
const [triggerChange, setTriggerChange] = useState(false);
const [form, setForm] = useState<JSX.Element>();
const [toast, setToast] = useState(<></>)
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' })
// clear out selector state on page load
useEffect(() => {
setData(new Array<IIngredient>());
setSelected(new Array<OptionType>());
setOptions(new Array<OptionType>());
}, [])
// store all ingredients on page mount
useEffect(() => {
token && (async() => {
const ingredients = new API.Ingredient(token);
const result = await ingredients.getAll();
if (result) {
setData((prev) => [...prev, ...result]);
// once async data is received, derive its new states
setOptions(result.map((each: IIngredient) => {
return { label: each.name, value: each.id }
}));
}
})();
}, [token])
useEffect(() => {
console.log(selected);
}, [selected])
useEffect(() => {
if (data.length) {
console.log('caught');
// create dropdown from new data
const selectorInstance = <Creatable
className="ui-creatable-component"
id="ingredient-selector"
isMulti
value={selected}
options={options}
onChange={(selection: MultiValue<OptionType>) => onChange(selection)}
onCreateOption={(input: string) => onCreateOption(input, () => {})}
/>
data.length && setSelector(selectorInstance);
setTriggerChange(true);
}
}, [data, options])
// once the dropdown data has populated, mount it within the full form
useEffect(() => {
triggerChange && setForm(
<Form<IRecipe> _config={{
parent: "AddRecipe",
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'SELECTOR', 'TINYMCE'],
initialState: input,
getState: getFormState,
richTextInitialValue: "<p>Enter recipe details here!</p>",
selectorInstance: selector
}} />
)
}, [triggerChange])
// once user information is available, store it in recipe data
useEffect(() => {
if (!user) return;
user && setInput((prev: IRecipe) => {
return {
...prev,
authoruserid: user.id!
}
})
}, [user])
// store input data from form
const getFormState = useCallback((data: IRecipe) => { const getFormState = useCallback((data: IRecipe) => {
setInput(data); setInput(data);
}, [input]) }, [input])
// submit handler
const handleCreate = async () => { const handleCreate = async () => {
if (!token) return; if (!token) return;
@@ -23,8 +110,6 @@ const AddRecipe = () => {
} }
} }
console.log('good to go!')
const recipe = new API.Recipe(token); const recipe = new API.Recipe(token);
const result = await recipe.post(input); const result = await recipe.post(input);
@@ -38,20 +123,6 @@ const AddRecipe = () => {
</Card> </Card>
) )
} }
useEffect(() => {
if (!user) return;
user && setInput((prev: IRecipe) => {
return {
...prev,
authoruserid: user.id!
}
})
}, [user])
useEffect(() => {
console.log(input);
}, [input])
return ( return (
<Page> <Page>
@@ -59,16 +130,7 @@ const AddRecipe = () => {
<Divider /> <Divider />
<Panel extraStyles="width-80"> <Panel extraStyles="width-80">
<Form parent={input} _config={{ { form }
parent: "AddRecipe",
keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"],
labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"],
dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'SELECTOR', 'TINYMCE'],
initialState: input,
getState: getFormState,
richTextInitialValue: "<p>Enter recipe details here!</p>"
}} />
<Button onClick={handleCreate}>Create Recipe!</Button> <Button onClick={handleCreate}>Create Recipe!</Button>
<div id="toast">{ toast }</div> <div id="toast">{ toast }</div>

View File

@@ -43,7 +43,7 @@ export default function Login() {
<Panel extraStyles="form-panel"> <Panel extraStyles="form-panel">
<Form parent={input} _config={{ <Form<IUserAuth> _config={{
parent: 'login', parent: 'login',
keys: Object.keys(input), keys: Object.keys(input),
labels: ["Email", "Password"], labels: ["Email", "Password"],

View File

@@ -54,7 +54,7 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
<Panel extraStyles="form-panel two-columns"> <Panel extraStyles="form-panel two-columns">
<Form parent={input} _config={{ <Form<IUser> _config={{
parent: "register", parent: "register",
keys: ['firstname', 'lastname', 'handle', 'email', 'password'], keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
initialState: input, initialState: input,

View File

@@ -1,6 +1,8 @@
import { ChangeEvent, FC, useEffect, useState } from "react" import { ChangeEvent, FC, useEffect, useState } from "react"
import { v4 } from "uuid" import { v4 } from "uuid"
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../context/AuthContext";
import { useSelectorContext } from "../../context/SelectorContext";
import SelectorProvider from "../../context/SelectorProvider";
import { IIngredient, IUser } from "../../schemas"; import { IIngredient, IUser } from "../../schemas";
import API from "../../util/API"; import API from "../../util/API";
import RichText from "./RichText" import RichText from "./RichText"
@@ -16,26 +18,25 @@ export interface FormConfig<T> {
dataTypes?: string[] dataTypes?: string[]
richTextInitialValue?: string richTextInitialValue?: string
extraStyles?: string extraStyles?: string
selectorInstance?: JSX.Element
} }
interface FormProps { interface FormProps {
parent: any
_config: FormConfig<any> _config: FormConfig<any>
} }
const Form: FC<FormProps> = ({ parent, _config }) => { function Form<T>({ _config }: FormProps) {
type T = typeof parent;
const { getState } = _config; const { getState } = _config;
const [config, setConfig] = useState<FormConfig<T>>(); const [config, setConfig] = useState<FormConfig<T>>();
const [state, setState] = useState<T>(); const [state, setState] = useState<T>(_config.initialState);
const [contents, setContents] = useState<JSX.Element[]>(); const [contents, setContents] = useState<JSX.Element[]>();
const { token } = useAuthContext(); const { token } = useAuthContext();
// initial setup // initial setup
useEffect(() => { useEffect(() => {
if (!config) setConfig({ setConfig({
..._config, ..._config,
labels: _config.labels ?? _config.keys, labels: _config.labels ?? _config.keys,
dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"), dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"),
@@ -101,14 +102,14 @@ const Form: FC<FormProps> = ({ parent, _config }) => {
</div> </div>
) )
} else if (config.dataTypes![i] == 'SELECTOR') { } else if (config.dataTypes![i] == 'SELECTOR') {
type StrongType = Partial<T> & { id: number, name: string }; if (!config.selectorInstance) throw new Error("Dropdown was not provided to form component.")
const storedResult = await (async() => {
const result = await populateSelector(config?.labels![i] || ""); return (
if (result) return result as T[]; <div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
return null; <label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
})(); { config.selectorInstance }
</div>
return <Selector<StrongType> config={config} idx={i} optionList={storedResult || []} /> )
} else { } else {
return ( return (
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}> <div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>

View File

@@ -1,24 +1,22 @@
import reactSelect from "react-select"
import makeAnimated from "react-select/animated";
import { FormConfig } from "./Form" import { FormConfig } from "./Form"
import { v4 } from "uuid"
interface Entity { interface OptionType {
id: string | number value: number
name?: string label: string
} }
function Selector<T extends Entity>({ config, idx, optionList }: { config: FormConfig<T>, idx: number, optionList: Array<T> }) { interface SelectorProps<T> {
// const Selector: FC<{ optionList: Array<T extends HasID> }> = ({ optionList }) => { config: FormConfig<T>
idx: number
update: (e: any, idx: number) => void
optionList: Array<OptionType>
loader?: Array<T>
}
function Selector<T>({ config, idx, update, optionList }: SelectorProps<T>) {
return ( return (
<div className="form-row form-row-selector" id={`${config.parent}-row-${idx}`} key={v4()}> <>
<label htmlFor={`${config.parent}-${config.keys[idx]}`}>{config.labels![idx]}</label> </>
<select className="ui-select-component">
{ optionList.map(item =>
<option id={`select-item-${item.name}-${item.id}`}>{item.name}</option>
)}
</select>
</div>
) )
} }

View File

@@ -0,0 +1,31 @@
import { createContext, Dispatch, SetStateAction, useContext } from "react"
import { OptionType } from "../util/types"
interface SelectorContextProps<T> {
data: Array<T>
setData: Dispatch<SetStateAction<Array<T>>> | VoidFunction
selected: Array<OptionType>
setSelected: Dispatch<SetStateAction<Array<OptionType>>> | VoidFunction
options: Array<OptionType>
setOptions: Dispatch<SetStateAction<Array<OptionType>>> | VoidFunction
selector: JSX.Element
setSelector: Dispatch<SetStateAction<JSX.Element>> | VoidFunction
onChange: (...params: any) => void
onCreateOption: (label: string, generateObject: (label: string, id: number) => T) => void
}
const defaultValue: SelectorContextProps<any> = {
data: new Array<any>(),
setData: () => {},
selected: new Array<any>(),
setSelected: () => {},
options: new Array<OptionType>(),
setOptions: () => {},
selector: <></>,
setSelector: () => {},
onChange: () => {},
onCreateOption: (label: string) => {},
}
export const SelectorContext = createContext<SelectorContextProps<any>>(defaultValue);
export const useSelectorContext = () => useContext(SelectorContext);

View File

@@ -0,0 +1,40 @@
import { useState } from "react";
import { OptionType } from "../util/types";
import { SelectorContext } from "./SelectorContext";
function SelectorProvider<T>({ children }: { children: JSX.Element | JSX.Element[] }) {
const [data, setData] = useState<Array<T>>([]);
const [selector, setSelector] = useState<JSX.Element>(<></>)
const [options, setOptions] = useState<Array<OptionType>>([]);
const [selected, setSelected] = useState<Array<OptionType>>([]);
/**
* Event handler for a change in selection state
*/
const onChange = (data: Array<any>) => {
setSelected((prev) => [...prev, ...data]);
}
const onCreateOption = (label: string, generateObject: (label: string, id: number) => T) => {
const newID = options.length + 1;
const newOption: OptionType = { label: label, value: newID }
setOptions((prev) => [...prev, newOption]);
setSelected((prev) => [...prev, newOption]);
setData((prev) => [...prev, generateObject(label, newID)]);
}
const providerValue = {
data, setData, selector, setSelector,
options, setOptions, selected, setSelected,
onChange, onCreateOption
}
return (
<SelectorContext.Provider value={ providerValue }>
{ children }
</SelectorContext.Provider>
)
}
export default SelectorProvider;

View File

@@ -2,12 +2,15 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'
import AuthProvider from './context/AuthProvider' import AuthProvider from './context/AuthProvider'
import SelectorProvider from './context/SelectorProvider'
import './sass/index.scss' import './sass/index.scss'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<AuthProvider> <AuthProvider>
<React.StrictMode> <SelectorProvider>
<App /> <React.StrictMode>
</React.StrictMode> <App />
</React.StrictMode>
</SelectorProvider>
</AuthProvider> </AuthProvider>
) )

View File

@@ -7,3 +7,7 @@
text-align: center; text-align: center;
background-color: $richblack; background-color: $richblack;
} }
.ui-creatable-component {
margin-bottom: 10rem;
}

View File

@@ -188,6 +188,11 @@ module API {
constructor(token: string) { constructor(token: string) {
super(Settings.getAPISTRING() + "/app/ingredient", token); super(Settings.getAPISTRING() + "/app/ingredient", token);
} }
async associateIngredientWithRecipe(recipeID: string | number, ingredientID: string | number) {
const response = await this.instance.post(this.endpoint + `/${ingredientID}?recipeID=${recipeID}`, this.headers);
return Promise.resolve(response.data);
}
} }
export class Collection extends RestController<ICollection> { export class Collection extends RestController<ICollection> {

View File

@@ -38,6 +38,14 @@ interface CheckboxProps {
FormElement: typeof Form FormElement: typeof Form
} }
/**
* Type declaration for react-select dropdown options
*/
export interface OptionType {
value: number
label: string
}
export type PageComponent = FC<PortalBase> export type PageComponent = FC<PortalBase>
export type PanelComponent = FC<PortalBase> export type PanelComponent = FC<PortalBase>
export type ButtonComponent = FC<ButtonParams> export type ButtonComponent = FC<ButtonParams>