chore: all attribute forms

This commit is contained in:
Aaryan Khandelwal 2023-09-12 19:36:35 +05:30
parent 6867154963
commit e5b466a3c4
26 changed files with 1823 additions and 16 deletions

View File

@ -0,0 +1,160 @@
import Image from "next/image";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { CheckCircle2, ChevronDown } from "lucide-react";
// assets
import CheckRepresentation from "public/custom-attributes/checkbox/check.svg";
import ToggleSwitchRepresentation from "public/custom-attributes/checkbox/toggle-switch.svg";
// types
import { ICustomAttribute, TCustomAttributeTypes } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
const checkboxAttributeRepresentations = [
{
image: CheckRepresentation,
key: "check",
label: "Check",
},
{
image: ToggleSwitchRepresentation,
key: "toggle_switch",
label: "Toggle Switch",
},
];
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "checked",
display_name: "",
extra_settings: {
representation: "check",
},
is_required: false,
};
export const CheckboxAttributeForm: React.FC<Props> = () => {
const { control, watch } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Input placeholder="Enter field title" />
<div>
<p className="text-xs">Default value</p>
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="default_value"
value="checked"
id="checked"
className="scale-75"
defaultChecked
/>
<label htmlFor="checked">Checked</label>
</div>
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="default_value"
value="unchecked"
id="unchecked"
className="scale-75"
/>
<label htmlFor="unchecked">Unchecked</label>
</div>
</div>
</div>
</div>
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
<div>
<h6 className="text-xs">Show as</h6>
<div className="mt-2 flex items-center gap-4 flex-wrap">
<Controller
control={control}
name="extra_settings.representation"
render={({ field: { onChange, value } }) => (
<>
{checkboxAttributeRepresentations.map((representation) => (
<div
key={representation.key}
className={`rounded divide-y w-32 cursor-pointer border ${
value === representation.key
? "border-custom-primary-100 divide-custom-primary-100"
: "border-custom-border-200 divide-custom-border-200"
}`}
onClick={() => onChange(representation.key)}
>
<div className="h-24 p-2.5 grid place-items-center">
<Image src={representation.image} alt={representation.label} />
</div>
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
{representation.label}
{value === representation.key && (
<CheckCircle2
size={14}
strokeWidth={1.5}
className="text-custom-primary-100"
/>
)}
</div>
</div>
))}
</>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,129 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { CustomSelect, ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST, DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
extra_settings: {
date_format: "DD-MM-YYYY",
time_format: "12",
},
is_required: false,
};
export const DateTimeAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.datetime;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="extra_settings.date_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">
{DATE_FORMATS.find((f) => f.value === value)?.label}
</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{DATE_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
<Controller
control={control}
name="extra_settings.time_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">
{TIME_FORMATS.find((f) => f.value === value)?.label}
</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{TIME_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,92 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
is_required: false,
};
export const EmailAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.email;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="email"
placeholder="Enter default email"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,89 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// components
import { FileFormatsDropdown } from "components/custom-attributes";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
display_name: "",
extra_settings: {
file_formats: [],
},
is_multi: false,
is_required: false,
};
export const FileAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.files;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="extra_settings.file_formats"
render={({ field: { onChange, value } }) => (
<FileFormatsDropdown value={value} onChange={onChange} />
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,9 @@
export * from "./checkbox-attribute-form";
export * from "./date-time-attribute-form";
export * from "./email-attribute-form";
export * from "./file-attribute-form";
export * from "./number-attribute-form";
export * from "./relation-attribute-form";
export * from "./select-attribute-form";
export * from "./text-attribute-form";
export * from "./url-attribute-form";

View File

@ -0,0 +1,212 @@
import Image from "next/image";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { CheckCircle2, ChevronDown } from "lucide-react";
// assets
import NumericalRepresentation from "public/custom-attributes/number/numerical.svg";
import BarRepresentation from "public/custom-attributes/number/bar.svg";
import RingRepresentation from "public/custom-attributes/number/ring.svg";
// types
import { ICustomAttribute, TCustomAttributeTypes } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
import { Controller, useForm } from "react-hook-form";
type Props = {};
const numberAttributeRepresentations = [
{
image: NumericalRepresentation,
key: "numerical",
label: "Numerical",
},
{
image: BarRepresentation,
key: "bar",
label: "Bar",
},
{
image: RingRepresentation,
key: "ring",
label: "Ring",
},
];
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
extra_settings: {
color: "Blue",
divided_by: 100,
representation: "numerical",
show_number: true,
},
is_required: false,
};
export const NumberAttributeForm: React.FC<Props> = () => {
const { control, watch } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="number"
placeholder="Enter default value"
value={value}
onChange={onChange}
step={1}
/>
)}
/>
</div>
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
<div>
<h6 className="text-xs">Show as</h6>
<div className="mt-2 flex items-center gap-4 flex-wrap">
<Controller
control={control}
name="extra_settings.representation"
render={({ field: { onChange, value } }) => (
<>
{numberAttributeRepresentations.map((representation) => (
<div
key={representation.key}
className={`rounded divide-y w-32 cursor-pointer border ${
value === representation.key
? "border-custom-primary-100 divide-custom-primary-100"
: "border-custom-border-200 divide-custom-border-200"
}`}
onClick={() => onChange(representation.key)}
>
<div className="h-24 p-2.5 grid place-items-center">
<Image src={representation.image} alt={representation.label} />
</div>
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
{representation.label}
{value === representation.key && (
<CheckCircle2
size={14}
strokeWidth={1.5}
className="text-custom-primary-100"
/>
)}
</div>
</div>
))}
</>
)}
/>
</div>
</div>
{(watch("extra_settings.representation") === "bar" ||
watch("extra_settings.representation") === "ring") && (
<div className="mt-6 grid grid-cols-3 gap-x-2 gap-y-3 items-center">
<>
<div className="text-xs">Divided by</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.divided_by"
render={({ field: { onChange, value } }) => (
<Input
type="number"
placeholder="Maximum value"
value={value}
onChange={onChange}
step={1}
/>
)}
/>
</div>
</>
<>
<div className="text-xs">Color</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.color"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Accent color"
value={value}
onChange={onChange}
/>
)}
/>
</div>
</>
<>
<div className="text-xs">Show number</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.show_number"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value} onChange={onChange} />
)}
/>
</div>
</>
</div>
)}
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,129 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { CustomSelect, ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST, CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
display_name: "",
is_multi: false,
is_required: false,
unit: "cycle",
};
export const RelationAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="unit"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={<span className="capitalize text-xs">{value}</span>}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{CUSTOM_ATTRIBUTE_UNITS.map((unit) => {
if (unit.value === "user") return null;
return (
<CustomSelect.Option key={unit.value} value={unit.value}>
{unit.label}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>
<div>
<p className="text-xs">Selection type</p>
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="is_multi"
value="false"
id="singleSelect"
className="scale-75"
defaultChecked
/>
<label htmlFor="singleSelect">Single Select</label>
</div>
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="is_multi"
value="true"
id="multiSelect"
className="scale-75"
/>
<label htmlFor="multiSelect">Multi select</label>
</div>
</div>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,157 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react";
// ui
import { CustomSelect, ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown, GripVertical, MoreHorizontal } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
import { TwitterPicker } from "react-color";
import React from "react";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
is_required: false,
};
export const SelectOption: React.FC = () => (
<div className="group w-3/5 flex items-center justify-between gap-1 hover:bg-custom-background-80 px-2 py-1 rounded">
<div className="flex items-center gap-1 flex-grow truncate">
{/* <button type="button">
<GripVertical className="text-custom-text-400" size={14} />
</button> */}
<p className="bg-custom-primary-500/10 text-custom-text-300 text-xs p-1 rounded inline truncate">
🚀 Option 1
</p>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
<button
type="button"
className="hidden group-hover:inline-block text-custom-text-300 text-xs"
>
Set as default
</button>
<button type="button">
<MoreHorizontal className="text-custom-text-400" size={14} />
</button>
</div>
</div>
);
export const SelectAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<div>
<p className="text-xs">Options</p>
<div className="mt-3 space-y-2">
{/* TODO: map over options */}
<SelectOption />
<SelectOption />
<SelectOption />
</div>
<div className="mt-2 w-3/5">
<div className="bg-custom-background-100 rounded border border-custom-border-200 flex items-center gap-2 px-3 py-2">
<span className="flex-shrink-0 text-xs grid place-items-center">🚀</span>
<input
type="text"
className="flex-grow border-none outline-none placeholder:text-custom-text-400 text-xs"
placeholder="Enter new option"
/>
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button className="grid place-items-center h-3.5 w-3.5 rounded-sm focus:outline-none">
<span className="h-full w-full rounded-sm bg-black" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute bottom-full right-0 z-10 mb-1 px-2 sm:px-0">
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker
color={"#ff0000"}
onChange={(value) => {
onChange(value.hex);
close();
}}
/>
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,78 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
is_required: false,
};
export const TextAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.text;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter default value" value={value} onChange={onChange} />
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<ToggleSwitch value={true} onChange={() => {}} />
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,92 @@
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Disclosure } from "@headlessui/react";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
const defaultFormValues: Partial<ICustomAttribute> = {
default_value: "",
display_name: "",
is_required: false,
};
export const UrlAttributeForm: React.FC<Props> = () => {
const { control } = useForm({ defaultValues: defaultFormValues });
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.url;
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="url"
placeholder="Enter default URL"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<button
type="button"
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
>
Remove
</button>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -0,0 +1,127 @@
import React, { useState } from "react";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
import { Search } from "lucide-react";
type Props = {
onChange: (value: string[]) => void;
value: string[];
};
const FILE_EXTENSIONS: {
[category: string]: string[];
} = {
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff", ".svg", ".eps", ".psd", ".ai"],
video: [".mp4", ".avi", ".mkv", ".mpg", ".mpeg", ".flv", ".wmv"],
audio: [".mp3", ".wav", ".ogg", ".flac", ".aac"],
document: [
".txt",
".doc",
".docx",
".pdf",
".ppt",
".pptx",
".xls",
".xlsx",
".html",
".htm",
".csv",
".xml",
],
};
const searchExtensions = (query: string) => {
query = query.toLowerCase();
const filteredExtensions: {
[category: string]: string[];
} = {};
for (const category in FILE_EXTENSIONS) {
const extensions = FILE_EXTENSIONS[category].filter((extension) =>
extension.toLowerCase().includes(query)
);
if (extensions.length > 0) {
filteredExtensions[category] = extensions;
}
}
return filteredExtensions;
};
export const FileFormatsDropdown: React.FC<Props> = ({ onChange, value }) => {
const [query, setQuery] = useState("");
const options = searchExtensions(query);
return (
<Combobox
as="div"
value={value}
onChange={(val) => {
console.log(val);
onChange(val);
}}
className="relative flex-shrink-0 text-left"
multiple
>
{({ open }: { open: boolean }) => (
<>
<Combobox.Button className="px-3 py-2 bg-custom-background-100 rounded border border-custom-border-200 text-xs w-full text-left">
All Formats
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Combobox.Options className="absolute z-10 bottom-full mb-2 border-[0.5px] border-custom-border-300 p-1 w-full max-h-64 flex flex-col rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none overflow-hidden">
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2 mb-1">
<Search className="text-custom-text-400" size={12} />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="h-full overflow-y-auto">
{Object.keys(options).map((category) =>
options[category].map((extension) => (
<Combobox.Option
key={extension}
value={extension}
className={({ active, selected }) =>
`flex items-center gap-1 cursor-pointer select-none truncate rounded px-1 py-1.5 accent-custom-primary-100 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ active, selected }) => (
<>
<input
type="checkbox"
className="scale-75"
checked={value.includes(extension)}
readOnly
/>
{extension}
</>
)}
</Combobox.Option>
))
)}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
);
};

View File

@ -0,0 +1,2 @@
export * from "./file-formats-dropdown";
export * from "./types-dropdown";

View File

@ -0,0 +1,67 @@
import React from "react";
import { useRouter } from "next/router";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { Plus } from "lucide-react";
// types
import { TCustomAttributeTypes } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {};
export const TypesDropdown: React.FC<Props> = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
return (
<Listbox as="div" className="relative flex-shrink-0 text-left">
{({ open }: { open: boolean }) => (
<>
<Listbox.Button className="flex items-center gap-1 text-xs font-medium text-custom-primary-100">
<Plus size={14} strokeWidth={1.5} />
Add Attribute
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Listbox.Options className="absolute z-10 bottom-full mb-2 border-[0.5px] border-custom-border-300 p-1 min-w-[10rem] rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none">
{Object.keys(CUSTOM_ATTRIBUTES_LIST).map((type) => {
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type as TCustomAttributeTypes];
return (
<Listbox.Option
key={type}
value={type}
className={({ active, selected }) =>
`flex items-center gap-1 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ active, selected }) => (
<>
<typeMetaData.icon size={14} strokeWidth={1.5} />
{typeMetaData.label}
</>
)}
</Listbox.Option>
);
})}
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
);
};

View File

@ -0,0 +1,4 @@
export * from "./attribute-forms";
export * from "./dropdowns";
export * from "./input";
export * from "./object-modal";

View File

@ -0,0 +1,18 @@
import { forwardRef } from "react";
export const Input = forwardRef(
(props: React.InputHTMLAttributes<HTMLInputElement>, ref: React.Ref<HTMLInputElement>) => {
const { className = "", type, ...rest } = props;
return (
<input
ref={ref}
type={type ?? "text"}
className={`placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none h-9 ${className}`}
{...rest}
/>
);
}
);
Input.displayName = "Input";

View File

@ -0,0 +1,113 @@
import React from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
import {
TextAttributeForm,
Input,
TypesDropdown,
NumberAttributeForm,
CheckboxAttributeForm,
RelationAttributeForm,
DateTimeAttributeForm,
UrlAttributeForm,
EmailAttributeForm,
FileAttributeForm,
SelectAttributeForm,
} from "components/custom-attributes";
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
isOpen: boolean;
onClose: () => void;
onSubmit?: () => Promise<void>;
};
export const ObjectModal: React.FC<Props> = ({ isOpen, onClose, onSubmit }) => {
const handleClose = () => {
onClose();
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 h-full w-full z-20">
<div className="flex items-center justify-center h-full w-full p-4 sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="bg-custom-background-100 p-6 w-1/2 h-[85%] flex flex-col rounded-xl">
<h3 className="text-2xl font-semibold">New Object</h3>
<div className="mt-5 space-y-5 h-full overflow-y-auto">
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded text-sm">
🚀
</div>
<Input placeholder="Enter Object Title" />
</div>
<textarea
name="objectDescription"
id="objectDescription"
className="placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none"
cols={30}
rows={5}
placeholder="Enter Object Description"
/>
</div>
<div>
<h4 className="font-medium">Attributes</h4>
<div className="mt-2 space-y-2">
{/* TODO: Map over attributes */}
<TextAttributeForm />
<NumberAttributeForm />
<CheckboxAttributeForm />
<RelationAttributeForm />
<DateTimeAttributeForm />
<UrlAttributeForm />
<EmailAttributeForm />
<FileAttributeForm />
<SelectAttributeForm />
{/* <CUSTOM_ATTRIBUTES_LIST.text.component />
<CUSTOM_ATTRIBUTES_LIST.number.component />
<CUSTOM_ATTRIBUTES_LIST.checkbox.component />
<CUSTOM_ATTRIBUTES_LIST.relation.component />
<CUSTOM_ATTRIBUTES_LIST.datetime.component />
<CUSTOM_ATTRIBUTES_LIST.url.component />
<CUSTOM_ATTRIBUTES_LIST.email.component />
<CUSTOM_ATTRIBUTES_LIST.files.component />
<CUSTOM_ATTRIBUTES_LIST.select.component />
<CUSTOM_ATTRIBUTES_LIST.multi_select.component /> */}
</div>
<div className="mt-3">
<TypesDropdown />
</div>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -20,7 +20,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
className={`relative flex-shrink-0 inline-flex ${ className={`relative flex-shrink-0 inline-flex ${
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11" size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${ } flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-green-500" : "bg-custom-background-80" value ? "bg-custom-primary-100" : "bg-custom-background-80"
} ${className || ""}`} } ${className || ""}`}
> >
<span className="sr-only">{label}</span> <span className="sr-only">{label}</span>

View File

@ -0,0 +1,124 @@
// components
import {
CheckboxAttributeForm,
DateTimeAttributeForm,
EmailAttributeForm,
FileAttributeForm,
NumberAttributeForm,
RelationAttributeForm,
SelectAttributeForm,
TextAttributeForm,
UrlAttributeForm,
} from "components/custom-attributes";
// icons
import {
AtSign,
Baseline,
CaseSensitive,
CheckCircle,
Clock4,
Disc,
FileMinus,
Forward,
Hash,
Link2,
LucideIcon,
} from "lucide-react";
// types
import { TCustomAttributeTypes, TCustomAttributeUnits } from "types";
export const CUSTOM_ATTRIBUTES_LIST: {
[key in Partial<TCustomAttributeTypes>]: {
component: React.FC<any>;
icon: LucideIcon;
label: string;
};
} = {
checkbox: {
component: CheckboxAttributeForm,
icon: CheckCircle,
label: "Checkbox",
},
datetime: {
component: DateTimeAttributeForm,
icon: Clock4,
label: "Date Time",
},
email: {
component: EmailAttributeForm,
icon: AtSign,
label: "Email",
},
files: {
component: FileAttributeForm,
icon: FileMinus,
label: "Files",
},
multi_select: {
component: SelectAttributeForm,
icon: Disc,
label: "Multi Select",
},
number: {
component: NumberAttributeForm,
icon: Hash,
label: "Number",
},
option: {
icon: Baseline,
label: "Option",
},
relation: {
component: RelationAttributeForm,
icon: Forward,
label: "Relation",
},
select: {
component: SelectAttributeForm,
icon: Disc,
label: "Select",
},
text: {
component: TextAttributeForm,
icon: CaseSensitive,
label: "Text",
},
url: {
component: UrlAttributeForm,
icon: Link2,
label: "URL",
},
};
export const CUSTOM_ATTRIBUTE_UNITS: {
label: string;
value: TCustomAttributeUnits;
}[] = [
{
label: "Cycle",
value: "cycle",
},
{
label: "Issue",
value: "issue",
},
{
label: "Module",
value: "module",
},
{
label: "User",
value: "user",
},
];
export const DATE_FORMATS = [
{ label: "Day/Month/Year", value: "DD-MM-YYYY" },
{ label: "Month/Day/Year", value: "MM-DD-YYYY" },
{ label: "Year/Month/Day", value: "YYYY-MM-DD" },
];
export const TIME_FORMATS = [
{ label: "12 Hours", value: "12" },
{ label: "24 Hours", value: "24" },
];

View File

@ -0,0 +1,81 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// hooks
import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
// components
import { SettingsHeader } from "components/project";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { PrimaryButton } from "components/ui";
// icons
import { TableProperties } from "lucide-react";
// types
import type { NextPage } from "next";
// helpers
import { truncateText } from "helpers/string.helper";
import { ObjectModal } from "components/custom-attributes";
const ControlSettings: NextPage = () => {
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { projectDetails } = useProjectDetails();
return (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectId}/issues`}
linkTruncate
/>
<BreadcrumbItem title="Control Settings" unshrinkTitle />
</Breadcrumbs>
}
>
<ObjectModal
isOpen={isCreateObjectModalOpen}
onClose={() => setIsCreateObjectModalOpen(false)}
/>
<div className="p-8">
<SettingsHeader />
<div>
<div className="flex items-center justify-between gap-4">
<h2 className="text-xl font-medium">Custom Objects</h2>
<PrimaryButton onClick={() => setIsCreateObjectModalOpen(true)}>
Add Object
</PrimaryButton>
</div>
<div className="mt-4 border-y border-custom-border-100">
<div className="space-y-4 divide-y divide-custom-border-100">
{/* TODO: Map over objects */}
<div className="p-4 flex items-center justify-between gap-4">
<div className="flex gap-4">
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
<TableProperties size={20} strokeWidth={1.5} />
</div>
<div>
<h5 className="text-sm font-medium">Title</h5>
<p className="text-custom-text-300 text-xs">Description of 100 chars</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ProjectAuthorizationWrapper>
);
};
export default ControlSettings;

View File

@ -0,0 +1,20 @@
<svg width="108" height="80" viewBox="0 0 108 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5492_135824)">
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H105.691C106.886 0.144318 107.856 1.11352 107.856 2.30909V79.8557H0.144318V2.30909Z" fill="white" stroke="#E5E5E5" stroke-width="0.288636"/>
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H106.268C107.145 0.144318 107.856 0.855066 107.856 1.73182V5.85568H0.144318V2.30909Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.288636"/>
<rect x="2.70312" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#EF4444"/>
<rect x="5.00781" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#FCD34D"/>
<rect x="7.32031" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#4ADE80"/>
<mask id="mask0_5492_135824" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="31" y="18" width="47" height="44">
<rect x="31.0547" y="18" width="46.3656" height="44" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_5492_135824)">
<path d="M51.6412 44.8737L47.3894 40.6219C47.136 40.3685 46.8174 40.2389 46.4338 40.233C46.0502 40.2271 45.7258 40.3568 45.4606 40.6219C45.1955 40.8871 45.0629 41.2085 45.0629 41.5863C45.0629 41.964 45.1955 42.2855 45.4606 42.5506L50.4832 47.5732C50.814 47.904 51.2 48.0694 51.6412 48.0694C52.0823 48.0694 52.4683 47.904 52.7991 47.5732L62.9815 37.3908C63.2349 37.1374 63.3645 36.8189 63.3704 36.4352C63.3763 36.0516 63.2466 35.7272 62.9815 35.4621C62.7163 35.1969 62.3949 35.0644 62.0171 35.0644C61.6394 35.0644 61.3179 35.1969 61.0528 35.4621L51.6412 44.8737ZM54.2417 58.3328C51.8369 58.3328 49.5765 57.8764 47.4606 56.9638C45.3445 56.0511 43.5039 54.8125 41.9387 53.248C40.3734 51.6834 39.1342 49.8436 38.2212 47.7285C37.3081 45.6135 36.8516 43.3535 36.8516 40.9488C36.8516 38.544 37.3079 36.2836 38.2206 34.1676C39.1332 32.0516 40.3718 30.2109 41.9364 28.6457C43.501 27.0804 45.3408 25.8413 47.4558 24.9282C49.5709 24.0151 51.8308 23.5586 54.2356 23.5586C56.6404 23.5586 58.9008 24.0149 61.0168 24.9276C63.1328 25.8403 64.9734 27.0789 66.5386 28.6434C68.1039 30.208 69.3431 32.0478 70.2561 34.1629C71.1692 36.2779 71.6258 38.5378 71.6258 40.9426C71.6258 43.3474 71.1694 45.6078 70.2567 47.7238C69.3441 49.8398 68.1055 51.6804 66.5409 53.2457C64.9764 54.8109 63.1365 56.0501 61.0215 56.9632C58.9064 57.8763 56.6465 58.3328 54.2417 58.3328ZM54.2387 55.5875C58.3262 55.5875 61.7883 54.1691 64.6252 51.3322C67.462 48.4954 68.8805 45.0332 68.8805 40.9457C68.8805 36.8582 67.462 33.396 64.6252 30.5592C61.7883 27.7223 58.3262 26.3039 54.2387 26.3039C50.1512 26.3039 46.689 27.7223 43.8521 30.5592C41.0153 33.396 39.5969 36.8582 39.5969 40.9457C39.5969 45.0332 41.0153 48.4954 43.8521 51.3322C46.689 54.1691 50.1512 55.5875 54.2387 55.5875Z" fill="#A3A3A3"/>
</g>
</g>
<defs>
<clipPath id="clip0_5492_135824">
<rect width="108" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,16 @@
<svg width="108" height="80" viewBox="0 0 108 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5492_135841)">
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H105.691C106.886 0.144318 107.856 1.11352 107.856 2.30909V79.8557H0.144318V2.30909Z" fill="white" stroke="#E5E5E5" stroke-width="0.288636"/>
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H106.268C107.145 0.144318 107.856 0.855066 107.856 1.73182V5.85568H0.144318V2.30909Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.288636"/>
<rect x="2.70312" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#EF4444"/>
<rect x="5.00781" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#FCD34D"/>
<rect x="7.32031" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#4ADE80"/>
<rect x="30" y="25" width="48" height="31" rx="15.5" fill="#A3A3A3"/>
<ellipse cx="45.384" cy="40.5005" rx="12.9231" ry="13.02" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5492_135841">
<rect width="108" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,16 @@
<svg width="108" height="80" viewBox="0 0 108 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5160_64883)">
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H105.691C106.886 0.144318 107.856 1.11352 107.856 2.30909V79.8557H0.144318V2.30909Z" fill="white" stroke="#E5E5E5" stroke-width="0.288636"/>
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H106.268C107.145 0.144318 107.856 0.855066 107.856 1.73182V5.85568H0.144318V2.30909Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.288636"/>
<rect x="2.70312" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#EF4444"/>
<rect x="5.00781" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#FCD34D"/>
<rect x="7.32031" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#4ADE80"/>
<path d="M26.5547 40H83.2214" stroke="#D4D4D4" stroke-width="5.66667" stroke-linecap="round"/>
<path d="M26.5547 40H51.1102" stroke="#525252" stroke-width="5.66667" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_5160_64883">
<rect width="108" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,15 @@
<svg width="108" height="80" viewBox="0 0 108 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5160_64788)">
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H105.691C106.886 0.144318 107.856 1.11352 107.856 2.30909V79.8557H0.144318V2.30909Z" fill="white" stroke="#E5E5E5" stroke-width="0.288636"/>
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H106.268C107.145 0.144318 107.856 0.855066 107.856 1.73182V5.85568H0.144318V2.30909Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.288636"/>
<rect x="2.70312" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#EF4444"/>
<rect x="5.00781" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#FCD34D"/>
<rect x="7.32031" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#4ADE80"/>
<path d="M48.8118 30.5455V48H45.6499V33.6222H45.5476L41.4652 36.2301V33.3324L45.8033 30.5455H48.8118ZM53.4836 48V45.7159L59.5433 39.7756C60.1228 39.1903 60.6058 38.6705 60.9922 38.2159C61.3785 37.7614 61.6683 37.321 61.8615 36.8949C62.0547 36.4687 62.1512 36.0142 62.1512 35.5312C62.1512 34.9801 62.0262 34.5085 61.7762 34.1165C61.5262 33.7188 61.1825 33.4119 60.745 33.196C60.3075 32.9801 59.8103 32.8722 59.2535 32.8722C58.6797 32.8722 58.1768 32.9915 57.745 33.2301C57.3132 33.4631 56.978 33.7955 56.7393 34.2273C56.5064 34.6591 56.3899 35.1733 56.3899 35.7699H53.3814C53.3814 34.6619 53.6342 33.6989 54.1399 32.8807C54.6456 32.0625 55.3416 31.429 56.228 30.9801C57.12 30.5312 58.1427 30.3068 59.2961 30.3068C60.4666 30.3068 61.495 30.5256 62.3814 30.9631C63.2677 31.4006 63.9552 32 64.4439 32.7614C64.9382 33.5227 65.1853 34.392 65.1853 35.3693C65.1853 36.0227 65.0603 36.6648 64.8103 37.2955C64.5603 37.9261 64.12 38.625 63.4893 39.392C62.8643 40.1591 61.9865 41.0881 60.8558 42.179L57.8473 45.2386V45.358H65.4495V48H53.4836Z" fill="#737373"/>
</g>
<defs>
<clipPath id="clip0_5160_64788">
<rect width="108" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,24 @@
<svg width="108" height="80" viewBox="0 0 108 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5160_64904)">
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H105.691C106.886 0.144318 107.856 1.11352 107.856 2.30909V79.8557H0.144318V2.30909Z" fill="white" stroke="#E5E5E5" stroke-width="0.288636"/>
<path d="M0.144318 2.30909C0.144318 1.11352 1.11352 0.144318 2.30909 0.144318H106.268C107.145 0.144318 107.856 0.855066 107.856 1.73182V5.85568H0.144318V2.30909Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.288636"/>
<rect x="2.70312" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#EF4444"/>
<rect x="5.00781" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#FCD34D"/>
<rect x="7.32031" y="2.125" width="1.73182" height="1.73182" rx="0.865909" fill="#4ADE80"/>
<g clip-path="url(#clip1_5160_64904)">
<circle cx="54.9411" cy="40.027" r="15.1351" stroke="#E5E5E5" stroke-width="3.78378"/>
<mask id="path-7-inside-1_5160_64904" fill="white">
<path d="M43.9218 53.0058C46.0076 54.7775 48.4897 56.0203 51.1578 56.629C53.8259 57.2377 56.6015 57.1943 59.2492 56.5024C61.897 55.8106 64.3391 54.4908 66.3684 52.6547C68.3978 50.8187 69.9547 48.5205 70.9072 45.955C71.8598 43.3894 72.1799 40.6321 71.8404 37.9165C71.501 35.201 70.512 32.6073 68.9573 30.3551C67.4025 28.103 65.3278 26.2588 62.909 24.9788C60.4901 23.6987 57.7983 23.0206 55.0617 23.0018L55.0362 26.713C57.1764 26.7277 59.2814 27.258 61.1731 28.259C63.0647 29.2601 64.6872 30.7023 65.9031 32.4636C67.1189 34.2248 67.8924 36.2532 68.1578 38.3769C68.4233 40.5005 68.1729 42.6569 67.428 44.6632C66.6831 46.6696 65.4655 48.4668 63.8785 49.9027C62.2915 51.3385 60.3817 52.3707 58.3111 52.9117C56.2404 53.4527 54.0698 53.4867 51.9832 53.0107C49.8967 52.5347 47.9556 51.5627 46.3244 50.1772L43.9218 53.0058Z"/>
</mask>
<path d="M43.9218 53.0058C46.0076 54.7775 48.4897 56.0203 51.1578 56.629C53.8259 57.2377 56.6015 57.1943 59.2492 56.5024C61.897 55.8106 64.3391 54.4908 66.3684 52.6547C68.3978 50.8187 69.9547 48.5205 70.9072 45.955C71.8598 43.3894 72.1799 40.6321 71.8404 37.9165C71.501 35.201 70.512 32.6073 68.9573 30.3551C67.4025 28.103 65.3278 26.2588 62.909 24.9788C60.4901 23.6987 57.7983 23.0206 55.0617 23.0018L55.0362 26.713C57.1764 26.7277 59.2814 27.258 61.1731 28.259C63.0647 29.2601 64.6872 30.7023 65.9031 32.4636C67.1189 34.2248 67.8924 36.2532 68.1578 38.3769C68.4233 40.5005 68.1729 42.6569 67.428 44.6632C66.6831 46.6696 65.4655 48.4668 63.8785 49.9027C62.2915 51.3385 60.3817 52.3707 58.3111 52.9117C56.2404 53.4527 54.0698 53.4867 51.9832 53.0107C49.8967 52.5347 47.9556 51.5627 46.3244 50.1772L43.9218 53.0058Z" fill="#D9D9D9" stroke="#737373" stroke-width="3.78378" mask="url(#path-7-inside-1_5160_64904)"/>
</g>
</g>
<defs>
<clipPath id="clip0_5160_64904">
<rect width="108" height="80" fill="white"/>
</clipPath>
<clipPath id="clip1_5160_64904">
<rect width="70" height="34.0541" fill="white" transform="translate(19 23)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

33
web/types/custom-attributes.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
export type TCustomAttributeTypes =
| "checkbox"
| "datetime"
| "email"
| "entity"
| "files"
| "multi_select"
| "number"
| "option"
| "relation"
| "select"
| "text"
| "url";
export type TCustomAttributeUnits = "cycle" | "issue" | "module" | "user";
export interface ICustomAttribute {
color: string;
default_value: string;
description: string;
display_name: string;
extra_settings: any;
icon: string;
id: string;
is_default: boolean;
is_multi: boolean;
is_required: boolean;
is_shared: boolean;
parent: string;
sort_order: number;
type: TCustomAttributeTypes;
unit: TCustomAttributeUnits;
}

30
web/types/index.d.ts vendored
View File

@ -1,23 +1,23 @@
export * from "./users";
export * from "./workspace";
export * from "./cycles";
export * from "./projects";
export * from "./state";
export * from "./invitation";
export * from "./issues";
export * from "./modules";
export * from "./views";
export * from "./integration";
export * from "./pages";
export * from "./ai";
export * from "./estimate";
export * from "./importer"; export * from "./importer";
export * from "./inbox"; export * from "./ai";
export * from "./analytics"; export * from "./analytics";
export * from "./calendar"; export * from "./calendar";
export * from "./custom-attributes";
export * from "./cycles";
export * from "./estimate";
export * from "./inbox";
export * from "./integration";
export * from "./issues";
export * from "./modules";
export * from "./notifications"; export * from "./notifications";
export * from "./waitlist"; export * from "./pages";
export * from "./projects";
export * from "./reaction"; export * from "./reaction";
export * from "./state";
export * from "./users";
export * from "./views";
export * from "./waitlist";
export * from "./workspace";
export type NestedKeyOf<ObjectType extends object> = { export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object