Repository: langflow-ai/langflow
Pull request: fix: update LANGFLOW_COMPONENTS_PATH env variable behavior
this pr unhide components that are not originally from langflow on the sidebar and fix the LANGFLOW_COMPONENTS_PATH behavior.

fix #5256

Problem statement:
LANGFLOW_COMPONENTS_PATH not working
### Bug Description

the custom path for components does not work as expected

### Reproduction

add a path to the .env using the LANGFLOW_COMPONENTS_PATH system variable
add a python component to the provided path
notice the component does not display in the sidebar

### Expected behavior

the component should be displayed

### Who can help?

_No response_

### Operating System

macos sequoia

### Langflow Version

1.1

### Python Version

3.12

### Screenshot

_No response_

### Flow File

_No response_

Hints from issue discussion:
I do confim the behavior.  By following the documentation here https://docs.langflow.org/components-custom-components, i am not able to display my own component in latest, nightly neitheir 1.1.0
It work in version 1.0.19
There are several discution in discord on the subject.
It is clearly important because to add my own custom components the only solution is to fork your fantastic projet which is a pitty.
could you have a look to this problem by restoring ENV LANGFLOW_COMPONENTS_PATH="/app/langflow/custom_components/" feature ?

Thanks a lot in advance.

I have tested against the nightly build Starting Langflow v1.1.1.dev28...
bug seems still there.
did you find where the modifications have been introduce since version 1.1.0 ?
Hope having news on it.

Commit message: [autofix.ci] apply automated fixes

Patch to review:
diff --git a/src/backend/base/langflow/custom/directory_reader/directory_reader.py b/src/backend/base/langflow/custom/directory_reader/directory_reader.py
index 81ac913f5356..cb10eea48792 100644
--- a/src/backend/base/langflow/custom/directory_reader/directory_reader.py
+++ b/src/backend/base/langflow/custom/directory_reader/directory_reader.py
@@ -135,12 +135,7 @@ def get_files(self):
             if "deactivated" in file_path.parent.name:
                 continue
 
-            # The other condtion is that it should be
-            # in the safe_path/[folder]/[file].py format
-            # any folders below [folder] will be ignored
-            # basically the parent folder of the file should be a
-            # folder in the safe_path
-            if file_path.is_file() and file_path.parent.parent == safe_path_obj and not file_path.name.startswith("__"):
+            if file_path.is_file() and not file_path.name.startswith("__"):
                 file_list.append(str(file_path))
         return file_list
 
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx
index 3756e37bf87c..2e54e274bce6 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup/index.tsx
@@ -3,6 +3,7 @@ import {
   SidebarGroupContent,
   SidebarMenu,
 } from "@/components/ui/sidebar";
+import { SIDEBAR_BUNDLES } from "@/utils/styleUtils";
 import { memo } from "react";
 import { CategoryGroupProps } from "../../types";
 import { CategoryDisclosure } from "../categoryDisclouse";
@@ -23,20 +24,42 @@ export const CategoryGroup = memo(function CategoryGroup({
     <SidebarGroup className="p-3">
       <SidebarGroupContent>
         <SidebarMenu>
-          {CATEGORIES.toSorted(
-            (a, b) =>
-              (search !== "" ? sortedCategories : CATEGORIES).findIndex(
-                (value) => value === a.name,
-              ) -
-              (search !== "" ? sortedCategories : CATEGORIES).findIndex(
-                (value) => value === b.name,
-              ),
-          ).map(
-            (item) =>
-              dataFilter[item.name] &&
-              Object.keys(dataFilter[item.name]).length > 0 && (
+          {Object.entries(dataFilter)
+            .filter(
+              ([categoryName, items]) =>
+                // filter out bundles
+                !SIDEBAR_BUNDLES.some((cat) => cat.name === categoryName) &&
+                categoryName !== "custom_component" &&
+                Object.keys(items).length > 0,
+            )
+            .sort(([aName], [bName]) => {
+              const categoryList =
+                search !== ""
+                  ? sortedCategories
+                  : CATEGORIES.map((c) => c.name);
+              const aIndex = categoryList.indexOf(aName);
+              const bIndex = categoryList.indexOf(bName);
+
+              // If neither is in CATEGORIES, keep their relative order
+              if (aIndex === -1 && bIndex === -1) return 0;
+              // If only a is not in CATEGORIES, put it after b
+              if (aIndex === -1) return 1;
+              // If only b is not in CATEGORIES, put it after a
+              if (bIndex === -1) return -1;
+              // If both are in CATEGORIES, sort by their index
+              return aIndex - bIndex;
+            })
+            .map(([categoryName]) => {
+              const item = CATEGORIES.find(
+                (cat) => cat.name === categoryName,
+              ) ?? {
+                name: categoryName,
+                icon: "folder",
+                display_name: categoryName,
+              };
+              return (
                 <CategoryDisclosure
-                  key={item.name}
+                  key={categoryName}
                   item={item}
                   openCategories={openCategories}
                   setOpenCategories={setOpenCategories}
@@ -46,8 +69,8 @@ export const CategoryGroup = memo(function CategoryGroup({
                   onDragStart={onDragStart}
                   sensitiveSort={sensitiveSort}
                 />
-              ),
-          )}
+              );
+            })}
         </SidebarMenu>
       </SidebarGroupContent>
     </SidebarGroup>
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
index 32e41fb34555..857430ee875b 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
@@ -120,19 +120,12 @@ export function FlowSidebarComponent() {
   const sortedCategories = useMemo(() => {
     if (!searchResults || !searchFilteredData) return [];
 
-    return Object.keys(searchFilteredData)
-      .filter(
-        (category) =>
-          Object.keys(searchFilteredData[category]).length > 0 &&
-          (CATEGORIES.find((c) => c.name === category) ||
-            BUNDLES.find((b) => b.name === category)),
-      )
-      .toSorted((a, b) =>
-        searchResults.fuseCategories.indexOf(b) <
-        searchResults.fuseCategories.indexOf(a)
-          ? 1
-          : -1,
-      );
+    return Object.keys(searchFilteredData).toSorted((a, b) =>
+      searchResults.fuseCategories.indexOf(b) <
+      searchResults.fuseCategories.indexOf(a)
+        ? 1
+        : -1,
+    );
   }, [searchResults, searchFilteredData, CATEGORIES, BUNDLES]);
 
   const finalFilteredData = useMemo(() => {
@@ -305,16 +298,6 @@ export function FlowSidebarComponent() {
     [dataFilter],
   );
 
-  const hasCategoryItems = useMemo(
-    () =>
-      CATEGORIES.some(
-        (item) =>
-          dataFilter[item.name] &&
-          Object.keys(dataFilter[item.name]).length > 0,
-      ),
-    [dataFilter],
-  );
-
   return (
     <Sidebar
       collapsible="offcanvas"
@@ -342,20 +325,18 @@ export function FlowSidebarComponent() {
       <SidebarContent>
         {hasResults ? (
           <>
-            {hasCategoryItems && (
-              <CategoryGroup
-                dataFilter={dataFilter}
-                sortedCategories={sortedCategories}
-                CATEGORIES={CATEGORIES}
-                openCategories={openCategories}
-                setOpenCategories={setOpenCategories}
-                search={search}
-                nodeColors={nodeColors}
-                chatInputAdded={chatInputAdded}
-                onDragStart={onDragStart}
-                sensitiveSort={sensitiveSort}
-              />
-            )}
+            <CategoryGroup
+              dataFilter={dataFilter}
+              sortedCategories={sortedCategories}
+              CATEGORIES={CATEGORIES}
+              openCategories={openCategories}
+              setOpenCategories={setOpenCategories}
+              search={search}
+              nodeColors={nodeColors}
+              chatInputAdded={chatInputAdded}
+              onDragStart={onDragStart}
+              sensitiveSort={sensitiveSort}
+            />
             {hasBundleItems && (
               <MemoizedSidebarGroup
                 BUNDLES={BUNDLES}


Code review comments:
diff_hunk: @@ -135,12 +135,7 @@ def get_files(self):
             if "deactivated" in file_path.parent.name:
                 continue
 
-            # The other condtion is that it should be
-            # in the safe_path/[folder]/[file].py format
-            # any folders below [folder] will be ignored
-            # basically the parent folder of the file should be a
-            # folder in the safe_path
-            if file_path.is_file() and file_path.parent.parent == safe_path_obj and not file_path.name.startswith("__"):
+            if file_path.is_file() and not file_path.name.startswith("__"):
path: src/backend/base/langflow/custom/directory_reader/directory_reader.py
text: @user1:
why remove safe path check?

@author:
this file_path.parent.parent was enforcing some hierarchy to the component's folder that is not necessary and confuses users, now you can have a folder with components and that work, you don't need to have a folder with a child folder to work

@user1:
This check makes sure we load files only one level deep.

@user1:
We should not remove this as it will break other things.