pagination and context with store

This commit is contained in:
cristhianzl 2023-10-25 22:18:31 -03:00
commit 1081ace70f
21 changed files with 319 additions and 207 deletions

View file

@ -22,49 +22,53 @@ from langflow.services.store.service import StoreService
router = APIRouter(prefix="/store", tags=["Components Store"])
def get_user_store_api_key(user: User = Depends(auth_utils.get_current_active_user)):
def get_user_store_api_key(
user: User = Depends(auth_utils.get_current_active_user),
settings_service=Depends(get_settings_service),
):
if not user.store_api_key:
raise HTTPException(
status_code=400, detail="You must have a store API key set."
)
return user.store_api_key
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
return decrypted
def get_optional_user_store_api_key(
user: User = Depends(auth_utils.get_current_active_user),
settings_service=Depends(get_settings_service),
):
return user.store_api_key
if not user.store_api_key:
return None
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
return decrypted
@router.post("/components/", response_model=ComponentResponse, status_code=201)
def create_component(
component: StoreComponentCreate,
store_service: StoreService = Depends(get_store_service),
settings_service=Depends(get_settings_service),
store_api_Key: str = Depends(get_user_store_api_key),
):
try:
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service)
return store_service.upload(decrypted, component)
return store_service.upload(store_api_Key, component)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.get("/components/", response_model=List[ListComponentResponse])
def list_components(
filter_by_user: bool = Query(False),
page: int = 1,
limit: int = 10,
store_service: StoreService = Depends(get_store_service),
store_api_Key: str = Depends(get_optional_user_store_api_key),
settings_service=Depends(get_settings_service),
):
try:
fields = ["id", "name", "description", "user_created.name", "is_component"]
if store_api_Key:
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service)
else:
decrypted = None
result = store_service.list_components(decrypted, page, limit, fields=fields)
result = store_service.list_components(
store_api_Key, page, limit, fields=fields, filter_by_user=filter_by_user
)
return result
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@ -73,16 +77,13 @@ def list_components(
@router.get("/components/{component_id}", response_model=DownloadComponentResponse)
def read_component(
component_id: UUID,
filter_by_user: bool = Query(False),
store_service: StoreService = Depends(get_store_service),
store_api_Key: str = Depends(get_user_store_api_key),
settings_service=Depends(get_settings_service),
):
# If the component is from the store, we need to get it from the store
try:
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service)
component = store_service.download(decrypted, component_id, filter_by_user)
component = store_service.download(store_api_Key, component_id)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@ -102,6 +103,7 @@ async def search_endpoint(
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
sort: Optional[List[str]] = Query(None),
filter_by_user: bool = Query(False),
fields: Optional[List[str]] = Query(None),
store_service: "StoreService" = Depends(get_store_service),
store_api_Key: str = Depends(get_optional_user_store_api_key),
@ -118,6 +120,7 @@ async def search_endpoint(
date_to=date_to,
sort=sort,
fields=fields,
filter_by_user=filter_by_user,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

View file

@ -72,6 +72,7 @@ class StoreService(Service):
date_to: Optional[datetime] = None,
sort: Optional[List[str]] = ["-likes"],
fields: Optional[List[str]] = None,
filter_by_user: bool = False,
) -> List[ComponentResponse]:
# ?sort=sort,-date_created,author.name
@ -104,34 +105,6 @@ class StoreService(Service):
if fields:
params["fields"] = ",".join(fields)
results = self._get(self.components_url, api_key, params)
return [ComponentResponse(**component) for component in results]
def list_components(
self,
api_key: str,
page: int = 1,
limit: int = 10,
fields: Optional[List[str]] = None,
) -> List[ListComponentResponse]:
params = {"page": page, "limit": limit}
# ?aggregate[count]=likes
params["fields"] = (
",".join(fields)
if fields
else ",".join(["id", "name", "description", "count(likes)", "is_component"])
)
results = self._get(self.components_url, api_key, params)
return [ListComponentResponse(**component) for component in results]
def download(
self, api_key: str, component_id: str, filter_by_user: bool
) -> DownloadComponentResponse:
url = f"{self.components_url}/{component_id}"
params = {
"fields": ",".join(["id", "name", "description", "data", "is_component"])
}
if filter_by_user:
params["deep"] = json.dumps(
{
@ -140,6 +113,52 @@ class StoreService(Service):
}
}
)
else:
params["filter"] = json.dumps({"status": {"_eq": "public"}})
results = self._get(self.components_url, api_key, params)
return [ComponentResponse(**component) for component in results]
def list_components(
self,
api_key: str,
page: int = 1,
limit: int = 15,
fields: Optional[List[str]] = None,
filter_by_user: bool = False,
) -> List[ListComponentResponse]:
params = {"page": page, "limit": limit}
# ?aggregate[count]=likes
params["fields"] = (
",".join(fields)
if fields
else ",".join(["id", "name", "description", "count(likes)", "is_component"])
)
# Only public components or the ones created by the user
# check for "public" or "Public"
if filter_by_user:
params["deep"] = json.dumps(
{
"components": {
"_filter": {"user_created": {"token": {"_eq": api_key}}}
}
}
)
else:
params["filter"] = params["filter"] = json.dumps(
{"status": {"_in": ["public", "Public"]}}
)
results = self._get(self.components_url, api_key, params)
return [ListComponentResponse(**component) for component in results]
def download(self, api_key: str, component_id: str) -> DownloadComponentResponse:
url = f"{self.components_url}/{component_id}"
params = {
"fields": ",".join(["id", "name", "description", "data", "is_component"])
}
component = self._get(url, api_key, params)
self.call_webhook(api_key, self.webhook_url, component_id)

View file

@ -3796,9 +3796,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
},
"node_modules/@types/react": {
"version": "18.2.31",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz",
"integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==",
"version": "18.2.32",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz",
"integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -4517,9 +4517,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001553",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz",
"integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==",
"version": "1.0.30001554",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
"integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==",
"funding": [
{
"type": "opencollective",
@ -10220,9 +10220,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz",
"integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==",
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -13755,9 +13755,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
},
"@types/react": {
"version": "18.2.31",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz",
"integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==",
"version": "18.2.32",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz",
"integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -14248,9 +14248,9 @@
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
},
"caniuse-lite": {
"version": "1.0.30001553",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz",
"integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A=="
"version": "1.0.30001554",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
"integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ=="
},
"ccount": {
"version": "2.0.1",
@ -18070,9 +18070,9 @@
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ=="
},
"tailwindcss": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz",
"integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==",
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"requires": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",

View file

@ -16,6 +16,7 @@ export default function PaginatorComponent({
rowsCount = [10, 20, 50, 100],
totalRowsCount = 0,
paginate,
storeComponent = false,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [maxIndex, setMaxPageIndex] = useState(
@ -30,7 +31,13 @@ export default function PaginatorComponent({
<>
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"></div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div
className={
storeComponent
? "flex items-center lg:space-x-8 "
: "flex items-center space-x-6 lg:space-x-8 "
}
>
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
@ -54,7 +61,8 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pageIndex} of {maxIndex}
Page {pageIndex}
{!storeComponent && <> of {maxIndex}</>}
</div>
<div className="flex items-center space-x-2">
<Button

View file

@ -34,7 +34,7 @@ export default function Header(): JSX.Element {
return (
<div className="header-arrangement">
<div className="header-start-display">
<div className="header-start-display lg:w-[30%]">
<Link to="/">
<span className="ml-4 text-2xl"></span>
</Link>
@ -47,14 +47,19 @@ export default function Header(): JSX.Element {
<Link to="/">
<Button
className="gap-2"
variant={location.pathname === "/" ? "primary" : "secondary"}
variant={
location.pathname === "/flows" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm"
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="flex-1">{USER_PROJECTS_HEADER}</div>
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button>
</Link>
<Link to="/community">
{/* <Link to="/community">
<Button
className="gap-2"
variant={
@ -65,7 +70,7 @@ export default function Header(): JSX.Element {
<IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div>
</Button>
</Link>
</Link> */}
{hasStore && (
<Link to="/store">
<Button
@ -79,16 +84,16 @@ export default function Header(): JSX.Element {
</Link>
)}
</div>
<div className="header-end-division">
<div className="header-end-division lg:w-[30%]">
<div className="header-end-display">
<a
href="https://github.com/logspace-ai/langflow"
target="_blank"
rel="noreferrer"
className="header-github-link"
className="header-github-link gap-2"
>
<FaGithub className="mr-2 h-5 w-5" />
Star
<FaGithub className="h-5 w-5" />
<div className="hidden lg:block">Star</div>
<div className="header-github-display">{stars}</div>
</a>
<a

View file

@ -1,11 +1,10 @@
import { createContext, useState } from "react";
import { checkHasStore } from "../controllers/API";
import { storeContextType } from "../types/contexts/store";
import { FlowType } from "../types/flow";
//store context to share user components and update them
const initialValue = {
savedFlows: {},
savedFlows: new Set<string>(),
setSavedFlows: () => {},
hasStore: false,
setHasStore: () => {},
@ -14,7 +13,8 @@ const initialValue = {
export const StoreContext = createContext<storeContextType>(initialValue);
export function StoreProvider({ children }) {
const [savedFlows, setSavedFlows] = useState<{ [key: string]: FlowType }>({});
const [savedFlows, setSavedFlows] = useState<Set<string>>(new Set());
const [hasStore, setHasStore] = useState(false);
checkHasStore().then((res) => {

View file

@ -1,5 +1,5 @@
import { AxiosError } from "axios";
import _ from "lodash";
import _, { cloneDeep } from "lodash";
import {
ReactNode,
createContext,
@ -40,7 +40,7 @@ import {
updateTemplate,
} from "../utils/reactflowUtils";
import {
IncrementObjectKey,
createRandomKey,
getRandomDescription,
getRandomName,
getSetFromObject,
@ -80,7 +80,7 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => {},
saveComponent: (component: NodeDataType) => {},
saveComponent: async (component: NodeDataType) => "",
deleteComponent: (id: string, key: string) => {},
};
@ -154,8 +154,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return;
}
if (flow.data && flow.is_component) {
storeComponents[(flow.data.nodes[0].data as NodeDataType).type] =
_.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
(flow.data.nodes[0].data as NodeDataType).node!.display_name =
flow.name;
storeComponents[
createRandomKey(
(flow.data.nodes[0].data as NodeDataType).type,
uid()
)
] = _.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return;
}
if (!skipUpdate) processDataFromFlow(flow, false);
@ -165,9 +171,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
setData((prev) => {
let newData = _.cloneDeep(prev);
Object.keys(storeComponents).forEach((key) => {
newData["custom_components"][key] = storeComponents[key];
});
const customComponent = newData["custom_components"]["CustomComponent"];
newData["custom_components"] = cloneDeep(storeComponents);
newData["custom_components"]["CustomComponent"] = customComponent;
return newData;
});
}
@ -458,6 +465,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
@ -482,6 +490,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle,
targetHandle,
id,
data: cloneDeep(edge.data),
style: { stroke: "#555" },
className:
targetHandleObject.type === "Text"
@ -538,7 +547,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
let data = flow?.data ? flow.data : null;
if (data) {
processFlowEdges(flow);
processFlowNodes(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
// updateNodes(data.nodes, data.edges);
@ -605,7 +615,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const addFlowToLocalState = (newFlow: FlowType) => {
let newFlows: FlowType[] = [];
setFlows((prevState) => {
newFlows.concat(prevState);
newFlows = newFlows.concat(prevState);
newFlows.push(newFlow);
return [...prevState, newFlow];
});
@ -671,12 +681,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
component.node!.official = false;
let key = component.type;
if (data["custom_components"][key] !== undefined) {
let { newKey, increment } = IncrementObjectKey(
data["custom_components"],
key
);
key = newKey;
component.type = newKey;
let increment: number;
component.type = createRandomKey(key, uid());
let componentNodes: { [key: string]: APIClassType } = {};
Object.keys(data["custom_components"]).forEach((key) => {
componentNodes[key] = data["custom_components"][key];
@ -697,7 +703,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
` (${increment})`;
}
}
addFlow(true, createFlowComponent(component));
return addFlow(true, createFlowComponent(component));
}
function deleteComponent(id: string, key: string) {

View file

@ -597,6 +597,20 @@ export async function getStoreComponents(page: number = 1, limit: number = 10) {
}
}
export async function getStoreSavedComponents() {
try {
const res = await api.get(
`${BASE_URL_API}store/components/?filter_by_user=true`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function postStoreComponents(component: Component) {
try {
const res = await api.post(`${BASE_URL_API}store/components/`, component);
@ -676,3 +690,15 @@ export async function checkHasStore() {
throw error;
}
}
export async function getNumberOfComponents() {
try {
const res = await api.get(`${BASE_URL_API}store/components/count`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}

View file

@ -7,6 +7,7 @@ import {
useRef,
useState,
} from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
@ -64,6 +65,7 @@ const EditNodeModal = forwardRef(
ref
) => {
const [modalOpen, setModalOpen] = useState(open ?? false);
const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data);
@ -83,11 +85,14 @@ const EditNodeModal = forwardRef(
const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue;
setDataValue(newValue);
updateNodeInternals(data.id);
};
useEffect(() => {
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
if (modalOpen) {
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
}
}, [modalOpen]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);

View file

@ -310,7 +310,6 @@ export default function Page({
event.dataTransfer.getData("nodedata")
);
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created
const position = reactFlowInstance!.project({
x: event.clientX - reactflowBounds!.left,

View file

@ -309,7 +309,12 @@ export default function ExtraSidebar(): JSX.Element {
>
<div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName])
.sort(sensitiveSort)
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
)
.map((SBItemName: string, index) => (
<ShadTooltip
content={

View file

@ -70,20 +70,21 @@ export default function NodeToolbarComponent({
function handleShareComponent() {
const componentFlow = cloneDeep(data);
saveFlowStore(createFlowComponent(componentFlow)).then(
() => {
saveComponent(componentFlow);
setSuccessData({
title: "Component shared successfully",
});
},
(err) => {
setErrorData({
title: "Error sharing component",
list: [err["response"]["data"]["detail"]],
});
}
);
saveComponent(componentFlow).then(() => {
saveFlowStore(createFlowComponent(componentFlow)).then(
(_) => {
setSuccessData({
title: "Component shared successfully",
});
},
(err) => {
setErrorData({
title: "Error sharing component",
list: [err["response"]["data"]["detail"]],
});
}
);
});
}
const handleSelectChange = (event) => {
switch (event) {
@ -98,7 +99,6 @@ export default function NodeToolbarComponent({
downloadNode(createFlowComponent(cloneDeep(data)));
break;
case "Share":
console.log("Share");
setShowconfirmShare(true);
break;
case "SaveAll":

View file

@ -96,7 +96,6 @@ export default function ComponentsComponent() {
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows

View file

@ -94,7 +94,6 @@ export default function FlowsComponent() {
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows

View file

@ -1,5 +1,5 @@
import { Link, ToyBrick } from "lucide-react";
import { useContext, useRef, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../../components/genericIconComponent";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
@ -10,6 +10,8 @@ import {
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { alertContext } from "../../../contexts/alertContext";
import { StoreContext } from "../../../contexts/storeContext";
import { TabsContext } from "../../../contexts/tabsContext";
import { getComponent, saveFlowStore } from "../../../controllers/API";
import { FlowType } from "../../../types/flow";
@ -17,12 +19,19 @@ import { FlowComponent } from "../../../types/store";
import cloneFLowWithParent from "../../../utils/storeUtils";
export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
const [added, setAdded] = useState(false);
const { savedFlows } = useContext(StoreContext);
const [added, setAdded] = useState(savedFlows.has(data.id) ? true : false);
const [loading, setLoading] = useState(false);
const { addFlow } = useContext(TabsContext);
const { setSuccessData } = useContext(alertContext);
const flowData = useRef<FlowType>();
useEffect(() => {
setAdded(savedFlows.has(data.id) ? true : false);
}, [savedFlows]);
function handleAdd() {
setLoading(true);
getComponent(data.id).then(
(res) => {
console.log(res);
@ -45,7 +54,19 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
}
function handleInstall() {
addFlow(true, flowData.current!);
if (flowData.current) {
addFlow(true, flowData.current!).then(() => {
setSuccessData({ title: "Flow Installed" });
});
} else {
getComponent(data.id).then((res) => {
console.log(res);
const newFLow = cloneFLowWithParent(res, res.id, data.is_component);
flowData.current = newFLow;
addFlow(true, newFLow);
setSuccessData({ title: "Flow Installed" });
});
}
}
function handleFork(flowId: string, is_component: boolean) {
@ -145,7 +166,6 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
size="sm"
className="whitespace-nowrap "
onClick={() => {
setLoading(true);
if (!added) {
handleAdd();
} else {

View file

@ -4,6 +4,7 @@ import { useContext, useEffect, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
@ -16,53 +17,61 @@ import {
} from "../../components/ui/select";
import { Switch } from "../../components/ui/switch";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { StoreContext } from "../../contexts/storeContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getStoreComponents, searchComponent } from "../../controllers/API";
import {
getStoreComponents,
getStoreSavedComponents,
searchComponent,
} from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { FlowComponent } from "../../types/store";
import { cn } from "../../utils/utils";
import { MarketCardComponent } from "./components/market-card";
export default function StorePage(): JSX.Element {
const { setTabId } = useContext(TabsContext);
const { setApiKey, apiKey } = useContext(AuthContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const [data, setData] = useState<FlowComponent[]>([]);
const [dataSelect, setDataSelect] = useState<FlowComponent[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState(false);
const [filteredCategories, setFilteredCategories] = useState(new Set());
const [inputText, setInputText] = useState<string>("");
const [searchData, setSearchData] = useState(data);
const [errorApiKey, setErrorApiKey] = useState(false);
const { setErrorData } = useContext(alertContext);
const { addFlow } = useContext(TabsContext);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(1);
const { setSavedFlows } = useContext(StoreContext);
async function getSavedComponents() {
setLoading(true);
const result = await getStoreSavedComponents();
let savedIds = new Set<string>();
result.forEach((flow) => {
savedIds.add(flow.id);
});
setSavedFlows(savedIds);
}
useEffect(() => {
handleGetComponents();
getSavedComponents().then((_) => handleGetComponents());
}, []);
const handleGetComponents = () => {
setLoading(true);
getStoreComponents(index - 1, 10000)
.then((res) => {
setSearchData(res.slice(0, 10));
setLoading(false);
setErrorApiKey(false);
setData(res);
setSearchData(res.slice(0, size));
setTotalRowsCount(res.length);
setData(res);
setLoading(false);
})
.catch((err) => {
setSearchData([]);
setLoading(false);
setErrorApiKey(true);
setErrorData({
title: "Error to get components.",
list: [err["response"]["data"]["detail"]],
@ -71,6 +80,12 @@ export default function StorePage(): JSX.Element {
};
const handleSearch = (inputText: string) => {
if (inputText === "") {
handleGetComponents();
setSearch(false);
return;
}
setSearch(true);
setLoading(true);
searchComponent(inputText).then(
(res) => {
@ -87,17 +102,15 @@ export default function StorePage(): JSX.Element {
setLoading(true);
getStoreComponents(pageIndex, pageSize)
.then((res) => {
setSearchData(res.slice(0, size));
setData(res);
setPageIndex(pageIndex);
setPageSize(pageSize);
setSearchData(res);
setLoading(false);
setErrorApiKey(false);
setData(res);
})
.catch((err) => {
setSearchData([]);
setLoading(false);
setErrorApiKey(true);
setErrorData({
title: "Error to get components.",
list: [err["response"]["data"]["detail"]],
@ -105,14 +118,9 @@ export default function StorePage(): JSX.Element {
});
}
function resetFilter() {
setPageIndex(1);
setPageSize(10);
handleGetComponents();
}
const loadingWithApiKey = loading;
const renderComponents = !loading;
const renderPagination = searchData.length > 0 && !loading && !search;
return (
<>
@ -189,56 +197,61 @@ export default function StorePage(): JSX.Element {
</Select>
</div>
</div>
<div className="flex items-center justify-center gap-4">
{Array.from(new Set(searchData.map((i) => i.is_component))).map(
(i, idx) => (
<Badge
onClick={() => {
filteredCategories.has(i)
? setFilteredCategories((old) => {
let newFilteredCategories = cloneDeep(old);
newFilteredCategories.delete(i);
return newFilteredCategories;
})
: setFilteredCategories((old) => {
let newFilteredCategories = cloneDeep(old);
newFilteredCategories.add(i);
return newFilteredCategories;
});
}}
variant="gray"
size="md"
className={cn(
"cursor-pointer border-none",
filteredCategories.has(i)
? "bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
<Link className="mr-1.5 w-4" />
{i}
</Badge>
)
<div className="flex h-2 items-center justify-center gap-4">
{renderComponents &&
Array.from(new Set(searchData.map((i) => i.is_component))).map(
(i, idx) => (
<Badge
onClick={() => {
filteredCategories.has(i)
? setFilteredCategories((old) => {
let newFilteredCategories = cloneDeep(old);
newFilteredCategories.delete(i);
return newFilteredCategories;
})
: setFilteredCategories((old) => {
let newFilteredCategories = cloneDeep(old);
newFilteredCategories.add(i);
return newFilteredCategories;
});
}}
variant="gray"
size="md"
className={cn(
"cursor-pointer border-none",
filteredCategories.has(i)
? "bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
<Link className="mr-1.5 w-4" />
{i}
</Badge>
)
)}
</div>
<div className="mt-6 grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{loadingWithApiKey ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
searchData
.filter(
(f) =>
Array.from(filteredCategories).length === 0 ||
filteredCategories.has(f.is_component)
)
.map((item, idx) => (
<MarketCardComponent key={idx} data={item} />
))
)}
</div>
{renderComponents && (
<>
<div className="mt-6 grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{searchData
.filter(
(f) =>
Array.from(filteredCategories).length === 0 ||
filteredCategories.has(f.is_component)
)
.map((item, idx) => (
<MarketCardComponent key={idx} data={item} />
))}
</div>
</>
)}
</div>
{totalRowsCount > 0 && !loading && (
{renderPagination && (
<div className="relative bottom-2">
<PaginatorComponent
storeComponent={true}
@ -251,9 +264,6 @@ export default function StorePage(): JSX.Element {
></PaginatorComponent>
</div>
)}
{loadingWithApiKey && (
<div className="flex w-full flex-col gap-4 p-4">Loading...</div>
)}
</div>
</>
);

View file

@ -123,7 +123,7 @@
@apply flex gap-2;
}
.primary-input {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 text-sm;
}
.skeleton-card {
@ -237,7 +237,7 @@
}
.main-page-flows-display {
@apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-4;
@apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3;
}
.community-page-arrangement {
@ -495,7 +495,7 @@
@apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium;
}
.header-menu-bar-display {
@apply flex max-w-[200px] cursor-pointer items-center gap-2;
@apply flex max-w-[120px] lg:max-w-[200px] cursor-pointer items-center gap-2;
}
.header-menu-flow-name {
@apply flex-1 truncate;
@ -505,13 +505,13 @@
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-border bg-background;
@apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted;
}
.header-start-display {
@apply flex w-[30%] items-center justify-start gap-2;
@apply flex items-center justify-start gap-2;
}
.header-end-division {
@apply flex w-[30%] justify-end px-2;
@apply flex justify-end px-2;
}
.header-end-display {
@apply ml-auto mr-2 flex items-center gap-5;
@ -535,7 +535,7 @@
@apply hover:bg-accent hover:text-accent-foreground;
}
.header-github-display {
@apply -mr-px ml-2 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
@apply -mr-px ml-1 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
}
.header-notifications-box {
@apply fixed left-0 top-0 h-screen w-screen;

View file

@ -1,8 +1,6 @@
import { FlowType } from "../flow";
export type storeContextType = {
savedFlows: { [key: string]: FlowType };
setSavedFlows: (newState: { [key: string]: FlowType }) => void;
savedFlows: Set<string>;
setSavedFlows: (newState: Set<string>) => void;
setHasStore: (store: boolean) => void;
hasStore: boolean;
};

View file

@ -36,7 +36,7 @@ export type TabsContextType = {
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
setTweak: (tweak: tweakType) => tweakType | void;
getTweak: tweakType;
saveComponent: (component: NodeDataType) => void;
saveComponent: (component: NodeDataType) => Promise<String | undefined>;
deleteComponent: (id: string, key: string) => void;
};

View file

@ -1009,7 +1009,8 @@ export function expandGroupNode(
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
gNodes[nodeIndex].selected = false;
// keep the nodes selected after ungrouping
// gNodes[nodeIndex].selected = false;
if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else {
@ -1096,3 +1097,8 @@ export function downloadNode(NodeFLow: FlowType) {
element.download = `${NodeFLow.name}.json`;
element.click();
}
export function updateComponentNameAndType(
data: any,
component: NodeDataType
) {}

View file

@ -571,7 +571,7 @@ export function IncrementObjectKey(
export function removeCountFromString(input: string): string {
// Define a regex pattern to match the count in parentheses
const pattern = /\s*\(\d+\)\s*$/;
const pattern = /\s*\(\w+\)\s*$/;
// Use the `replace` method to remove the matched pattern
const result = input.replace(pattern, "");
@ -579,9 +579,13 @@ export function removeCountFromString(input: string): string {
return result.trim(); // Trim any leading/trailing spaces
}
export function createRandomKey(key: string, uid: string): string {
return removeCountFromString(key) + ` (${uid})`;
}
export function sensitiveSort(a: string, b: string): number {
// Extract the name and number from each string using regular expressions
const regex = /(.+) \((\d+)\)/;
const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex);
const matchB = b.match(regex);