workshopping multi select
This commit is contained in:
@@ -5,11 +5,7 @@ import { useAuthContext } from "../context/AuthContext";
|
||||
import { ICollection, IIngredient } from "../schemas";
|
||||
import { Button, Page } from "./ui";
|
||||
import API from "../util/API";
|
||||
|
||||
interface OptionType {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
import { OptionType } from "../util/types";
|
||||
|
||||
export default function Sandbox() {
|
||||
const [ingredients, setIngredients] = useState<Array<IIngredient>>([]);
|
||||
|
||||
@@ -1,18 +1,105 @@
|
||||
import { useCallback, useRef, useEffect, useState, createRef } from "react";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { Button, Card, Divider, Form, Page, Panel } from "../ui"
|
||||
import { IRecipe } from "../../schemas";
|
||||
import { IIngredient, IRecipe } from "../../schemas";
|
||||
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 { user, token } = useAuthContext();
|
||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] })
|
||||
const [toast, setToast] = useState(<></>)
|
||||
const {
|
||||
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) => {
|
||||
setInput(data);
|
||||
}, [input])
|
||||
|
||||
// submit handler
|
||||
const handleCreate = async () => {
|
||||
if (!token) return;
|
||||
|
||||
@@ -23,8 +110,6 @@ const AddRecipe = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('good to go!')
|
||||
|
||||
const recipe = new API.Recipe(token);
|
||||
const result = await recipe.post(input);
|
||||
|
||||
@@ -39,36 +124,13 @@ const AddRecipe = () => {
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
user && setInput((prev: IRecipe) => {
|
||||
return {
|
||||
...prev,
|
||||
authoruserid: user.id!
|
||||
}
|
||||
})
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(input);
|
||||
}, [input])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Add a New Recipe</h1>
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="width-80">
|
||||
<Form parent={input} _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>"
|
||||
}} />
|
||||
|
||||
{ form }
|
||||
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||
|
||||
<div id="toast">{ toast }</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Login() {
|
||||
|
||||
<Panel extraStyles="form-panel">
|
||||
|
||||
<Form parent={input} _config={{
|
||||
<Form<IUserAuth> _config={{
|
||||
parent: 'login',
|
||||
keys: Object.keys(input),
|
||||
labels: ["Email", "Password"],
|
||||
|
||||
@@ -54,7 +54,7 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
||||
|
||||
<Panel extraStyles="form-panel two-columns">
|
||||
|
||||
<Form parent={input} _config={{
|
||||
<Form<IUser> _config={{
|
||||
parent: "register",
|
||||
keys: ['firstname', 'lastname', 'handle', 'email', 'password'],
|
||||
initialState: input,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { v4 } from "uuid"
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { useSelectorContext } from "../../context/SelectorContext";
|
||||
import SelectorProvider from "../../context/SelectorProvider";
|
||||
import { IIngredient, IUser } from "../../schemas";
|
||||
import API from "../../util/API";
|
||||
import RichText from "./RichText"
|
||||
@@ -16,26 +18,25 @@ export interface FormConfig<T> {
|
||||
dataTypes?: string[]
|
||||
richTextInitialValue?: string
|
||||
extraStyles?: string
|
||||
selectorInstance?: JSX.Element
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
parent: any
|
||||
_config: FormConfig<any>
|
||||
}
|
||||
|
||||
const Form: FC<FormProps> = ({ parent, _config }) => {
|
||||
type T = typeof parent;
|
||||
function Form<T>({ _config }: FormProps) {
|
||||
const { getState } = _config;
|
||||
|
||||
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 { token } = useAuthContext();
|
||||
|
||||
// initial setup
|
||||
useEffect(() => {
|
||||
if (!config) setConfig({
|
||||
setConfig({
|
||||
..._config,
|
||||
labels: _config.labels ?? _config.keys,
|
||||
dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"),
|
||||
@@ -101,14 +102,14 @@ const Form: FC<FormProps> = ({ parent, _config }) => {
|
||||
</div>
|
||||
)
|
||||
} else if (config.dataTypes![i] == 'SELECTOR') {
|
||||
type StrongType = Partial<T> & { id: number, name: string };
|
||||
const storedResult = await (async() => {
|
||||
const result = await populateSelector(config?.labels![i] || "");
|
||||
if (result) return result as T[];
|
||||
return null;
|
||||
})();
|
||||
if (!config.selectorInstance) throw new Error("Dropdown was not provided to form component.")
|
||||
|
||||
return <Selector<StrongType> config={config} idx={i} optionList={storedResult || []} />
|
||||
return (
|
||||
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
<label htmlFor={`${config.parent}-${each}`}>{config.labels![i]}</label>
|
||||
{ config.selectorInstance }
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="form-row" id={`${config.parent}-row-${i}`} key={v4()}>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import reactSelect from "react-select"
|
||||
import makeAnimated from "react-select/animated";
|
||||
import { FormConfig } from "./Form"
|
||||
import { v4 } from "uuid"
|
||||
|
||||
interface Entity {
|
||||
id: string | number
|
||||
name?: string
|
||||
interface OptionType {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
|
||||
function Selector<T extends Entity>({ config, idx, optionList }: { config: FormConfig<T>, idx: number, optionList: Array<T> }) {
|
||||
// const Selector: FC<{ optionList: Array<T extends HasID> }> = ({ optionList }) => {
|
||||
interface SelectorProps<T> {
|
||||
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 (
|
||||
<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>
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
31
client/src/context/SelectorContext.tsx
Normal file
31
client/src/context/SelectorContext.tsx
Normal 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);
|
||||
40
client/src/context/SelectorProvider.tsx
Normal file
40
client/src/context/SelectorProvider.tsx
Normal 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;
|
||||
@@ -2,12 +2,15 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import AuthProvider from './context/AuthProvider'
|
||||
import SelectorProvider from './context/SelectorProvider'
|
||||
import './sass/index.scss'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<AuthProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<SelectorProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</SelectorProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
|
||||
@@ -7,3 +7,7 @@
|
||||
text-align: center;
|
||||
background-color: $richblack;
|
||||
}
|
||||
|
||||
.ui-creatable-component {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
@@ -188,6 +188,11 @@ module API {
|
||||
constructor(token: string) {
|
||||
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> {
|
||||
|
||||
@@ -38,6 +38,14 @@ interface CheckboxProps {
|
||||
FormElement: typeof Form
|
||||
}
|
||||
|
||||
/**
|
||||
* Type declaration for react-select dropdown options
|
||||
*/
|
||||
export interface OptionType {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
|
||||
export type PageComponent = FC<PortalBase>
|
||||
export type PanelComponent = FC<PortalBase>
|
||||
export type ButtonComponent = FC<ButtonParams>
|
||||
|
||||
Reference in New Issue
Block a user