♻️ 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 { useTranslations } from "next-intl";
import { toUnicode } from "punycode";
import { ResourceSelector, type SelectedResource } from "./resource-selector";
type FormProps = {
open: boolean;
@@ -99,18 +100,21 @@ export default function CreateShareLinkForm({
orgQueries.resources({ orgId: org?.org.orgId ?? "" })
);
const resources = useMemo(
() =>
allResources
.filter((r) => r.http)
.map((r) => ({
resourceId: r.resourceId,
name: r.name,
niceId: r.niceId,
resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
})),
[allResources]
);
const [selectedResource, setSelectedResource] =
useState<SelectedResource | null>(null);
// const resources = useMemo(
// () =>
// allResources
// .filter((r) => r.http)
// .map((r) => ({
// resourceId: r.resourceId,
// name: r.name,
// niceId: r.niceId,
// resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
// })),
// [allResources]
// );
const formSchema = z.object({
resourceId: z.number({ message: t("shareErrorSelectResource") }),
@@ -199,15 +203,11 @@ export default function CreateShareLinkForm({
setAccessToken(token.accessToken);
setAccessTokenId(token.accessTokenId);
const resource = resources.find(
(r) => r.resourceId === values.resourceId
);
onCreated?.({
accessTokenId: token.accessTokenId,
resourceId: token.resourceId,
resourceName: values.resourceName,
resourceNiceId: resource ? resource.niceId : "",
resourceNiceId: selectedResource ? selectedResource.niceId : "",
title: token.title,
createdAt: token.createdAt,
expiresAt: token.expiresAt
@@ -217,10 +217,10 @@ export default function CreateShareLinkForm({
setLoading(false);
}
function getSelectedResourceName(id: number) {
const resource = resources.find((r) => r.resourceId === id);
return `${resource?.name}`;
}
// function getSelectedResourceName(id: number) {
// const resource = resources.find((r) => r.resourceId === id);
// return `${resource?.name}`;
// }
return (
<>
@@ -241,7 +241,7 @@ export default function CreateShareLinkForm({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<div className="flex flex-col gap-y-4 px-1">
{!link && (
<Form {...form}>
<form
@@ -269,10 +269,8 @@ export default function CreateShareLinkForm({
"text-muted-foreground"
)}
>
{field.value
? getSelectedResourceName(
field.value
)
{selectedResource?.name
? selectedResource.name
: t(
"resourceSelect"
)}
@@ -281,7 +279,7 @@ export default function CreateShareLinkForm({
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
{/* <Command>
<CommandInput
placeholder={t(
"resourceSearch"
@@ -333,7 +331,36 @@ export default function CreateShareLinkForm({
)}
</CommandGroup>
</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>
</Popover>
<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({
queryKey: ["ORG", orgId, "RESOURCES"] as const,
queryKey: ["ORG", orgId, "RESOURCES", { query, perPage }] as const,
queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({
pageSize: "10000"
pageSize: perPage.toString()
});
if (query?.trim()) {
sp.set("query", query);
}
const res = await meta!.api.get<
AxiosResponse<ListResourcesResponse>
>(`/org/${orgId}/resources?${sp.toString()}`, { signal });