refactor: improve store and collection card components (#3350)
* refactor: add card store component * refactor: use StoreCardComponent for displaying store cards * refactor: Remove unused code * refactor: Update Collection card to fit only to the my collection case * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
642df45bf6
commit
981f314a73
10 changed files with 399 additions and 454 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { FlowType } from "@/types/flow";
|
||||||
import { storeComponent } from "../../../../types/store";
|
import { storeComponent } from "../../../../types/store";
|
||||||
import { cn } from "../../../../utils/utils";
|
import { cn } from "../../../../utils/utils";
|
||||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||||
import { Card, CardHeader, CardTitle } from "../../../ui/card";
|
import { Card, CardHeader, CardTitle } from "../../../ui/card";
|
||||||
|
|
||||||
export default function DragCardComponent({ data }: { data: storeComponent }) {
|
export default function DragCardComponent({ data }: { data: FlowType }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { FlowType } from "@/types/flow";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||||
import { storeComponent } from "../../../types/store";
|
import { storeComponent } from "../../../types/store";
|
||||||
import DragCardComponent from "../components/dragCardComponent";
|
import DragCardComponent from "../components/dragCardComponent";
|
||||||
|
|
||||||
const useDragStart = (data: storeComponent) => {
|
const useDragStart = (data: FlowType) => {
|
||||||
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
||||||
|
|
||||||
const onDragStart = useCallback(
|
const onDragStart = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
import { usePostLikeComponent } from "@/controllers/API/queries/store";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Control } from "react-hook-form";
|
import { Control } from "react-hook-form";
|
||||||
import { getComponent } from "../../controllers/API";
|
|
||||||
import IOModal from "../../modals/IOModal";
|
import IOModal from "../../modals/IOModal";
|
||||||
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
|
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
import { useStoreStore } from "../../stores/storeStore";
|
|
||||||
import { FlowType } from "../../types/flow";
|
import { FlowType } from "../../types/flow";
|
||||||
import { storeComponent } from "../../types/store";
|
import { getInputsAndOutputs } from "../../utils/storeUtils";
|
||||||
import cloneFLowWithParent, {
|
|
||||||
getInputsAndOutputs,
|
|
||||||
} from "../../utils/storeUtils";
|
|
||||||
import { cn } from "../../utils/utils";
|
import { cn } from "../../utils/utils";
|
||||||
import IconComponent from "../genericIconComponent";
|
import IconComponent from "../genericIconComponent";
|
||||||
import ShadTooltip from "../shadTooltipComponent";
|
import ShadTooltip from "../shadTooltipComponent";
|
||||||
|
|
@ -26,57 +19,28 @@ import {
|
||||||
import { Checkbox } from "../ui/checkbox";
|
import { Checkbox } from "../ui/checkbox";
|
||||||
import { FormControl, FormField } from "../ui/form";
|
import { FormControl, FormField } from "../ui/form";
|
||||||
import Loading from "../ui/loading";
|
import Loading from "../ui/loading";
|
||||||
import useDataEffect from "./hooks/use-data-effect";
|
|
||||||
import useInstallComponent from "./hooks/use-handle-install";
|
|
||||||
import useDragStart from "./hooks/use-on-drag-start";
|
import useDragStart from "./hooks/use-on-drag-start";
|
||||||
import { convertTestName } from "./utils/convert-test-name";
|
import { convertTestName } from "./utils/convert-test-name";
|
||||||
|
|
||||||
export default function CollectionCardComponent({
|
export default function CollectionCardComponent({
|
||||||
data,
|
data,
|
||||||
authorized = true,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
button,
|
|
||||||
onClick,
|
onClick,
|
||||||
onDelete,
|
|
||||||
playground,
|
|
||||||
control,
|
control,
|
||||||
}: {
|
}: {
|
||||||
data: storeComponent;
|
data: FlowType;
|
||||||
authorized?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
button?: JSX.Element;
|
|
||||||
playground?: boolean;
|
|
||||||
onDelete?: () => void;
|
|
||||||
control?: Control<any, any>;
|
control?: Control<any, any>;
|
||||||
}) {
|
}) {
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
|
|
||||||
const isStore = false;
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false);
|
|
||||||
const [likesCount, setLikesCount] = useState(data?.liked_by_count ?? 0);
|
|
||||||
const [downloadsCount, setDownloadsCount] = useState(
|
|
||||||
data?.downloads_count ?? 0,
|
|
||||||
);
|
|
||||||
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||||
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
|
||||||
const [openPlayground, setOpenPlayground] = useState(false);
|
const [openPlayground, setOpenPlayground] = useState(false);
|
||||||
const [loadingPlayground, setLoadingPlayground] = useState(false);
|
const [loadingPlayground, setLoadingPlayground] = useState(false);
|
||||||
|
|
||||||
const selectedFlowsComponentsCards = useFlowsManagerStore(
|
const selectedFlowsComponentsCards = useFlowsManagerStore(
|
||||||
(state) => state.selectedFlowsComponentsCards,
|
(state) => state.selectedFlowsComponentsCards,
|
||||||
);
|
);
|
||||||
|
|
||||||
const name = data.is_component ? "Component" : "Flow";
|
|
||||||
|
|
||||||
async function getFlowData() {
|
|
||||||
const res = await getComponent(data.id);
|
|
||||||
const newFlow = cloneFLowWithParent(res, res.id, data.is_component, true);
|
|
||||||
return newFlow;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPlayground(flow?: FlowType) {
|
function hasPlayground(flow?: FlowType) {
|
||||||
if (!flow) {
|
if (!flow) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -84,52 +48,7 @@ export default function CollectionCardComponent({
|
||||||
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
|
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
|
||||||
return inputs.length > 0 || outputs.length > 0;
|
return inputs.length > 0 || outputs.length > 0;
|
||||||
}
|
}
|
||||||
|
const playground = !(data.is_component ?? false);
|
||||||
useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount);
|
|
||||||
|
|
||||||
const { handleInstall } = useInstallComponent(
|
|
||||||
data,
|
|
||||||
name,
|
|
||||||
isStore,
|
|
||||||
downloadsCount,
|
|
||||||
setDownloadsCount,
|
|
||||||
setLoading,
|
|
||||||
setSuccessData,
|
|
||||||
setErrorData,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutate, isPending } = usePostLikeComponent();
|
|
||||||
|
|
||||||
const handleLikeWMutate = () => {
|
|
||||||
if (likedByUser !== undefined || likedByUser !== null) {
|
|
||||||
const temp = likedByUser;
|
|
||||||
const tempNum = likesCount;
|
|
||||||
setLikedByUser((prev) => !prev);
|
|
||||||
setLikesCount((prev) => (temp ? prev - 1 : prev + 1));
|
|
||||||
mutate(
|
|
||||||
{ componentId: data.id },
|
|
||||||
{
|
|
||||||
onSuccess: (res) => {
|
|
||||||
setLikesCount(res.data.likes_count);
|
|
||||||
setLikedByUser(res.data.liked_by_user);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setLikesCount(tempNum);
|
|
||||||
setLikedByUser(temp);
|
|
||||||
if (error.response.status === 403) {
|
|
||||||
return setValidApiKey(false);
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
setErrorData({
|
|
||||||
title: `Error liking ${name}.`,
|
|
||||||
list: [error.response.data.detail],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectedCard =
|
const isSelectedCard =
|
||||||
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
|
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
|
||||||
|
|
||||||
|
|
@ -165,51 +84,9 @@ export default function CollectionCardComponent({
|
||||||
)}
|
)}
|
||||||
name={data.is_component ? "ToyBrick" : "Group"}
|
name={data.is_component ? "ToyBrick" : "Group"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShadTooltip content={data.name}>
|
<ShadTooltip content={data.name}>
|
||||||
<div className="w-full truncate pr-3">{data.name}</div>
|
<div className="w-full truncate pr-3">{data.name}</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
{data?.metadata !== undefined && (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{data.private && (
|
|
||||||
<ShadTooltip content="Private">
|
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<IconComponent name="Lock" className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
|
||||||
{!data.is_component && (
|
|
||||||
<ShadTooltip content="Components">
|
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<IconComponent name="ToyBrick" className="h-4 w-4" />
|
|
||||||
<span data-testid={`total-${data.name}`}>
|
|
||||||
{data?.metadata?.total ?? 0}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
|
||||||
<ShadTooltip content="Likes">
|
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<IconComponent name="Heart" className={cn("h-4 w-4")} />
|
|
||||||
<span data-testid={`likes-${data.name}`}>
|
|
||||||
{likesCount ?? 0}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
<ShadTooltip content="Downloads">
|
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
||||||
<IconComponent
|
|
||||||
name="DownloadCloud"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
<span data-testid={`downloads-${data.name}`}>
|
|
||||||
{downloadsCount ?? 0}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{control && (
|
{control && (
|
||||||
<div
|
<div
|
||||||
className="flex"
|
className="flex"
|
||||||
|
|
@ -238,200 +115,19 @@ export default function CollectionCardComponent({
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{data.user_created && data.user_created.username && (
|
|
||||||
<span className="text-sm text-primary">
|
|
||||||
by <b>{data.user_created.username}</b>
|
|
||||||
{data.last_tested_version && (
|
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
|{" "}
|
|
||||||
<span className="text-xs">
|
|
||||||
{" "}
|
|
||||||
⛓︎ v{data.last_tested_version}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="flex w-full flex-1 flex-wrap gap-2"></div>
|
<div className="flex w-full flex-1 flex-wrap gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardDescription className="pb-2 pt-2">
|
<CardDescription className="pb-2 pt-2">
|
||||||
<div
|
<div className="truncate-doubleline">{data.description}</div>
|
||||||
className={
|
|
||||||
data?.metadata !== undefined
|
|
||||||
? "truncate"
|
|
||||||
: "truncate-doubleline"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data.description}
|
|
||||||
</div>
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<div className="z-50 flex w-full items-center justify-between gap-2">
|
<div className="z-50 flex w-full items-center justify-between gap-2">
|
||||||
<div className="flex w-full flex-wrap items-end justify-end gap-2">
|
<div className="flex w-full flex-wrap items-end justify-end gap-2">
|
||||||
{playground && data?.metadata !== undefined ? (
|
{playground && (
|
||||||
<Button
|
<Button
|
||||||
disabled={loadingPlayground || !authorized}
|
disabled={loadingPlayground || !hasPlayground(data)}
|
||||||
key={data.id}
|
|
||||||
tabIndex={-1}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="z-50 gap-2 whitespace-nowrap"
|
|
||||||
data-testid={"playground-flow-button-" + data.id}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setLoadingPlayground(true);
|
|
||||||
const flow = getFlowById(data.id);
|
|
||||||
if (flow) {
|
|
||||||
if (!hasPlayground(flow)) {
|
|
||||||
setErrorData({
|
|
||||||
title: "Error",
|
|
||||||
list: ["This flow doesn't have a playground."],
|
|
||||||
});
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentFlow(flow);
|
|
||||||
setOpenPlayground(true);
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
} else {
|
|
||||||
getFlowData().then((res) => {
|
|
||||||
if (!hasPlayground(res)) {
|
|
||||||
setErrorData({
|
|
||||||
title: "Error",
|
|
||||||
list: ["This flow doesn't have a playground."],
|
|
||||||
});
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentFlow(res);
|
|
||||||
setOpenPlayground(true);
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!loadingPlayground ? (
|
|
||||||
<IconComponent
|
|
||||||
name="BotMessageSquareIcon"
|
|
||||||
className="h-4 w-4 select-none"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loading className="h-4 w-4 text-medium-indigo" />
|
|
||||||
)}
|
|
||||||
Playground
|
|
||||||
</Button>
|
|
||||||
) : undefined}
|
|
||||||
{data.liked_by_count != undefined && (
|
|
||||||
<div className="flex gap-0.5">
|
|
||||||
{onDelete && data?.metadata !== undefined ? (
|
|
||||||
<ShadTooltip
|
|
||||||
content={
|
|
||||||
authorized ? "Delete" : "Please review your API key."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DeleteConfirmationModal onConfirm={onDelete}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={
|
|
||||||
"whitespace-nowrap" +
|
|
||||||
(!authorized ? " cursor-not-allowed" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
data-testid={`delete-${convertTestName(data.name)}`}
|
|
||||||
name="Trash2"
|
|
||||||
className={cn(
|
|
||||||
"h-5 w-5",
|
|
||||||
!authorized ? "text-ring" : "",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</DeleteConfirmationModal>
|
|
||||||
</ShadTooltip>
|
|
||||||
) : (
|
|
||||||
<ShadTooltip
|
|
||||||
content={
|
|
||||||
authorized ? "Like" : "Please review your API key."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={isPending}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={
|
|
||||||
"whitespace-nowrap" +
|
|
||||||
(!authorized ? " cursor-not-allowed" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (!authorized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleLikeWMutate();
|
|
||||||
}}
|
|
||||||
data-testid={`like-${data.name}`}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name="Heart"
|
|
||||||
className={cn(
|
|
||||||
"h-5 w-5",
|
|
||||||
likedByUser
|
|
||||||
? "fill-destructive stroke-destructive"
|
|
||||||
: "",
|
|
||||||
!authorized ? "text-ring" : "",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
)}
|
|
||||||
<ShadTooltip
|
|
||||||
content={
|
|
||||||
authorized
|
|
||||||
? isStore
|
|
||||||
? "Download"
|
|
||||||
: "Install Locally"
|
|
||||||
: "Please review your API key."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={loading}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={
|
|
||||||
"whitespace-nowrap" +
|
|
||||||
(!authorized ? " cursor-not-allowed" : "") +
|
|
||||||
(!loading ? " p-0.5" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (loading || !authorized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleInstall();
|
|
||||||
}}
|
|
||||||
data-testid={`install-${data.name}`}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name={
|
|
||||||
loading ? "Loader2" : isStore ? "Download" : "Plus"
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
|
|
||||||
!authorized ? "text-ring" : "",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ShadTooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{playground && data?.metadata === undefined && (
|
|
||||||
<Button
|
|
||||||
disabled={loadingPlayground || !authorized}
|
|
||||||
key={data.id}
|
key={data.id}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -456,18 +152,9 @@ export default function CollectionCardComponent({
|
||||||
setOpenPlayground(true);
|
setOpenPlayground(true);
|
||||||
setLoadingPlayground(false);
|
setLoadingPlayground(false);
|
||||||
} else {
|
} else {
|
||||||
getFlowData().then((res) => {
|
|
||||||
if (!hasPlayground(res)) {
|
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
list: ["This flow doesn't have a playground."],
|
list: ["Error getting flow data."],
|
||||||
});
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentFlow(res);
|
|
||||||
setOpenPlayground(true);
|
|
||||||
setLoadingPlayground(false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
import useAddFlow from "@/hooks/flows/use-add-flow";
|
|
||||||
import emojiRegex from "emoji-regex";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { FlowType } from "../../types/flow";
|
|
||||||
import { updateIds } from "../../utils/reactflowUtils";
|
|
||||||
import { cn } from "../../utils/utils";
|
|
||||||
import IconComponent from "../genericIconComponent";
|
|
||||||
import ShadTooltip from "../shadTooltipComponent";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "../ui/card";
|
|
||||||
|
|
||||||
export default function CollectionCardComponent({
|
|
||||||
flow,
|
|
||||||
}: {
|
|
||||||
flow: FlowType;
|
|
||||||
authorized?: boolean;
|
|
||||||
}) {
|
|
||||||
const addFlow = useAddFlow();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const isEmoji = (str: string) => emojiRegex().test(str);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"group relative flex h-48 w-2/6 flex-col justify-between overflow-hidden transition-all hover:shadow-md",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
|
|
||||||
{flow.icon && isEmoji(flow.icon) && (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-center rounded-md p-2 align-middle"
|
|
||||||
style={{ backgroundColor: flow.icon_bg_color }}
|
|
||||||
>
|
|
||||||
<div className="h-7 w-7 pl-0.5">{flow.icon}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(!flow.icon || !isEmoji(flow.icon)) && (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-center rounded-md p-2 align-middle"
|
|
||||||
style={{ backgroundColor: flow.icon_bg_color }}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
className={cn("h-7 w-7 flex-shrink-0 text-flow-icon")}
|
|
||||||
name={flow.icon || "Group"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ShadTooltip content={flow.name}>
|
|
||||||
<div className="w-full truncate">{flow.name}</div>
|
|
||||||
</ShadTooltip>
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription className="pb-2 pt-2">
|
|
||||||
<ShadTooltip
|
|
||||||
side="bottom"
|
|
||||||
styleClasses="z-50"
|
|
||||||
content={flow.description}
|
|
||||||
>
|
|
||||||
<div className="truncate-doubleline">{flow.description}</div>
|
|
||||||
</ShadTooltip>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardFooter>
|
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
|
||||||
<div className="flex w-full flex-wrap justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
updateIds(flow.data!);
|
|
||||||
addFlow({ flow }).then((id) => {
|
|
||||||
navigate("/flow/" + id);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
tabIndex={-1}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name="ExternalLink"
|
|
||||||
className="main-page-nav-button select-none"
|
|
||||||
/>
|
|
||||||
Select Flow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import useAddFlow from "@/hooks/flows/use-add-flow";
|
||||||
|
import { getComponent } from "../../../controllers/API";
|
||||||
|
import { storeComponent } from "../../../types/store";
|
||||||
|
import cloneFlowWithParent from "../../../utils/storeUtils";
|
||||||
|
|
||||||
|
const useInstallComponent = (
|
||||||
|
data: storeComponent,
|
||||||
|
name: string,
|
||||||
|
downloadsCount: number,
|
||||||
|
setDownloadsCount: (value: any) => void,
|
||||||
|
setLoading: (value: boolean) => void,
|
||||||
|
setSuccessData: (value: { title: string }) => void,
|
||||||
|
setErrorData: (value: { title: string; list: string[] }) => void,
|
||||||
|
) => {
|
||||||
|
const addFlow = useAddFlow();
|
||||||
|
|
||||||
|
const handleInstall = () => {
|
||||||
|
const temp = downloadsCount;
|
||||||
|
setDownloadsCount((old) => Number(old) + 1);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
getComponent(data.id)
|
||||||
|
.then((res) => {
|
||||||
|
const newFlow = cloneFlowWithParent(res, res.id, data.is_component);
|
||||||
|
addFlow({ flow: newFlow })
|
||||||
|
.then((id) => {
|
||||||
|
setSuccessData({
|
||||||
|
title: `${name} Installed Successfully.`,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setLoading(false);
|
||||||
|
setErrorData({
|
||||||
|
title: `Error installing the ${name}`,
|
||||||
|
list: [error.response.data.detail],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoading(false);
|
||||||
|
setErrorData({
|
||||||
|
title: `Error installing the ${name}`,
|
||||||
|
list: [err.response.data.detail],
|
||||||
|
});
|
||||||
|
setDownloadsCount(temp);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleInstall };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useInstallComponent;
|
||||||
328
src/frontend/src/components/storeCardComponent/index.tsx
Normal file
328
src/frontend/src/components/storeCardComponent/index.tsx
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
import { usePostLikeComponent } from "@/controllers/API/queries/store";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { getComponent } from "../../controllers/API";
|
||||||
|
import IOModal from "../../modals/IOModal";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
|
import { useStoreStore } from "../../stores/storeStore";
|
||||||
|
import { FlowType } from "../../types/flow";
|
||||||
|
import { storeComponent } from "../../types/store";
|
||||||
|
import cloneFLowWithParent, {
|
||||||
|
getInputsAndOutputs,
|
||||||
|
} from "../../utils/storeUtils";
|
||||||
|
import { cn } from "../../utils/utils";
|
||||||
|
import IconComponent from "../genericIconComponent";
|
||||||
|
import ShadTooltip from "../shadTooltipComponent";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../ui/card";
|
||||||
|
import Loading from "../ui/loading";
|
||||||
|
import useDataEffect from "./hooks/use-data-effect";
|
||||||
|
import useInstallComponent from "./hooks/use-handle-install";
|
||||||
|
import { convertTestName } from "./utils/convert-test-name";
|
||||||
|
|
||||||
|
export default function StoreCardComponent({
|
||||||
|
data,
|
||||||
|
authorized = true,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
data: storeComponent;
|
||||||
|
authorized?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false);
|
||||||
|
const [likesCount, setLikesCount] = useState(data?.liked_by_count ?? 0);
|
||||||
|
const [downloadsCount, setDownloadsCount] = useState(
|
||||||
|
data?.downloads_count ?? 0,
|
||||||
|
);
|
||||||
|
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||||
|
const [openPlayground, setOpenPlayground] = useState(false);
|
||||||
|
const [loadingPlayground, setLoadingPlayground] = useState(false);
|
||||||
|
const playground =
|
||||||
|
data.last_tested_version?.includes("1.0.0") && !data.is_component;
|
||||||
|
|
||||||
|
const name = data.is_component ? "Component" : "Flow";
|
||||||
|
|
||||||
|
async function getFlowData() {
|
||||||
|
const res = await getComponent(data.id);
|
||||||
|
const newFlow = cloneFLowWithParent(res, res.id, data.is_component, true);
|
||||||
|
return newFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPlayground(flow?: FlowType) {
|
||||||
|
if (!flow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
|
||||||
|
return inputs.length > 0 || outputs.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount);
|
||||||
|
|
||||||
|
const { handleInstall } = useInstallComponent(
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
downloadsCount,
|
||||||
|
setDownloadsCount,
|
||||||
|
setLoading,
|
||||||
|
setSuccessData,
|
||||||
|
setErrorData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutate, isPending } = usePostLikeComponent();
|
||||||
|
|
||||||
|
const handleLikeWMutate = () => {
|
||||||
|
if (likedByUser !== undefined || likedByUser !== null) {
|
||||||
|
const temp = likedByUser;
|
||||||
|
const tempNum = likesCount;
|
||||||
|
setLikedByUser((prev) => !prev);
|
||||||
|
setLikesCount((prev) => (temp ? prev - 1 : prev + 1));
|
||||||
|
mutate(
|
||||||
|
{ componentId: data.id },
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setLikesCount(res.data.likes_count);
|
||||||
|
setLikedByUser(res.data.liked_by_user);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setLikesCount(tempNum);
|
||||||
|
setLikedByUser(temp);
|
||||||
|
if (error.response.status === 403) {
|
||||||
|
return setValidApiKey(false);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
setErrorData({
|
||||||
|
title: `Error liking ${name}.`,
|
||||||
|
list: [error.response.data.detail],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
data-testid={`card-${convertTestName(data.name)}`}
|
||||||
|
//TODO check color schema
|
||||||
|
className={cn(
|
||||||
|
"group relative flex h-[11rem] flex-col justify-between overflow-hidden",
|
||||||
|
!data.is_component &&
|
||||||
|
"hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#5f5f5f0e]",
|
||||||
|
disabled ? "pointer-events-none opacity-50" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<CardHeader>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
|
||||||
|
<IconComponent
|
||||||
|
className={cn(
|
||||||
|
"visible flex-shrink-0",
|
||||||
|
data.is_component
|
||||||
|
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||||
|
: "h-7 w-7 flex-shrink-0 text-flow-icon",
|
||||||
|
)}
|
||||||
|
name={data.is_component ? "ToyBrick" : "Group"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShadTooltip content={data.name}>
|
||||||
|
<div className="w-full truncate pr-3">{data.name}</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{data.private && (
|
||||||
|
<ShadTooltip content="Private">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<IconComponent name="Lock" className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
|
{!data.is_component && (
|
||||||
|
<ShadTooltip content="Components">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<IconComponent name="ToyBrick" className="h-4 w-4" />
|
||||||
|
<span data-testid={`total-${data.name}`}>
|
||||||
|
{data?.metadata?.total ?? 0}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
|
<ShadTooltip content="Likes">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<IconComponent name="Heart" className={cn("h-4 w-4")} />
|
||||||
|
<span data-testid={`likes-${data.name}`}>
|
||||||
|
{likesCount ?? 0}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
<ShadTooltip content="Downloads">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<IconComponent name="DownloadCloud" className="h-4 w-4" />
|
||||||
|
<span data-testid={`downloads-${data.name}`}>
|
||||||
|
{downloadsCount ?? 0}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{data.user_created && data.user_created.username && (
|
||||||
|
<span className="text-sm text-primary">
|
||||||
|
by <b>{data.user_created.username}</b>
|
||||||
|
{data.last_tested_version && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
|{" "}
|
||||||
|
<span className="text-xs">
|
||||||
|
{" "}
|
||||||
|
⛓︎ v{data.last_tested_version}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex w-full flex-1 flex-wrap gap-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardDescription className="pb-2 pt-2">
|
||||||
|
<div className="truncate-doubleline">{data.description}</div>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardFooter>
|
||||||
|
<div className="z-50 flex w-full items-center justify-between gap-2">
|
||||||
|
<div className="flex w-full flex-wrap items-end justify-end gap-2">
|
||||||
|
{playground && (
|
||||||
|
<Button
|
||||||
|
disabled={loadingPlayground || !authorized}
|
||||||
|
key={data.id}
|
||||||
|
tabIndex={-1}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="z-50 gap-2 whitespace-nowrap"
|
||||||
|
data-testid={"playground-flow-button-" + data.id}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setLoadingPlayground(true);
|
||||||
|
getFlowData().then((res) => {
|
||||||
|
if (!hasPlayground(res)) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error",
|
||||||
|
list: ["This flow doesn't have a playground."],
|
||||||
|
});
|
||||||
|
setLoadingPlayground(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentFlow(res);
|
||||||
|
setOpenPlayground(true);
|
||||||
|
setLoadingPlayground(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!loadingPlayground ? (
|
||||||
|
<IconComponent
|
||||||
|
name="BotMessageSquareIcon"
|
||||||
|
className="h-4 w-4 select-none"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loading className="h-4 w-4 text-medium-indigo" />
|
||||||
|
)}
|
||||||
|
Playground
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-0.5">
|
||||||
|
<ShadTooltip
|
||||||
|
content={authorized ? "Like" : "Please review your API key."}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={isPending}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={
|
||||||
|
"whitespace-nowrap" +
|
||||||
|
(!authorized ? " cursor-not-allowed" : "")
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (!authorized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleLikeWMutate();
|
||||||
|
}}
|
||||||
|
data-testid={`like-${data.name}`}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name="Heart"
|
||||||
|
className={cn(
|
||||||
|
"h-5 w-5",
|
||||||
|
likedByUser
|
||||||
|
? "fill-destructive stroke-destructive"
|
||||||
|
: "",
|
||||||
|
!authorized ? "text-ring" : "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</ShadTooltip>
|
||||||
|
<ShadTooltip
|
||||||
|
content={
|
||||||
|
authorized
|
||||||
|
? "Install Locally"
|
||||||
|
: "Please review your API key."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={
|
||||||
|
"whitespace-nowrap" +
|
||||||
|
(!authorized ? " cursor-not-allowed" : "") +
|
||||||
|
(!loading ? " p-0.5" : "")
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (loading || !authorized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleInstall();
|
||||||
|
}}
|
||||||
|
data-testid={`install-${data.name}`}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name={loading ? "Loader2" : "Plus"}
|
||||||
|
className={cn(
|
||||||
|
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
|
||||||
|
!authorized ? "text-ring" : "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
{openPlayground && (
|
||||||
|
<IOModal
|
||||||
|
key={data.id}
|
||||||
|
cleanOnClose={true}
|
||||||
|
open={openPlayground}
|
||||||
|
setOpen={setOpenPlayground}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</IOModal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function convertTestName(name: string): string {
|
||||||
|
return name.replace(/ /g, "-").toLowerCase();
|
||||||
|
}
|
||||||
|
|
@ -17,29 +17,6 @@ const CollectionCard = ({ item, type, isLoading, control }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderButton = () => {
|
|
||||||
if (!isComponent) {
|
|
||||||
return (
|
|
||||||
<Link to={editFlowLink}>
|
|
||||||
<Button
|
|
||||||
tabIndex={-1}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
data-testid={editFlowButtonTestId}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name="ExternalLink"
|
|
||||||
className="main-page-nav-button select-none"
|
|
||||||
/>
|
|
||||||
Edit Flow
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionCardComponent
|
<CollectionCardComponent
|
||||||
data={{
|
data={{
|
||||||
|
|
@ -48,9 +25,7 @@ const CollectionCard = ({ item, type, isLoading, control }) => {
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
data-testid={editFlowButtonTestId}
|
data-testid={editFlowButtonTestId}
|
||||||
button={renderButton()!}
|
|
||||||
onClick={!isComponent ? handleClick : undefined}
|
onClick={!isComponent ? handleClick : undefined}
|
||||||
playground={!isComponent}
|
|
||||||
control={control}
|
control={control}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import ShadTooltip from "../../components/shadTooltipComponent";
|
||||||
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
|
|
||||||
|
import StoreCardComponent from "@/components/storeCardComponent";
|
||||||
import { useGetTagsQuery } from "@/controllers/API/queries/store";
|
import { useGetTagsQuery } from "@/controllers/API/queries/store";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import PaginatorComponent from "../../components/paginatorComponent";
|
import PaginatorComponent from "../../components/paginatorComponent";
|
||||||
|
|
@ -326,15 +327,11 @@ export default function StorePage(): JSX.Element {
|
||||||
searchData.map((item) => {
|
searchData.map((item) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CollectionCardComponent
|
<StoreCardComponent
|
||||||
key={item.id}
|
key={item.id}
|
||||||
data={item}
|
data={item}
|
||||||
authorized={validApiKey}
|
authorized={validApiKey}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
playground={
|
|
||||||
item.last_tested_version?.includes("1.0.0") &&
|
|
||||||
!item.is_component
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue