Skip to content

Commit

Permalink
✨ feat(ui): wrap select input
Browse files Browse the repository at this point in the history
  • Loading branch information
thrownullexception committed Jun 20, 2024
1 parent a3d07c9 commit d45f9f3
Show file tree
Hide file tree
Showing 35 changed files with 480 additions and 415 deletions.
58 changes: 0 additions & 58 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
],
"bin": "./bin/dashpress",
"dependencies": {
"@dashpress/bacteria": "^0.0.13",
"@dashpress/bacteria": "^0.0.14",
"@lingui/core": "^4.10.0",
"@lingui/react": "^4.10.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
Expand Down Expand Up @@ -85,7 +85,6 @@
"ramda": "0.27.1",
"randomstring": "^1.2.2",
"react": "18.2.0",
"react-datepicker": "^4.8.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.1",
Expand Down Expand Up @@ -141,7 +140,6 @@
"@types/qs": "^6.9.7",
"@types/ramda": "0.27.40",
"@types/react": "^18.0.11",
"@types/react-datepicker": "^4.4.1",
"@types/react-dom": "^18.0.4",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function Alert({ type, message, renderJsx, action }: IProps) {
<IconCmp size={24} />
</div>
<div className="w-full self-center my-3">
<p className="text-sm" style={{ color: hexColor }}>
<p className="text-sm !mb-0" style={{ color: hexColor }}>
{(renderJsx ? message : getBestErrorMessage(message)) as string}
</p>
{action && (
Expand Down
8 changes: 3 additions & 5 deletions src/components/app/form/input/Stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ import { FormInput } from "./text";
import { FormNumberInput } from "./number";
import { FormRichTextArea } from "../../../../frontend/design-system/components/Form/RichText";
import { FormTextArea } from "./textarea";
import {
FormMultiSelect,
FormSelect,
} from "../../../../frontend/design-system/components/Form/Select";
import { FormMultiSelect } from "../../../../frontend/design-system/components/Form/Select";
import { FormCodeEditor } from "../../../../frontend/design-system/components/Form/CodeEditor";
import { AsyncFormSelect } from "../../../../frontend/design-system/components/Form/Select/Async";
import { FormSwitch } from "./switch";
import { FormFileInput } from "../../../../frontend/design-system/components/Form/File";
import { FormSelectButton } from "./select-button";
Expand All @@ -24,6 +20,8 @@ import { FormPasswordInput } from "./password";
import { FormButton } from "../../button/form";
import { ActionButtons } from "../../button/action";
import { FormDateInput } from "./date";
import { AsyncFormSelect } from "./select-async";
import { FormSelect } from "./select";

function DemoForm() {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/app/form/input/date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function ControlledFormDateInput({
</Popover>
);
}
// TODO fix date
// TODO fix date with timezone
export function FormDateInput(formInput: IFormDateInput) {
const { input, disabled, meta, minDate, maxDate } = formInput;
let { value } = input;
Expand Down
3 changes: 2 additions & 1 deletion src/components/app/form/input/icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { Field } from "react-final-form";
import { SystemIconsKeys, SystemIconsList } from "shared/constants/Icons";
import { required } from "frontend/lib/validations";
import { userFriendlyCase } from "shared/lib/strings/friendly-case";
import { FormSelect } from "frontend/design-system/components/Form/Select";
import { msg } from "@lingui/macro";
import { FormTextArea } from "../textarea";
import { FormSelect } from "../select";

export function IconInputField() {
return (
<Field name="icon" validateFields={[]} validate={required}>
{({ input, meta }) =>
SystemIconsList.includes(input.value as SystemIconsKeys) ? (
// TODO render the icons and add search
<FormSelect
label={msg`Icon`}
required
Expand Down
41 changes: 41 additions & 0 deletions src/components/app/form/input/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import { Loader, Search } from "react-feather";
import { useState } from "react";
import { Input } from "@/components/ui/input";

interface IProps {
onChange: (value: string) => void;
loading?: boolean;
}

export function FormSearch({ onChange, loading }: IProps) {
const { _ } = useLingui();

const [value, setValue] = useState("");

return (
<div className="relative flex w-full">
<button
className="text-primary pl-3 border-b border-border"
type="button"
>
{loading ? (
<Loader className="animate-spin mr-2 h-4 w-4 shrink-0" />
) : (
<Search className="mr-2 h-4 w-4 shrink-0" />
)}
</button>
<Input
className="rounded-none focus-visible:ring-0 px-1 border-t-0 border-x-0"
type="search"
value={value}
onChange={(e) => {
onChange(e.target.value.toLowerCase());
setValue(e.target.value);
}}
placeholder={_(msg`Search`)}
/>
</div>
);
}
88 changes: 88 additions & 0 deletions src/components/app/form/input/select-async.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useState } from "react";
import { useAsync, useDebounce } from "react-use";
import { ISelectData } from "shared/types/options";
import { useApi } from "frontend/lib/data/useApi";
import { ApiRequest } from "frontend/lib/data/makeRequest";
import { ErrorAlert } from "@/components/app/alert";
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";
import { FormSelect } from "./select";

interface IProps extends IBaseFormSelect {
url: string;
referenceUrl?: (value: string) => string;
limit?: number;
}

export function AsyncFormSelect(props: IProps) {
const { input, url, referenceUrl, limit = 50 } = props;

const [search, setSearch] = useState("");
const [debounceSearch, setDebounceSearch] = useState("");

const fullData = useApi<ISelectData[]>(url, {
defaultData: [],
});

const selectOptions = useApi<ISelectData[]>(
debounceSearch ? `${url}?search=${debounceSearch}` : url,
{
defaultData: [],
}
);

const valueLabelToUse = useAsync(async () => {
if (!input.value) {
return undefined;
}
const isValueInFirstDataLoad = fullData.data.find(
({ value }: ISelectData) => value === input.value
);

if (isValueInFirstDataLoad) {
return isValueInFirstDataLoad.label;
}

const isValueInSelectionOptions = selectOptions.data.find(
({ value }: ISelectData) => value === input.value
);

if (isValueInSelectionOptions) {
return isValueInSelectionOptions.label;
}

if (!referenceUrl) {
return undefined; // or the value
}

return await ApiRequest.GET(referenceUrl(input.value));
}, [url, fullData.isLoading]);

useDebounce(
() => {
setDebounceSearch(search);
},
700,
[search]
);

if (fullData.error || selectOptions.error) {
return <ErrorAlert message={fullData.error || selectOptions.error} />;
}

if (fullData.data.length >= limit) {
return (
<FormSelect
{...props}
selectData={selectOptions.data}
onSearch={{
isLoading: selectOptions.isLoading,
onChange: setSearch,
value: search,
valueLabel: valueLabelToUse.value,
}}
/>
);
}

return <FormSelect {...props} selectData={fullData.data} />;
}
39 changes: 10 additions & 29 deletions src/components/app/form/input/select-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import styled from "styled-components";
import { sluggify } from "shared/lib/strings";
import { ISelectData } from "shared/types/options";
import { useLingui } from "@lingui/react";
Expand All @@ -8,28 +7,6 @@ import { cn } from "@/lib/utils";
import { LabelAndError } from "./label-and-error";
import { IBaseFormSelect } from "@/frontend/design-system/components/Form/Select/types";

const Input = styled.input`
position: absolute;
clip: rect(0, 0, 0, 0);
pointer-events: none;
`;

const Root = styled.div`
position: relative;
display: inline-flex;
vertical-align: middle;
& > button:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
& > button:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-left: -1px;
}
`;

interface IFormSelect extends IBaseFormSelect {
selectData: ISelectData[];
size?: VariantProps<typeof buttonVariants>["size"];
Expand All @@ -41,7 +18,7 @@ export function FormSelectButton(formInput: IFormSelect) {
const { _ } = useLingui();
return (
<LabelAndError formInput={formInput}>
<Root>
<div className="inline-flex">
{selectData.map(({ value, label }, index) => {
const isChecked =
input.value === value || (index === 0 && input.value === undefined);
Expand All @@ -55,13 +32,17 @@ export function FormSelectButton(formInput: IFormSelect) {
aria-selected={isChecked}
disabled={disabled}
key={`${value}`}
className={cn({
"bg-primary text-primary-text": isChecked,
})}
className={cn(
"rounded-none border-l-0 last:rounded-r-sm first:rounded-l-sm first:border-l",
{
"bg-primary text-primary-text": isChecked,
}
)}
onClick={() => input.onChange(value)}
>
<Input
<input
type="radio"
className="sr-only pointer-events-none"
name={`${input.name}__${sluggify(
// eslint-disable-next-line no-nested-ternary
typeof value === "boolean"
Expand All @@ -77,7 +58,7 @@ export function FormSelectButton(formInput: IFormSelect) {
</Button>
);
})}
</Root>
</div>
</LabelAndError>
);
}
Loading

0 comments on commit d45f9f3

Please sign in to comment.