Remove old frontend
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
56
apps/console/src/components/form/ControlledField.tsx
Normal file
56
apps/console/src/components/form/ControlledField.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ComponentProps, JSX, JSXElementConstructor } from "react";
|
||||
import { Field } from "@probo/ui";
|
||||
import { Controller, type Control } from "react-hook-form";
|
||||
import { Select } from "@probo/ui";
|
||||
|
||||
type Props<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
|
||||
ComponentProps<T> & {
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function ControlledField({
|
||||
control,
|
||||
name,
|
||||
...props
|
||||
}: Props<typeof Field>) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Field
|
||||
{...props}
|
||||
{...field}
|
||||
// TODO : Find a better way to handle this case (comparing number and string for select create issues)
|
||||
value={field.value ? field.value.toString() : ""}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ControlledSelect({
|
||||
control,
|
||||
name,
|
||||
...props
|
||||
}: Props<typeof Select>) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id={name}
|
||||
{...props}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
49
apps/console/src/components/form/EmailsField.tsx
Normal file
49
apps/console/src/components/form/EmailsField.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useFieldArray } from "react-hook-form";
|
||||
import type { Control } from "react-hook-form";
|
||||
import type { UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
register: UseFormRegister<any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A field to handle multiple emails
|
||||
*/
|
||||
export function EmailsField({ control, register }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: "additionalEmailAddresses",
|
||||
control,
|
||||
});
|
||||
|
||||
return (
|
||||
<fieldset className="space-y-2">
|
||||
{fields.length > 0 && <Label>{__("Additional emails")}</Label>}
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-stretch">
|
||||
<Input
|
||||
className="w-full"
|
||||
{...register(`additionalEmailAddresses.${index}`)}
|
||||
type="email"
|
||||
/>
|
||||
<Button
|
||||
icon={IconTrashCan}
|
||||
variant="tertiary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
type="button"
|
||||
icon={IconPlusLarge}
|
||||
onClick={() => append("")}
|
||||
>
|
||||
{__("Add email")}
|
||||
</Button>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
90
apps/console/src/components/form/MeasureSelectField.tsx
Normal file
90
apps/console/src/components/form/MeasureSelectField.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Field, Option, Select } from "@probo/ui";
|
||||
import { Suspense, useMemo, useState, type ComponentProps } from "react";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { type Control, Controller } from "react-hook-form";
|
||||
import { usePaginatedMeasures } from "/hooks/graph/usePaginatedMeasures";
|
||||
|
||||
type Props = {
|
||||
organizationId: string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
} & ComponentProps<typeof Field>;
|
||||
|
||||
export function MeasureSelectField({
|
||||
organizationId,
|
||||
control,
|
||||
disabled,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<Field {...props}>
|
||||
<Suspense
|
||||
fallback={<Select variant="editor" disabled placeholder="Loading..." />}
|
||||
>
|
||||
<MeasureSelectWithQuery
|
||||
organizationId={organizationId}
|
||||
control={control}
|
||||
name={props.name}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
function MeasureSelectWithQuery(
|
||||
props: Pick<Props, "organizationId" | "control" | "name" | "disabled">
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { name, organizationId, control, disabled } = props;
|
||||
const { data } = usePaginatedMeasures(organizationId);
|
||||
const [search, setSearch] = useState("");
|
||||
const measures = useMemo(() => {
|
||||
return (
|
||||
data?.measures.edges
|
||||
?.filter(
|
||||
(edge) =>
|
||||
edge.node.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
edge.node.description?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
.map((edge) => edge.node) ?? []
|
||||
);
|
||||
}, [data?.measures.edges, search]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Select a measure")}
|
||||
onValueChange={field.onChange}
|
||||
{...field}
|
||||
className="w-full"
|
||||
value={field.value ?? ""}
|
||||
onSearch={setSearch}
|
||||
searchValue={search}
|
||||
disabled={disabled}
|
||||
>
|
||||
{measures?.map((m) => (
|
||||
<Option key={m.id} value={m.id}>
|
||||
<div className="space-y-1 text-start min-w-0">
|
||||
<div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap">
|
||||
{m.name}
|
||||
</div>
|
||||
<div className="text-sm text-txt-secondary">{m.category}</div>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
apps/console/src/components/form/PeopleSelectField.tsx
Normal file
71
apps/console/src/components/form/PeopleSelectField.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Avatar, Field, Option, Select } from "@probo/ui";
|
||||
import { Suspense, type ComponentProps } from "react";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { type Control, Controller } from "react-hook-form";
|
||||
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
|
||||
|
||||
type Props = {
|
||||
organizationId: string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
} & ComponentProps<typeof Field>;
|
||||
|
||||
export function PeopleSelectField({
|
||||
organizationId,
|
||||
control,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<Field {...props}>
|
||||
<Suspense
|
||||
fallback={<Select variant="editor" disabled placeholder="Loading..." />}
|
||||
>
|
||||
<PeopleSelectWithQuery
|
||||
organizationId={organizationId}
|
||||
control={control}
|
||||
name={props.name}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
function PeopleSelectWithQuery(
|
||||
props: Pick<Props, "organizationId" | "control" | "name" | "disabled">
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { name, organizationId, control } = props;
|
||||
const people = usePeople(organizationId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Select an owner")}
|
||||
onValueChange={field.onChange}
|
||||
key={people?.length.toString() ?? "0"}
|
||||
{...field}
|
||||
className="w-full"
|
||||
value={field.value ?? ""}
|
||||
>
|
||||
{people?.map((p) => (
|
||||
<Option key={p.id} value={p.id} className="flex gap-2">
|
||||
<Avatar name={p.fullName} />
|
||||
{p.fullName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
133
apps/console/src/components/form/VendorsMultiSelectField.tsx
Normal file
133
apps/console/src/components/form/VendorsMultiSelectField.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Avatar, Field, Option, Select, Badge, Button, IconCrossLargeX } from "@probo/ui";
|
||||
import { Suspense, useState, type ComponentProps } from "react";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { type Control, Controller } from "react-hook-form";
|
||||
import { useVendors } from "/hooks/graph/VendorGraph.ts";
|
||||
import { faviconUrl } from "@probo/helpers";
|
||||
|
||||
type Props = {
|
||||
organizationId: string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
} & ComponentProps<typeof Field>;
|
||||
|
||||
export function VendorsMultiSelectField({
|
||||
organizationId,
|
||||
control,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<Field {...props}>
|
||||
<Suspense
|
||||
fallback={<Select variant="editor" disabled placeholder="Loading..." />}
|
||||
>
|
||||
<VendorsMultiSelectWithQuery
|
||||
organizationId={organizationId}
|
||||
control={control}
|
||||
name={props.name}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
function VendorsMultiSelectWithQuery(
|
||||
props: Pick<Props, "organizationId" | "control" | "name" | "disabled">
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { name, organizationId, control } = props;
|
||||
const vendors = useVendors(organizationId);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
const selectedVendorIds = Array.isArray(field.value) ? field.value : [];
|
||||
const selectedVendors = vendors.filter(v => selectedVendorIds.includes(v.id));
|
||||
const availableVendors = vendors.filter(v => !selectedVendorIds.includes(v.id));
|
||||
|
||||
const handleAddVendor = (vendorId: string) => {
|
||||
const newValue = [...selectedVendorIds, vendorId];
|
||||
field.onChange(newValue);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleRemoveVendor = (vendorId: string) => {
|
||||
const newValue = selectedVendorIds.filter((id: string) => id !== vendorId);
|
||||
field.onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{availableVendors.length > 0 && (
|
||||
<Select
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Add vendors...")}
|
||||
onValueChange={handleAddVendor}
|
||||
key={`${selectedVendorIds.length}-${vendors.length}`}
|
||||
className="w-full"
|
||||
value=""
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{availableVendors.map((vendor) => (
|
||||
<Option key={vendor.id} value={vendor.id} className="flex gap-2">
|
||||
<Avatar
|
||||
name={vendor.name}
|
||||
src={faviconUrl(vendor.websiteUrl)}
|
||||
size="s"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span>{vendor.name}</span>
|
||||
{vendor.websiteUrl && (
|
||||
<span className="text-xs text-txt-secondary">
|
||||
{vendor.websiteUrl}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{selectedVendors.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedVendors.map((vendor) => (
|
||||
<Badge key={vendor.id} variant="neutral" className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={vendor.name}
|
||||
src={faviconUrl(vendor.websiteUrl)}
|
||||
size="s"
|
||||
/>
|
||||
<span>{vendor.name}</span>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={IconCrossLargeX}
|
||||
onClick={() => handleRemoveVendor(vendor.id)}
|
||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedVendors.length === 0 && availableVendors.length === 0 && (
|
||||
<div className="text-sm text-txt-secondary py-2">
|
||||
{__("No vendors available")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user