♻️ resource selector in create share link form

This commit is contained in:
Fred KISSIE
2026-03-19 04:44:24 +01:00
parent 8f33e25782
commit e15703164d
3 changed files with 151 additions and 31 deletions

View File

@@ -69,6 +69,7 @@ import {
import AccessTokenSection from "@app/components/AccessTokenUsage"; import AccessTokenSection from "@app/components/AccessTokenUsage";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toUnicode } from "punycode"; import { toUnicode } from "punycode";
import { ResourceSelector, type SelectedResource } from "./resource-selector";
type FormProps = { type FormProps = {
open: boolean; open: boolean;
@@ -99,18 +100,21 @@ export default function CreateShareLinkForm({
orgQueries.resources({ orgId: org?.org.orgId ?? "" }) orgQueries.resources({ orgId: org?.org.orgId ?? "" })
); );
const resources = useMemo( const [selectedResource, setSelectedResource] =
() => useState<SelectedResource | null>(null);
allResources
.filter((r) => r.http) // const resources = useMemo(
.map((r) => ({ // () =>
resourceId: r.resourceId, // allResources
name: r.name, // .filter((r) => r.http)
niceId: r.niceId, // .map((r) => ({
resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` // resourceId: r.resourceId,
})), // name: r.name,
[allResources] // niceId: r.niceId,
); // resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
// })),
// [allResources]
// );
const formSchema = z.object({ const formSchema = z.object({
resourceId: z.number({ message: t("shareErrorSelectResource") }), resourceId: z.number({ message: t("shareErrorSelectResource") }),
@@ -199,15 +203,11 @@ export default function CreateShareLinkForm({
setAccessToken(token.accessToken); setAccessToken(token.accessToken);
setAccessTokenId(token.accessTokenId); setAccessTokenId(token.accessTokenId);
const resource = resources.find(
(r) => r.resourceId === values.resourceId
);
onCreated?.({ onCreated?.({
accessTokenId: token.accessTokenId, accessTokenId: token.accessTokenId,
resourceId: token.resourceId, resourceId: token.resourceId,
resourceName: values.resourceName, resourceName: values.resourceName,
resourceNiceId: resource ? resource.niceId : "", resourceNiceId: selectedResource ? selectedResource.niceId : "",
title: token.title, title: token.title,
createdAt: token.createdAt, createdAt: token.createdAt,
expiresAt: token.expiresAt expiresAt: token.expiresAt
@@ -217,10 +217,10 @@ export default function CreateShareLinkForm({
setLoading(false); setLoading(false);
} }
function getSelectedResourceName(id: number) { // function getSelectedResourceName(id: number) {
const resource = resources.find((r) => r.resourceId === id); // const resource = resources.find((r) => r.resourceId === id);
return `${resource?.name}`; // return `${resource?.name}`;
} // }
return ( return (
<> <>
@@ -241,7 +241,7 @@ export default function CreateShareLinkForm({
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
<div className="space-y-4"> <div className="flex flex-col gap-y-4 px-1">
{!link && ( {!link && (
<Form {...form}> <Form {...form}>
<form <form
@@ -269,10 +269,8 @@ export default function CreateShareLinkForm({
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{field.value {selectedResource?.name
? getSelectedResourceName( ? selectedResource.name
field.value
)
: t( : t(
"resourceSelect" "resourceSelect"
)} )}
@@ -281,7 +279,7 @@ export default function CreateShareLinkForm({
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0"> <PopoverContent className="p-0">
<Command> {/* <Command>
<CommandInput <CommandInput
placeholder={t( placeholder={t(
"resourceSearch" "resourceSearch"
@@ -333,7 +331,36 @@ export default function CreateShareLinkForm({
)} )}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command> */}
<ResourceSelector
orgId={
org.org
.orgId
}
selectedResource={
selectedResource
}
onSelectResource={(
r
) => {
form.setValue(
"resourceId",
r.resourceId
);
form.setValue(
"resourceName",
r.name
);
form.setValue(
"resourceUrl",
`${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
);
setSelectedResource(
r
);
}}
/>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<FormMessage /> <FormMessage />

View File

@@ -0,0 +1,81 @@
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "./ui/command";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { CheckIcon } from "lucide-react";
import { cn } from "@app/lib/cn";
import type { ListResourcesResponse } from "@server/routers/resource";
import { useDebounce } from "use-debounce";
export type SelectedResource = Pick<
ListResourcesResponse["resources"][number],
"name" | "resourceId" | "fullDomain" | "niceId" | "ssl"
>;
export type ResourceSelectorProps = {
orgId: string;
selectedResource?: SelectedResource | null;
onSelectResource: (resource: SelectedResource) => void;
};
export function ResourceSelector({
orgId,
selectedResource,
onSelectResource
}: ResourceSelectorProps) {
const t = useTranslations();
const [resourceSearchQuery, setResourceSearchQuery] = useState("");
const [debouncedSearchQuery] = useDebounce(resourceSearchQuery, 150);
const { data: resources = [] } = useQuery(
orgQueries.resources({
orgId: orgId,
query: debouncedSearchQuery,
perPage: 10
})
);
return (
<Command shouldFilter={false}>
<CommandInput
placeholder={t("resourceSearch")}
value={resourceSearchQuery}
onValueChange={setResourceSearchQuery}
/>
<CommandList>
<CommandEmpty>{t("resourcesNotFound")}</CommandEmpty>
<CommandGroup>
{resources.map((r) => (
<CommandItem
value={`${r.name}:${r.resourceId}`}
key={r.resourceId}
onSelect={() => {
onSelectResource(r);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
r.resourceId ===
selectedResource?.resourceId
? "opacity-100"
: "opacity-0"
)}
/>
{`${r.name}`}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
);
}

View File

@@ -191,14 +191,26 @@ export const orgQueries = {
} }
}), }),
resources: ({ orgId }: { orgId: string }) => resources: ({
orgId,
query,
perPage = 10_000
}: {
orgId: string;
query?: string;
perPage?: number;
}) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "RESOURCES"] as const, queryKey: ["ORG", orgId, "RESOURCES", { query, perPage }] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({ const sp = new URLSearchParams({
pageSize: "10000" pageSize: perPage.toString()
}); });
if (query?.trim()) {
sp.set("query", query);
}
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<ListResourcesResponse> AxiosResponse<ListResourcesResponse>
>(`/org/${orgId}/resources?${sp.toString()}`, { signal }); >(`/org/${orgId}/resources?${sp.toString()}`, { signal });