Skip to content

Commit

Permalink
add toggle buttons to pub field form elements (#665)
Browse files Browse the repository at this point in the history
* add toggle buttons to pub field form elements

* only render field toggles on pub create form

* allow creation of empty pub

* remove log level change
  • Loading branch information
3mcd authored Sep 26, 2024
1 parent 07be5c2 commit 86a6fd7
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 65 deletions.
13 changes: 11 additions & 2 deletions core/app/components/UserSelect/UserSelectClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import {
PubFieldSelectorToggleButton,
} from "ui/pubFields";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip";
import { expect } from "utils";
import { cn, expect } from "utils";

import type { MemberSelectUser, MemberSelectUserWithMembership } from "./types";
import { addMember } from "~/app/c/[communitySlug]/members/[[...add]]/actions";
import { didSucceed, useServerAction } from "~/lib/serverActions";
import { useFormElementToggleContext } from "../forms/FormElementToggleContext";
import { UserAvatar } from "../UserAvatar";
import { isMemberSelectUserWithMembership } from "./types";
import { UserSelectAddUserButton } from "./UserSelectAddUserButton";
Expand Down Expand Up @@ -77,6 +78,8 @@ export function UserSelectClient({
const params = useSearchParams();
const options = useMemo(() => users.map(makeOptionFromUser), [users]);
const runAddMember = useServerAction(addMember);
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(fieldName);

// Force a re-mount of the <UserSelectAddUserButton> element when the
// autocomplete dropdown is closed.
Expand Down Expand Up @@ -104,7 +107,12 @@ export function UserSelectClient({
const formItem = (
<FormItem className="flex flex-col gap-y-1">
<div className="flex items-center justify-between">
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
<FormLabel
className={cn(
"text-sm font-medium leading-none",
!isEnabled && "cursor-not-allowed opacity-50"
)}
>
{fieldLabel}
</FormLabel>
{allowPubFieldSubstitution && <PubFieldSelectorToggleButton />}
Expand All @@ -113,6 +121,7 @@ export function UserSelectClient({
name={fieldName}
value={selectedUserOption}
options={options}
disabled={!isEnabled}
empty={
<UserSelectAddUserButton
key={addUserButtonKey}
Expand Down
20 changes: 14 additions & 6 deletions core/app/components/forms/FormElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DateElement } from "./elements/DateElement";
import { FileUploadElement } from "./elements/FIleUploadElement";
import { TextElement } from "./elements/TextElement";
import { UserIdSelect } from "./elements/UserSelectElement";
import { FormElementToggle } from "./FormElementToggle";

export type FormElementProps = {
pubId: PubsId;
Expand Down Expand Up @@ -48,28 +49,31 @@ export const FormElement = ({
}

const elementProps = { label: labelProp ?? "", name: slug };

let input: JSX.Element | undefined;

if (
schemaName === CoreSchemaType.String ||
schemaName === CoreSchemaType.Email ||
schemaName === CoreSchemaType.URL
) {
return <TextElement {...elementProps} />;
input = <TextElement {...elementProps} />;
}
if (schemaName === CoreSchemaType.Boolean) {
return <BooleanElement {...elementProps} />;
input = <BooleanElement {...elementProps} />;
}
if (schemaName === CoreSchemaType.FileUpload) {
return <FileUploadElement pubId={pubId} {...elementProps} />;
input = <FileUploadElement pubId={pubId} {...elementProps} />;
}
if (schemaName === CoreSchemaType.Vector3) {
return <Vector3Element {...elementProps} />;
input = <Vector3Element {...elementProps} />;
}
if (schemaName === CoreSchemaType.DateTime) {
return <DateElement {...elementProps} />;
input = <DateElement {...elementProps} />;
}
if (schemaName === CoreSchemaType.MemberId) {
const userId = values[element.slug!] as MembersId | undefined;
return (
input = (
<UserIdSelect
label={elementProps.label}
name={elementProps.name}
Expand All @@ -81,5 +85,9 @@ export const FormElement = ({
);
}

if (input) {
return <FormElementToggle {...elementProps}>{input}</FormElementToggle>;
}

throw new Error(`Invalid CoreSchemaType ${schemaName}`);
};
27 changes: 27 additions & 0 deletions core/app/components/forms/FormElementToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { PropsWithChildren } from "react";

import { Toggle } from "ui/toggle";

import { useFormElementToggleContext } from "./FormElementToggleContext";

type Props = PropsWithChildren<ElementProps>;

export const FormElementToggle = (props: Props) => {
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(props.name);
return (
<div className="flex gap-2">
<Toggle
aria-label="Toggle field"
className={
"z-50 h-full w-2 rounded-full p-0 data-[state=off]:bg-gray-200 data-[state=on]:bg-gray-400 data-[state=off]:opacity-50"
}
pressed={isEnabled}
onClick={() => formElementToggle.toggle(props.name)}
/>
<div className="w-full">{props.children}</div>
</div>
);
};
58 changes: 58 additions & 0 deletions core/app/components/forms/FormElementToggleContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import {
createContext,
PropsWithChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";

type FormElementToggleContext = {
isEnabled: (field: string) => boolean;
toggle: (field: string) => void;
};

const FormElementToggleContext = createContext<FormElementToggleContext>({
isEnabled: () => true,
toggle: () => {},
});

type Props = PropsWithChildren<{
fields: string[];
}>;

export const FormElementToggleProvider = (props: Props) => {
const [enabledFields, setEnabledFields] = useState(new Set(props.fields));

const isEnabled = useCallback(
(field: string) => {
return enabledFields.has(field);
},
[enabledFields]
);

const toggle = useCallback(
(field: string) => {
const nextEnabledFields = new Set(enabledFields);
if (nextEnabledFields.has(field)) {
nextEnabledFields.delete(field);
} else {
nextEnabledFields.add(field);
}
setEnabledFields(nextEnabledFields);
},
[enabledFields]
);

const value = useMemo(() => ({ isEnabled, toggle }), [isEnabled, toggle, enabledFields]);

return (
<FormElementToggleContext.Provider value={value}>
{props.children}
</FormElementToggleContext.Provider>
);
};

export const useFormElementToggleContext = () => useContext(FormElementToggleContext);
5 changes: 5 additions & 0 deletions core/app/components/forms/elements/BooleanElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { useFormContext } from "react-hook-form";
import { Checkbox } from "ui/checkbox";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/form";

import { useFormElementToggleContext } from "../FormElementToggleContext";

export const BooleanElement = ({ label, name }: ElementProps) => {
const { control } = useFormContext();
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(name);

return (
<FormField
Expand All @@ -19,6 +23,7 @@ export const BooleanElement = ({ label, name }: ElementProps) => {
<FormControl>
<Checkbox
checked={Boolean(field.value)}
disabled={!isEnabled}
onCheckedChange={(change) => {
if (typeof change === "boolean") {
field.onChange(change);
Expand Down
6 changes: 6 additions & 0 deletions core/app/components/forms/elements/ConfidenceElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useFormContext } from "react-hook-form";

import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/form";

import { useFormElementToggleContext } from "../FormElementToggleContext";

const Confidence = dynamic(
async () => import("ui/customRenderers/confidence/confidence").then((mod) => mod.Confidence),
{
Expand All @@ -16,6 +18,9 @@ const Confidence = dynamic(

export const Vector3Element = ({ label, name }: ElementProps) => {
const { control } = useFormContext();
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(name);

return (
<FormField
control={control}
Expand All @@ -27,6 +32,7 @@ export const Vector3Element = ({ label, name }: ElementProps) => {
<FormControl>
<Confidence
{...field}
disabled={!isEnabled}
min={0}
max={100}
onValueChange={(event) => field.onChange(event)}
Expand Down
11 changes: 10 additions & 1 deletion core/app/components/forms/elements/DateElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useFormContext } from "react-hook-form";

import { FormField, FormItem, FormLabel, FormMessage } from "ui/form";

import { useFormElementToggleContext } from "../FormElementToggleContext";

const DatePicker = dynamic(async () => import("ui/date-picker").then((mod) => mod.DatePicker), {
ssr: false,
// TODO: add better loading state
Expand All @@ -13,14 +15,21 @@ const DatePicker = dynamic(async () => import("ui/date-picker").then((mod) => mo

export const DateElement = ({ label, name }: ElementProps) => {
const { control } = useFormContext();
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(name);

return (
<FormField
name={name}
control={control}
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel>{label}</FormLabel>
<DatePicker date={field.value} setDate={(date) => field.onChange(date)} />
<DatePicker
disabled={!isEnabled}
date={field.value}
setDate={(date) => field.onChange(date)}
/>
<FormMessage />
</FormItem>
)}
Expand Down
5 changes: 5 additions & 0 deletions core/app/components/forms/elements/FIleUploadElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/for

import { upload } from "../actions";
import { FileUploadPreview } from "../FileUpload";
import { useFormElementToggleContext } from "../FormElementToggleContext";

const FileUpload = dynamic(
async () => import("ui/customRenderers/fileUpload/fileUpload").then((mod) => mod.FileUpload),
Expand All @@ -23,7 +24,10 @@ export const FileUploadElement = ({ pubId, label, name }: ElementProps & { pubId
return upload(pubId, fileName);
};
const { control, getValues } = useFormContext();
const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(name);
const files = getValues()[name];

return (
<div>
<FormField
Expand All @@ -37,6 +41,7 @@ export const FileUploadElement = ({ pubId, label, name }: ElementProps & { pubId
<FormControl>
<FileUpload
{...field}
disabled={!isEnabled}
upload={signedUploadUrl}
onUpdateFiles={(event: any[]) => {
field.onChange(event);
Expand Down
14 changes: 11 additions & 3 deletions core/app/components/forms/elements/TextElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import type { InputProps } from "ui/input";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import { useFormElementToggleContext } from "../FormElementToggleContext";

export const TextElement = ({ label, name, ...rest }: ElementProps & InputProps) => {
const { control } = useFormContext();

const formElementToggle = useFormElementToggleContext();
const isEnabled = formElementToggle.isEnabled(name);
return (
<FormField
control={control}
Expand All @@ -17,9 +20,14 @@ export const TextElement = ({ label, name, ...rest }: ElementProps & InputProps)
const { value, ...fieldRest } = field;
return (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormLabel disabled={!isEnabled}>{label}</FormLabel>
<FormControl>
<Input value={value ?? ""} {...fieldRest} {...rest} />
<Input
value={value ?? ""}
{...fieldRest}
{...rest}
disabled={!isEnabled}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
1 change: 1 addition & 0 deletions core/app/components/forms/elements/UserSelectElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const UserIdSelect = async ({
).executeTakeFirstOrThrow();
const queryParamName = `user-${id.split("-").pop()}`;
const query = searchParams?.[queryParamName] as string | undefined;

return (
<UserSelectServer
community={community}
Expand Down
14 changes: 12 additions & 2 deletions core/app/components/pubs/PubEditor/PubEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { db } from "~/kysely/database";
import { getPubCached } from "~/lib/server";
import { getPubFields } from "~/lib/server/pubFields";
import { FormElement } from "../../forms/FormElement";
import { FormElementToggleProvider } from "../../forms/FormElementToggleContext";
import { makeFormElementDefFromPubFields } from "./helpers";
import { PubEditorClient } from "./PubEditorClient";
import { getCommunityById, getStage } from "./queries";
Expand Down Expand Up @@ -96,8 +97,7 @@ export async function PubEditor(props: PubEditorProps) {
));

const currentStageId = pub?.stages[0]?.id ?? ("stageId" in props ? props.stageId : undefined);

return (
const editor = (
<PubEditorClient
availablePubTypes={community.pubTypes}
availableStages={community.stages}
Expand All @@ -112,4 +112,14 @@ export async function PubEditor(props: PubEditorProps) {
isUpdating={isUpdating}
/>
);

if (isUpdating) {
return editor;
}

return (
<FormElementToggleProvider fields={pubFields.map((pubField) => pubField.slug)}>
{editor}
</FormElementToggleProvider>
);
}
Loading

0 comments on commit 86a6fd7

Please sign in to comment.