Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Related pubs in form #910

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 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
20 changes: 17 additions & 3 deletions core/app/components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
* updated designs
*/

import type { ColumnDef, ColumnFiltersState, Row, SortingState } from "@tanstack/react-table";
import type {
ColumnDef,
ColumnFiltersState,
Row,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";

import * as React from "react";
import {
Expand Down Expand Up @@ -31,6 +37,10 @@ export interface DataTableProps<TData, TValue> {
className?: string;
striped?: boolean;
emptyState?: React.ReactNode;
/** Control row selection */
selectedRows?: RowSelectionState;
setSelectedRows?: React.Dispatch<React.SetStateAction<{}>>;
getRowId?: (data: TData) => string;
}

const STRIPED_ROW_STYLING = "hover:bg-gray-100 data-[state=selected]:bg-sky-50";
Expand All @@ -44,6 +54,9 @@ export function DataTable<TData, TValue>({
className,
striped,
emptyState,
selectedRows,
setSelectedRows,
getRowId,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
Expand All @@ -58,11 +71,12 @@ export function DataTable<TData, TValue>({
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnFiltersChange: setColumnFilters,
onRowSelectionChange: setRowSelection,
onRowSelectionChange: setSelectedRows ?? setRowSelection,
getRowId: getRowId,
state: {
sorting,
columnFilters,
rowSelection,
rowSelection: selectedRows ?? rowSelection,
},
});

Expand Down
15 changes: 7 additions & 8 deletions core/app/components/DataTable/v2/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { DataTable as DataTableV1 } from "../DataTable";
/**
* Wrapper around DataTable so that some fields can use updated designs
*/
export function DataTable<TData, TValue>({
columns,
data,
onRowClick,
}: Pick<DataTableProps<TData, TValue>, "columns" | "data" | "onRowClick">) {
export function DataTable<TData, TValue>(
props: Pick<
DataTableProps<TData, TValue>,
"columns" | "data" | "onRowClick" | "selectedRows" | "setSelectedRows" | "getRowId"
>
) {
return (
<DataTableV1
columns={columns}
data={data}
onRowClick={onRowClick}
{...props}
// Render nothing on empty instead of the default
emptyState={<></>}
hidePaginationWhenSinglePage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ const componentInfo: Record<InputComponent, SchemaComponentData> = {
[InputComponent.textInput]: {
name: "Input",
placeholder: "For short text",
demoComponent: ({ element }) => (
<Input
placeholder={
element.schemaName === CoreSchemaType.String ? "For short text" : "For numbers"
}
type={element.schemaName === CoreSchemaType.String ? "text" : "number"}
/>
),
demoComponent: ({ element }) => {
const isText =
element.schemaName === CoreSchemaType.String ||
element.schemaName === CoreSchemaType.Email;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email was showing up as a number component in the demo component part

return (
<Input
placeholder={isText ? "For short text" : "For numbers"}
type={isText ? "text" : "number"}
/>
);
},
},
[InputComponent.checkbox]: { name: "Checkbox", demoComponent: () => <Checkbox checked /> },
[InputComponent.datePicker]: {
Expand Down Expand Up @@ -183,7 +186,7 @@ const componentInfo: Record<InputComponent, SchemaComponentData> = {
return (
<div className="flex w-full flex-col gap-1 text-left text-sm">
<div className="text-gray-500">Label</div>
<MultiBlock title="Pub Relation" disabled />
<MultiBlock title="Pub Relation" disabled compact onAdd={() => {}} />
</div>
);
},
Expand Down
55 changes: 13 additions & 42 deletions core/app/components/FormBuilder/FormBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { FormBuilderSchema, FormElementData, PanelEvent, PanelState } from
import type { Form as PubForm } from "~/lib/server/form";
import { renderWithPubTokens } from "~/lib/server/render/pub/renderWithPubTokens";
import { didSucceed, useServerAction } from "~/lib/serverActions";
import { PanelHeader, PanelWrapper, SidePanel } from "../SidePanel";
import { saveForm } from "./actions";
import { ElementPanel } from "./ElementPanel";
import { FormBuilderProvider, useFormBuilder } from "./FormBuilderContext";
Expand Down Expand Up @@ -95,49 +96,12 @@ const elementPanelTitles: Record<PanelState["state"], string> = {
editingButton: "Edit Submission Button",
};

const PanelHeader = ({ state }: { state: PanelState["state"] }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved all the side panel stuff to another component so I could reuse it in the form fill page. I think this would be better off using a drawer or maybe even the new side bar component from shadcn, but for now I reused what we had to keep things consistent

const { dispatch } = useFormBuilder();
return (
<>
<div className="flex items-center justify-between">
<div className="text-sm uppercase text-gray-500">{elementPanelTitles[state]}</div>
{state !== "initial" && (
<Button
aria-label="Cancel"
variant="ghost"
size="sm"
className=""
onClick={() => dispatch({ eventName: "cancel" })}
>
<X size={16} className="text-muted-foreground" />
</Button>
)}
</div>
<hr />
</>
);
};

type Props = {
pubForm: PubForm;
id: string;
stages: Stages[];
};

// Render children in a portal so they can safely use <form> components
function PanelWrapper({
children,
sidebar,
}: {
children: React.ReactNode;
sidebar: Element | null;
}) {
if (!sidebar) {
return null;
}
return createPortal(children, sidebar);
}

export function FormBuilder({ pubForm, id, stages }: Props) {
const router = useRouter();
const pathname = usePathname();
Expand Down Expand Up @@ -336,7 +300,17 @@ export function FormBuilder({ pubForm, id, stages }: Props) {
</div>
<PanelWrapper sidebar={sidebarRef.current}>
<FormItem className="relative flex h-screen flex-col">
<PanelHeader state={panelState.state} />
<PanelHeader
title={
elementPanelTitles[panelState.state]
}
showCancel={
!(panelState.state === "initial")
}
onCancel={() =>
dispatch({ eventName: "cancel" })
}
/>
<FormControl>
<ElementPanel panelState={panelState} />
</FormControl>
Expand All @@ -351,10 +325,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) {
<TabsContent value="preview">Preview your form here</TabsContent>
</div>
</Tabs>
<div
ref={sidebarRef}
className="fixed right-0 top-[72px] z-10 flex h-[calc(100%-72px)] w-[380px] flex-col gap-10 overflow-auto border-l border-gray-200 bg-gray-50 p-4 pr-6 shadow"
></div>
<SidePanel ref={sidebarRef} />
</FormBuilderProvider>
</TokenProvider>
);
Expand Down
67 changes: 67 additions & 0 deletions core/app/components/SidePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { forwardRef } from "react";
import { createPortal } from "react-dom";

import { Button } from "ui/button";
import { X } from "ui/icon";
import { cn } from "utils";

// Render children in a portal so they can safely use <form> components
export const PanelWrapper = ({
children,
sidebar,
}: {
children: React.ReactNode;
sidebar: Element | null;
}) => {
if (!sidebar) {
return null;
}
return createPortal(children, sidebar);
};

export const PanelHeader = ({
title,
showCancel,
onCancel,
}: {
title: string;
showCancel: boolean;
onCancel: () => void;
}) => {
return (
<>
<div className="flex items-center justify-between">
<div className="text-sm uppercase text-gray-500">{title}</div>
{showCancel && (
<Button
aria-label="Cancel"
variant="ghost"
size="sm"
className=""
onClick={onCancel}
>
<X size={16} className="text-muted-foreground" />
</Button>
)}
</div>
<hr />
</>
);
};

export const SidePanel = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ children, className, ...rest }, ref) => {
return (
<div
{...rest}
ref={ref}
className={cn(
"fixed right-0 top-[72px] z-10 flex h-[calc(100%-72px)] w-[380px] flex-col gap-10 overflow-auto border-l border-gray-200 bg-gray-50 p-4 pr-6 shadow",
className
)}
>
{children}
</div>
);
}
);
100 changes: 100 additions & 0 deletions core/app/components/forms/AddRelatedPubsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";

import type { ColumnDef } from "@tanstack/react-table";

import { useRef, useState } from "react";

import type { PubsId } from "db/public";
import { pubFieldsIdSchema, pubsIdSchema } from "db/public";
import { Button } from "ui/button";
import { Checkbox } from "ui/checkbox";
import { DataTableColumnHeader } from "ui/data-table";

import type { GetPubsResult } from "~/lib/server";
import { PanelHeader, SidePanel } from "~/app/components/SidePanel";
import { DataTable } from "../DataTable/v2/DataTable";

const getColumns = () =>
[
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="mr-2 h-4 w-4"
/>
),
enableSorting: false,
enableHiding: false,
},
{
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
accessorKey: "name",
cell: ({ row }) => {
return (
<div className="flex items-center gap-2">
<span>{row.original.title}</span>
</div>
);
},
},
] as const satisfies ColumnDef<GetPubsResult[number], unknown>[];

export const AddRelatedPubsPanel = ({
title,
onCancel,
onAdd,
pubs,
}: {
title: string;
onCancel: () => void;
onAdd: (pubs: GetPubsResult) => void;
pubs: GetPubsResult;
}) => {
const sidebarRef = useRef(null);
const [selected, setSelected] = useState<Record<string, boolean>>({});

const handleAdd = () => {
const selectedPubIds = Object.entries(selected)
.filter(([pubId, selected]) => selected)
.map((selection) => selection[0] as PubsId);
const selectedPubs = pubs.filter((p) => selectedPubIds.includes(p.id));
onAdd(selectedPubs);
onCancel();
};

return (
<SidePanel ref={sidebarRef} className="justify-between">
<div className="flex flex-col gap-2">
<PanelHeader title={title} showCancel onCancel={onCancel} />
<DataTable
columns={getColumns()}
data={pubs}
selectedRows={selected}
setSelectedRows={setSelected}
getRowId={(d) => d.id}
/>
</div>
<div className="flex w-full justify-between gap-2">
<Button variant="outline" className="flex-1" onClick={onCancel}>
Cancel
</Button>
<Button onClick={handleAdd} className="flex-1 bg-blue-500 hover:bg-blue-600">
Add
</Button>
</div>
</SidePanel>
);
};
Loading
Loading