Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 92 additions & 59 deletions apps/webapp/app/components/assets/custom-fields-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,71 @@ export default function AssetCustomFields({
),
};

const requiredFields = customFields.filter((field) => field.required);
type LoaderCustomField = ReturnType<
typeof useLoaderData<typeof loader>
>["customFields"][number];

const optionalFields = customFields.filter((field) => !field.required);
const renderCustomFieldRow = (field: LoaderCustomField, index: number) => (
<FormRow
key={field.id + index}
rowLabel={field.name}
subHeading={field.helpText ? <p>{field.helpText}</p> : undefined}
className="border-b-0"
required={field.required}
>
{fieldTypeToCompMap[field.type as CustomFieldType]?.(
field as unknown as CustomField
) ?? (
<Input
hideLabel
placeholder={field.helpText || undefined}
type={field.type.toLowerCase()}
label={field.name}
name={`cf-${field.id}`}
error={getFieldError(field.id)}
disabled={disabled}
defaultValue={getCustomFieldVal(field.id)}
className="w-full"
required={isFieldRequired(field.id)}
/>
)}
</FormRow>
);

const hasGroups = useMemo(
() => customFields.some((field: LoaderCustomField) => field.group),
[customFields]
);

const groupedFields = useMemo(() => {
if (!hasGroups) return [];
const groupsMap = new Map<string | null, LoaderCustomField[]>();
for (const field of customFields) {
const gName = field.group?.name || null;
if (!groupsMap.has(gName)) {
groupsMap.set(gName, []);
}
groupsMap.get(gName)!.push(field);
}
const result: Array<{
groupName: string | null;
fields: LoaderCustomField[];
}> = [];
for (const [groupName, fields] of groupsMap.entries()) {
result.push({ groupName, fields });
}
return result;
}, [customFields, hasGroups]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const requiredFields = useMemo(
() => customFields.filter((field) => field.required),
[customFields]
);

const optionalFields = useMemo(
() => customFields.filter((field) => !field.required),
[customFields]
);

return (
<div className="border-b pb-6">
Expand All @@ -219,67 +281,38 @@ export default function AssetCustomFields({
</div>
{customFields.length > 0 ? (
<>
{requiredFields.length > 0 && (
<div className="border-t pt-4">
<h5>Required Fields</h5>
{requiredFields.map((field, index) => (
<FormRow
key={field.id + index}
rowLabel={field.name}
subHeading={
field.helpText ? <p>{field.helpText}</p> : undefined
}
className="border-b-0"
required={field.required}
>
{fieldTypeToCompMap[field.type]?.(field) ?? (
<Input
hideLabel
placeholder={field.helpText || undefined}
type={field.type.toLowerCase()}
label={field.name}
name={`cf-${field.id}`}
error={getFieldError(field.id)}
disabled={disabled}
defaultValue={getCustomFieldVal(field.id)}
className="w-full"
required={isFieldRequired(field.id)}
/>
{hasGroups ? (
<div className="flex flex-col gap-6">
{groupedFields.map(({ groupName, fields }) => (
<div key={groupName || "ungrouped"} className="border-t pt-4">
<h5 className="mb-3 text-base font-semibold text-gray-800">
{groupName || "Other Fields"}
</h5>
{fields.map((field, index) =>
renderCustomFieldRow(field, index)
)}
</FormRow>
</div>
))}
</div>
)}
{optionalFields.length > 0 && (
<div className="border-t pt-4">
<h5>Optional Fields</h5>
{optionalFields.map((field, index) => (
<FormRow
key={field.id + index}
rowLabel={field.name}
subHeading={
field.helpText ? <p>{field.helpText}</p> : undefined
}
className="border-b-0"
required={field.required}
>
{fieldTypeToCompMap[field.type]?.(field) ?? (
<Input
hideLabel
placeholder={field.helpText || undefined}
type={field.type.toLowerCase()}
label={field.name}
name={`cf-${field.id}`}
error={getFieldError(field.id)}
disabled={disabled}
defaultValue={getCustomFieldVal(field.id)}
className="w-full"
required={isFieldRequired(field.id)}
/>
) : (
<>
{requiredFields.length > 0 && (
<div className="border-t pt-4">
<h5>Required Fields</h5>
{requiredFields.map((field, index) =>
renderCustomFieldRow(field, index)
)}
</FormRow>
))}
</div>
</div>
)}
{optionalFields.length > 0 && (
<div className="border-t pt-4">
<h5>Optional Fields</h5>
{optionalFields.map((field, index) =>
renderCustomFieldRow(field, index)
)}
</div>
)}
</>
)}
</>
) : (
Expand Down
81 changes: 80 additions & 1 deletion apps/webapp/app/components/custom-fields/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useState } from "react";
import { CustomFieldType, type CustomField } from "@prisma/client";
import { useAtom } from "jotai";
import { Link, useActionData, useNavigation } from "react-router";
import {
Link,
useActionData,
useNavigation,
useLoaderData,
} from "react-router";
import { useZorm } from "react-zorm";
import { z } from "zod";
import { updateDynamicTitleAtom } from "~/atoms/dynamic-title-atom";
Expand Down Expand Up @@ -51,6 +56,18 @@ export const NewCustomFieldFormSchema = z.object({
.array(z.string().min(1, "Please select a category"))
.optional()
.default([]),
groupId: z
.string()
.optional()
.transform((val) => (val === "none" || !val ? null : val)),
position: z
.string()
.optional()
.transform((val) => {
if (!val) return 0;
const parsed = parseInt(val, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}),
});

/** Pass props of the values to be used as default for the form fields */
Expand All @@ -63,6 +80,8 @@ interface Props {
options?: CustomField["options"];
isEdit?: boolean;
categories?: string[];
groupId?: string | null;
position?: number;
}

/**
Expand Down Expand Up @@ -94,6 +113,8 @@ export const CustomFieldForm = ({
active,
isEdit = false,
categories = EMPTY_CATEGORIES,
groupId,
position = 0,
}: Props) => {
const navigation = useNavigation();
const zo = useZorm("NewQuestionWizardScreen", NewCustomFieldFormSchema);
Expand All @@ -107,6 +128,12 @@ export const CustomFieldForm = ({

const [, updateTitle] = useAtom(updateDynamicTitleAtom);

type FormLoaderData = {
groups?: Array<{ id: string; name: string }>;
};
const loaderData = (useLoaderData<FormLoaderData>() || {}) as FormLoaderData;
const groups = loaderData?.groups || [];

// Focus the Name field on mount — the form is the entry point for both
// create and edit pages, so initial focus belongs on the first field.
const nameInputRef = useAutoFocus<HTMLInputElement>();
Expand Down Expand Up @@ -330,6 +357,58 @@ export const CustomFieldForm = ({
</FormRow>
</div>

<FormRow rowLabel="Group" className="border-b-0 pb-[10px] pt-[6px]">
<Select
name="groupId"
defaultValue={groupId || "none"}
disabled={disabled}
>
<SelectTrigger className="px-3.5 py-3">
<SelectValue placeholder="Choose a group (optional)" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-full min-w-[300px]"
align="start"
>
<SelectItem value="none">
<span className="text-[14px] text-gray-700">
None (No Group)
</span>
</SelectItem>
{groups.map((g) => (
<SelectItem value={g.id} key={g.id}>
<span className="text-[14px] text-gray-700">{g.name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
{(validationErrors?.groupId?.message ||
zo.errors.groupId()?.message) && (
<div className="mt-1 text-sm text-error-500">
{validationErrors?.groupId?.message ||
zo.errors.groupId()?.message}
</div>
)}
</FormRow>

<FormRow rowLabel="Position" className="border-b-0 pb-[10px] pt-[6px]">
<Input
type="number"
label="Position"
hideLabel
name="position"
disabled={disabled}
defaultValue={position !== undefined ? String(position) : "0"}
placeholder="0"
error={
validationErrors?.position?.message ||
zo.errors.position()?.message
}
className="w-full"
/>
</FormRow>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{/* hidden field organization Id to get the organization Id on each form submission to link custom fields and its value is loaded using useOrganizationId hook */}
<input
type="hidden"
Expand Down
Loading