ui(admin): add egg variable elements

This commit is contained in:
Matthew Penner 2021-10-01 11:07:48 -06:00
parent e2de673488
commit 7239f0e336
No known key found for this signature in database
GPG key ID: BAB67850901908A8
6 changed files with 185 additions and 44 deletions

View file

@ -39,7 +39,7 @@ const EggRouter = () => {
useEffect(() => { useEffect(() => {
clearFlashes('egg'); clearFlashes('egg');
getEgg(Number(match.params?.id)) getEgg(Number(match.params?.id), [ 'variables' ])
.then(egg => setEgg(egg)) .then(egg => setEgg(egg))
.catch(error => { .catch(error => {
console.error(error); console.error(error);

View file

@ -1,11 +1,96 @@
import { Form, Formik, useFormikContext } from 'formik';
import React from 'react'; import React from 'react';
import tw from 'twin.macro';
import { object } from 'yup';
import { Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import { Egg } from '@/api/admin/eggs/getEgg'; import Checkbox from '@/components/elements/Checkbox';
import Field, { FieldRow, TextareaField } from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
const { isSubmitting } = useFormikContext();
export default ({ egg }: { egg: Egg }) => {
return ( return (
<AdminBox title={'Variables'}> <AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{name}</p>}>
{egg.name} <SpinnerOverlay visible={isSubmitting}/>
<Field
id={'name'}
name={'name'}
label={'Name'}
type={'text'}
css={tw`mb-6`}
/>
<TextareaField
id={'description'}
name={'description'}
label={'Description'}
rows={3}
css={tw`mb-4`}
/>
<FieldRow>
<Field
id={'envVariable'}
name={'envVariable'}
label={'Environment Variable'}
type={'text'}
/>
<Field
id={'defaultValue'}
name={'defaultValue'}
label={'Default Value'}
type={'text'}
/>
</FieldRow>
<div css={tw`flex flex-row mb-6`}>
<Checkbox
id={'userViewable'}
name={'userViewable'}
label={'User Viewable'}
/>
<Checkbox
id={'userEditable'}
name={'userEditable'}
label={'User Editable'}
css={tw`ml-auto`}
/>
</div>
<Field
id={'rules'}
name={'rules'}
label={'Validation Rules'}
type={'text'}
css={tw`mb-2`}
/>
</AdminBox> </AdminBox>
); );
}; }
export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
const submit = () => {
//
};
return (
<Formik
onSubmit={submit}
initialValues={{
}}
validationSchema={object().shape({
})}
>
<Form>
<div css={tw`grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6`}>
{egg.relations?.variables?.map(v => <EggVariableForm key={v.id} variable={v}/>)}
</div>
</Form>
</Formik>
);
}

View file

@ -1,42 +1,35 @@
import Label from '@/components/elements/Label';
import React from 'react'; import React from 'react';
import { Field, FieldProps } from 'formik'; import { Field, FieldProps } from 'formik';
import Input from '@/components/elements/Input'; import Input from '@/components/elements/Input';
import tw from 'twin.macro';
interface Props { interface Props {
name: string; name: string;
value: string; label?: string;
className?: string; className?: string;
} }
type OmitFields = 'ref' | 'name' | 'value' | 'type' | 'checked' | 'onClick' | 'onChange'; type OmitFields = 'ref' | 'name' | 'value' | 'type';
type InputProps = Omit<JSX.IntrinsicElements['input'], OmitFields>; type InputProps = Omit<JSX.IntrinsicElements['input'], OmitFields>;
const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => ( const Checkbox = ({ name, label, className, ...props }: Props & InputProps) => (
<Field name={name}> <Field name={name}>
{({ field, form }: FieldProps) => { {({ field }: FieldProps) => {
if (!Array.isArray(field.value)) {
console.error('Attempting to mount a checkbox using a field value that is not an array.');
return null;
}
return ( return (
<Input <div css={tw`flex flex-row`} className={className}>
{...field} <Input
{...props} {...field}
className={className} {...props}
type={'checkbox'} css={tw`w-5 h-5 mr-2`}
checked={(field.value || []).includes(value)} type={'checkbox'}
onClick={() => form.setFieldTouched(field.name, true)} />
onChange={e => { {label &&
const set = new Set(field.value); <div css={tw`flex-1`}>
set.has(value) ? set.delete(value) : set.add(value); <Label noBottomSpacing>{label}</Label>
</div>}
field.onChange(e); </div>
form.setFieldValue(field.name, Array.from(set));
}}
/>
); );
}} }}
</Field> </Field>

View file

@ -1,9 +1,9 @@
import React, { forwardRef } from 'react';
import { Field as FormikField, FieldProps } from 'formik'; import { Field as FormikField, FieldProps } from 'formik';
import React, { forwardRef } from 'react';
import tw, { styled } from 'twin.macro';
import Input, { Textarea } from '@/components/elements/Input'; import Input, { Textarea } from '@/components/elements/Input';
import Label from '@/components/elements/Label';
import InputError from '@/components/elements/InputError'; import InputError from '@/components/elements/InputError';
import tw from 'twin.macro'; import Label from '@/components/elements/Label';
interface OwnProps { interface OwnProps {
name: string; name: string;
@ -11,15 +11,17 @@ interface OwnProps {
label?: string; label?: string;
description?: string; description?: string;
validate?: (value: any) => undefined | string | Promise<any>; validate?: (value: any) => undefined | string | Promise<any>;
className?: string;
} }
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>; type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, ...props }, ref) => ( const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, className, ...props }, ref) => (
<FormikField innerRef={ref} name={name} validate={validate}> <FormikField innerRef={ref} name={name} validate={validate}>
{ {
({ field, form: { errors, touched } }: FieldProps) => ( ({ field, form: { errors, touched } }: FieldProps) => (
<div> <div className={className}>
{label && {label &&
<div css={tw`flex flex-row`} title={description}> <div css={tw`flex flex-row`} title={description}>
<Label htmlFor={id} isLight={light}>{label}</Label> <Label htmlFor={id} isLight={light}>{label}</Label>
@ -43,15 +45,17 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
)); ));
Field.displayName = 'Field'; Field.displayName = 'Field';
type Props2 = OwnProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name'>; export default Field;
export const TextareaField = forwardRef<HTMLTextAreaElement, Props2>( type TextareaProps = OwnProps & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name'>;
function TextareaField ({ id, name, light = false, label, description, validate, ...props }, ref) {
export const TextareaField = forwardRef<HTMLTextAreaElement, TextareaProps>(
function TextareaField ({ id, name, light = false, label, description, validate, className, ...props }, ref) {
return ( return (
<FormikField innerRef={ref} name={name} validate={validate}> <FormikField innerRef={ref} name={name} validate={validate}>
{ {
({ field, form: { errors, touched } }: FieldProps) => ( ({ field, form: { errors, touched } }: FieldProps) => (
<div> <div className={className}>
{label && <Label htmlFor={id} isLight={light}>{label}</Label>} {label && <Label htmlFor={id} isLight={light}>{label}</Label>}
<Textarea <Textarea
id={id} id={id}
@ -72,4 +76,22 @@ export const TextareaField = forwardRef<HTMLTextAreaElement, Props2>(
); );
TextareaField.displayName = 'TextareaField'; TextareaField.displayName = 'TextareaField';
export default Field; export const FieldRow = styled.div`
${tw`mb-6 md:w-full md:flex md:flex-row`};
& > div {
${tw`md:w-full md:flex md:flex-col mb-6 md:mb-0`};
&:first-of-type {
${tw`md:mr-3`};
}
&:not(:first-of-type):not(:last-of-type) {
${tw`md:mx-3`};
}
&:last-of-type {
${tw`md:ml-3`};
}
}
`;

View file

@ -1,7 +1,8 @@
import tw, { styled } from 'twin.macro'; import tw, { styled } from 'twin.macro';
const Label = styled.label<{ isLight?: boolean }>` const Label = styled.label<{ isLight?: boolean, noBottomSpacing?: boolean }>`
${tw`block text-xs uppercase text-neutral-200 mb-1 sm:mb-2`}; ${tw`block text-xs uppercase text-neutral-200`};
${props => !props.noBottomSpacing && tw`mb-1 sm:mb-2`};
${props => props.isLight && tw`text-neutral-700`}; ${props => props.isLight && tw`text-neutral-700`};
`; `;

View file

@ -1,8 +1,48 @@
import tw, { styled } from 'twin.macro'; import tw, { styled } from 'twin.macro';
import Checkbox from '@/components/elements/Checkbox';
import React from 'react'; import React from 'react';
import { useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import { Field, FieldProps } from 'formik';
import Input from '@/components/elements/Input';
interface CheckboxProps {
name: string;
value: string;
className?: string;
}
type CheckboxOmitFields = 'ref' | 'name' | 'value' | 'type' | 'checked' | 'onClick' | 'onChange';
type InputProps = Omit<JSX.IntrinsicElements['input'], CheckboxOmitFields> & CheckboxProps;
const Checkbox = ({ name, value, className, ...props }: InputProps) => (
<Field name={name}>
{({ field, form }: FieldProps) => {
if (!Array.isArray(field.value)) {
console.error('Attempting to mount a checkbox using a field value that is not an array.');
return null;
}
return (
<Input
{...field}
{...props}
className={className}
type={'checkbox'}
checked={(field.value || []).includes(value)}
onClick={() => form.setFieldTouched(field.name, true)}
onChange={e => {
const set = new Set(field.value);
set.has(value) ? set.delete(value) : set.add(value);
field.onChange(e);
form.setFieldValue(field.name, Array.from(set));
}}
/>
);
}}
</Field>
);
const Container = styled.label` const Container = styled.label`
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`}; ${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};