# Related items panel

> The records linked to a record, with a picker to add or remove a link.

Source: https://bettersupabase.com/docs/registry/ui-recipes/related-items-panel

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [relations block](https://bettersupabase.com/blocks/relations) must be installed (`npx better-supabase add relations`). Its page on bettersupabase.com is [/recipes/related-items-panel](https://bettersupabase.com/recipes/related-items-panel).

Web, with shadcn components:

```bash
npx shadcn add badge button card empty field select
```

```tsx title="apps/examples/nextjs/src/features/customers/components/related-items-panel.tsx"
/** Customers linked to this one (the `relates` kind), and a picker to link more. */
export function RelatedItemsPanel({
  customerId,
  relations,
  canManage,
}: {
  customerId: string;
  relations: Loaded<Relations>;
  canManage: boolean;
}) {
  const t = useExtracted("customers");

  return (
    <Card size="sm" data-recipe="related-items-panel">
      <CardHeader>
        <CardTitle>{t("Related customers")}</CardTitle>
        <CardDescription>
          {t("Link customers that belong together, such as a parent company.")}
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        {relations.kind === "error" ? (
          <RecordLoadError title={t("The related customers didn't load")} />
        ) : (
          <>
            {relations.data.links.length === 0 ? (
              <Empty className="border">
                <EmptyHeader>
                  <EmptyMedia variant="icon">
                    <LinkIcon />
                  </EmptyMedia>
                  <EmptyTitle>{t("No related customers")}</EmptyTitle>
                  <EmptyDescription>
                    {canManage
                      ? t("Pick a customer below to link it.")
                      : t("Nobody linked a customer yet.")}
                  </EmptyDescription>
                </EmptyHeader>
              </Empty>
            ) : (
              <RelatedList
                customerId={customerId}
                links={relations.data.links}
                canManage={canManage}
              />
            )}
            {canManage ? (
              <LinkForm
                customerId={customerId}
                options={relations.data.options.filter(
                  (option) =>
                    !relations.data.links.some(
                      (link) =>
                        link.subjectType === "customer" &&
                        link.subjectId === option.id,
                    ),
                )}
              />
            ) : null}
          </>
        )}
      </CardContent>
    </Card>
  );
}

function RelatedList({
  customerId,
  links,
  canManage,
}: {
  customerId: string;
  links: readonly RelatedCustomer[];
  canManage: boolean;
}) {
  const t = useExtracted("customers");
  const errorMessage = useErrorMessage();

  const unlink = useAction(unlinkCustomerRelations, {
    onSuccess: () => {
      toast.success(t("Link removed"));
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

  return (
    <ul className="flex flex-col divide-y" data-testid="related-customers">
      {links.map((link) => (
        <li
          key={link.id}
          className="flex items-center justify-between gap-2 py-2"
        >
          <div className="flex min-w-0 items-center gap-2">
            <SubjectLink
              subject={{ type: link.subjectType, id: link.subjectId }}
            />
            {link.kind === "relates" ? null : (
              <Badge variant="outline">{link.kind}</Badge>
            )}
          </div>
          {canManage ? (
            <Button
              size="icon-sm"
              variant="ghost"
              aria-label={t("Remove link")}
              disabled={unlink.pendingInputs.some(
                (input) =>
                  !(input instanceof FormData) &&
                  input.input.target.id === link.subjectId,
              )}
              onClick={() => {
                void unlink.run({
                  id: customerId,
                  input: {
                    kind: link.kind,
                    target: { type: link.subjectType, id: link.subjectId },
                  },
                });
              }}
            >
              <UnlinkIcon />
            </Button>
          ) : null}
        </li>
      ))}
    </ul>
  );
}

function LinkForm({
  customerId,
  options,
}: {
  customerId: string;
  options: readonly { readonly id: string; readonly name: string }[];
}) {
  const t = useExtracted("customers");
  const fieldId = useId();
  const errorMessage = useErrorMessage();
  const [targetId, setTargetId] = useState<string | null>(null);

  const link = useAction(linkCustomerRelations, {
    onSuccess: () => {
      setTargetId(null);
      toast.success(t("Customer linked"));
    },
  });

  const items = options.map((option) => ({
    value: option.id,
    label: option.name,
  }));

  if (items.length === 0) {
    return null;
  }

  return (
    <form
      className="flex items-end gap-2"
      onSubmit={(event) => {
        event.preventDefault();

        if (targetId) {
          void link.run({
            id: customerId,
            input: {
              kind: "relates",
              target: { type: "customer", id: targetId },
            },
          });
        }
      }}
    >
      <Field data-invalid={link.error ? true : undefined}>
        <FieldLabel htmlFor={`${fieldId}-related`}>
          {t("Link a customer")}
        </FieldLabel>
        <Select
          value={targetId}
          items={items}
          onValueChange={(value) => {
            setTargetId(value);
          }}
        >
          <SelectTrigger id={`${fieldId}-related`} className="w-full">
            <SelectValue placeholder={t("Pick a customer")} />
          </SelectTrigger>
          <SelectContent>
            {items.map((item) => (
              <SelectItem key={item.value} value={item.value}>
                {item.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        {link.error ? (
          <FieldError>{errorMessage(link.error)}</FieldError>
        ) : null}
      </Field>
      <Button
        type="submit"
        variant="outline"
        disabled={!targetId || link.pending}
      >
        {t("Link")}
      </Button>
    </form>
  );
}

/** The related customers card while its rows load. */
export function RelatedItemsPanelSkeleton() {
  return <RecordCardSkeleton rows={2} />;
}
```