(frontend): add switch-case-size helper for modal size management

♻️ (frontend): refactor BaseModal to use switchCaseModalSize helper
 (frontend): add GeneralPageHeaderComponent for settings page header
 (frontend): add PasswordFormComponent for password management in settings page

 (SettingsPage): add ProfileGradientFormComponent to allow users to choose profile gradient
 (SettingsPage): add StoreApiKeyFormComponent to enable users to store API keys

♻️ (GeneralPage): refactor GeneralPage to use modular components for forms
This commit is contained in:
cristhianzl 2024-06-05 09:38:03 -03:00
commit 14a7e2835e
7 changed files with 387 additions and 273 deletions

View file

@ -0,0 +1,67 @@
export const switchCaseModalSize = (size: string) => {
let minWidth: string;
let height: string;
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
height = "h-[40vh]";
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
height = "h-[60vh]";
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
height = "h-[80vh]";
break;
case "three-cards":
minWidth = "min-w-[1066px]";
height = "h-fit";
break;
case "large-thin":
minWidth = "min-w-[65vw]";
height = "h-[80vh]";
break;
case "md-thin":
minWidth = "min-w-[85vw]";
height = "h-[70vh]";
break;
case "sm-thin":
minWidth = "min-w-[65vw]";
height = "h-[70vh]";
break;
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
}
return { minWidth, height };
};

View file

@ -16,10 +16,10 @@ import {
} from "../../components/ui/dialog-with-no-close";
import { DialogClose } from "@radix-ui/react-dialog";
import ForwardedIconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
@ -148,71 +148,7 @@ function BaseModal({
(child) => (child as React.ReactElement).type === Footer,
);
let minWidth: string;
let height: string;
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
height = "h-[40vh]";
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
height = "h-[60vh]";
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
height = "h-[80vh]";
break;
case "three-cards":
minWidth = "min-w-[1066px]";
height = "h-fit";
break;
case "large-thin":
minWidth = "min-w-[65vw]";
height = "h-[80vh]";
break;
case "md-thin":
minWidth = "min-w-[85vw]";
height = "h-[70vh]";
break;
case "sm-thin":
minWidth = "min-w-[65vw]";
height = "h-[70vh]";
break;
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
}
let { minWidth, height } = switchCaseModalSize(size);
useEffect(() => {
if (onChangeOpenModal) {

View file

@ -0,0 +1,23 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
const GeneralPageHeaderComponent = () => {
return (
<>
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
General
<ForwardedIconComponent
name="SlidersHorizontal"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage settings related to Langflow and your account.
</p>
</div>
</div>
</>
);
};
export default GeneralPageHeaderComponent;

View file

@ -0,0 +1,93 @@
import * as Form from "@radix-ui/react-form";
import InputComponent from "../../../../../../components/inputComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
type PasswordFormComponentProps = {
password: string;
cnfPassword: string;
handleInput: (event: any) => void;
handlePatchPassword: (
password: string,
cnfPassword: string,
handleInput: any,
) => void;
};
const PasswordFormComponent = ({
password,
cnfPassword,
handleInput,
handlePatchPassword,
}: PasswordFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
handlePatchPassword(password, cnfPassword, handleInput);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message match="valueMissing" className="field-invalid">
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<Form.Message className="field-invalid" match="valueMissing">
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default PasswordFormComponent;

View file

@ -0,0 +1,68 @@
import * as Form from "@radix-ui/react-form";
import GradientChooserComponent from "../../../../../../components/gradientChooserComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
import { gradients } from "../../../../../../utils/styleUtils";
type ProfileGradientFormComponentProps = {
gradient: string;
handleInput: (event: any) => void;
handlePatchGradient: (gradient: string) => void;
userData: any;
};
const ProfileGradientFormComponent = ({
gradient,
handleInput,
handlePatchGradient,
userData,
}: ProfileGradientFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
handlePatchGradient(gradient);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: gradient
}
onChange={(value) => {
handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default ProfileGradientFormComponent;

View file

@ -0,0 +1,102 @@
import * as Form from "@radix-ui/react-form";
import InputComponent from "../../../../../../components/inputComponent";
import { Button } from "../../../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../../../components/ui/card";
import {
CREATE_API_KEY,
INSERT_API_KEY,
INVALID_API_KEY,
NO_API_KEY,
} from "../../../../../../constants/constants";
type StoreApiKeyFormComponentProps = {
apikey: string;
handleInput: (event: any) => void;
handleSaveKey: (apikey: string, handleInput: any) => void;
loadingApiKey: boolean;
validApiKey: boolean;
hasApiKey: boolean;
};
const StoreApiKeyFormComponent = ({
apikey,
handleInput,
handleSaveKey,
loadingApiKey,
validApiKey,
hasApiKey,
}: StoreApiKeyFormComponentProps) => {
return (
<>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
handleSaveKey(apikey, handleInput);
}}
>
<Card x-chunk="dashboard-04-chunk-2" id="api">
<CardHeader>
<CardTitle>Store API Key</CardTitle>
<CardDescription>
{(hasApiKey && !validApiKey
? INVALID_API_KEY
: !hasApiKey
? NO_API_KEY
: "") + INSERT_API_KEY}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full flex-col gap-3">
<div className="flex w-full gap-4">
<Form.Field name="apikey" className="w-full">
<InputComponent
id="apikey"
onChange={(value) => {
handleInput({ target: { name: "apikey", value } });
}}
value={apikey}
isForm
password={true}
placeholder="Insert your API Key"
className="w-full"
/>
<Form.Message match="valueMissing" className="field-invalid">
Please enter your API Key
</Form.Message>
</Form.Field>
</div>
<span className="pr-1 text-xs text-muted-foreground">
{CREATE_API_KEY}{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button
loading={loadingApiKey}
type="submit"
data-testid="api-key-save-button-store"
>
Save
</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
</>
);
};
export default StoreApiKeyFormComponent;

View file

@ -1,25 +1,6 @@
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import { useParams } from "react-router-dom";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import GradientChooserComponent from "../../../../components/gradientChooserComponent";
import InputComponent from "../../../../components/inputComponent";
import { Button } from "../../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../../../components/ui/card";
import {
CONTROL_PATCH_USER_STATE,
CREATE_API_KEY,
INSERT_API_KEY,
INVALID_API_KEY,
NO_API_KEY,
} from "../../../../constants/constants";
import { CONTROL_PATCH_USER_STATE } from "../../../../constants/constants";
import { AuthContext } from "../../../../contexts/authContext";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
@ -28,21 +9,24 @@ import {
inputHandlerEventType,
patchUserInputStateType,
} from "../../../../types/components";
import { gradients } from "../../../../utils/styleUtils";
import usePatchGradient from "../hooks/use-patch-gradient";
import usePatchPassword from "../hooks/use-patch-password";
import useSaveKey from "../hooks/use-save-key";
import useScrollToElement from "../hooks/use-scroll-to-element";
import GeneralPageHeaderComponent from "./components/GeneralPageHeader";
import PasswordFormComponent from "./components/PasswordForm";
import ProfileGradientFormComponent from "./components/ProfileGradientForm";
import StoreApiKeyFormComponent from "./components/StoreApiKeyForm";
export default function GeneralPage() {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
(state) => state.setCurrentFlowId,
);
const { scrollId } = useParams();
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE
CONTROL_PATCH_USER_STATE,
);
const { autoLogin } = useContext(AuthContext);
@ -63,14 +47,14 @@ export default function GeneralPage() {
const { handlePatchPassword } = usePatchPassword(
userData,
setSuccessData,
setErrorData
setErrorData,
);
const { handlePatchGradient } = usePatchGradient(
setSuccessData,
setErrorData,
userData,
setUserData
setUserData,
);
useScrollToElement(scrollId, setCurrentFlowId);
@ -80,7 +64,7 @@ export default function GeneralPage() {
setErrorData,
setHasApiKey,
setValidApiKey,
setLoadingApiKey
setLoadingApiKey,
);
function handleInput({
@ -91,192 +75,33 @@ export default function GeneralPage() {
return (
<div className="flex h-full w-full flex-col gap-6">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
General
<ForwardedIconComponent
name="SlidersHorizontal"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage settings related to Langflow and your account.
</p>
</div>
</div>
<GeneralPageHeaderComponent />
<div className="grid gap-6">
<Form.Root
onSubmit={(event) => {
handlePatchGradient(gradient);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: gradient
}
onChange={(value) => {
handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
{!autoLogin && (
<Form.Root
onSubmit={(event) => {
handlePatchPassword(password, cnfPassword, handleInput);
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message
match="valueMissing"
className="field-invalid"
>
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<ProfileGradientFormComponent
gradient={gradient}
handleInput={handleInput}
handlePatchGradient={handlePatchGradient}
userData={userData}
/>
<Form.Message
className="field-invalid"
match="valueMissing"
>
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
{!autoLogin && (
<PasswordFormComponent
password={password}
cnfPassword={cnfPassword}
handleInput={handleInput}
handlePatchPassword={handlePatchPassword}
/>
)}
{hasStore && (
<Form.Root
onSubmit={(event) => {
event.preventDefault();
handleSaveKey(apikey, handleInput);
}}
>
<Card x-chunk="dashboard-04-chunk-2" id="api">
<CardHeader>
<CardTitle>Store API Key</CardTitle>
<CardDescription>
{(hasApiKey && !validApiKey
? INVALID_API_KEY
: !hasApiKey
? NO_API_KEY
: "") + INSERT_API_KEY}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full flex-col gap-3">
<div className="flex w-full gap-4">
<Form.Field name="apikey" className="w-full">
<InputComponent
id="apikey"
onChange={(value) => {
handleInput({ target: { name: "apikey", value } });
}}
value={apikey}
isForm
password={true}
placeholder="Insert your API Key"
className="w-full"
/>
<Form.Message
match="valueMissing"
className="field-invalid"
>
Please enter your API Key
</Form.Message>
</Form.Field>
</div>
<span className="pr-1 text-xs text-muted-foreground">
{CREATE_API_KEY}{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button
loading={loadingApiKey}
type="submit"
data-testid="api-key-save-button-store"
>
Save
</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
<StoreApiKeyFormComponent
apikey={apikey}
handleInput={handleInput}
handleSaveKey={handleSaveKey}
loadingApiKey={loadingApiKey}
validApiKey={validApiKey}
hasApiKey={hasApiKey}
/>
)}
</div>
</div>