workshopping multi select
This commit is contained in:
@@ -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>>([]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()}>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,3 +7,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $richblack;
|
background-color: $richblack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-creatable-component {
|
||||||
|
margin-bottom: 10rem;
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user