refactor: sort json before exporting and add tags to flows (#6849)

* feat: Enhance flow download with recursive JSON sorting

- Add `sortJsonStructure` function to recursively sort object keys and array elements
- Improve `downloadFlow` function to create deterministic JSON output
- Add error handling for flow download process
- Ensure consistent and predictable JSON file generation

* feat: Include flow tags in export modal payload

Add `tags` to the export modal payload to ensure comprehensive flow metadata is preserved during export

* 📝 (model.py): Add tags field to FlowRead and FlowHeader models for better organization and categorization of flows
 (use-post-add-flow.ts): Add support for adding tags to a new flow in the frontend
 (exportModal/index.tsx): Include tags when exporting a flow in the export modal
♻️ (index.tsx): Refactor ListComponent to remove unnecessary comments and improve code readability
🔧 (reactflowUtils.ts): Update createNewFlow function to include tags field when creating a new flow

* style: add blank line

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2025-03-05 09:06:26 -03:00 committed by GitHub
commit e5f654a752
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 66 additions and 24 deletions

View file

@ -200,6 +200,7 @@ class FlowRead(FlowBase):
id: UUID
user_id: UUID | None = Field()
folder_id: UUID | None = Field()
tags: list[str] | None = Field(None, description="The tags of the flow")
class FlowHeader(BaseModel):
@ -215,6 +216,7 @@ class FlowHeader(BaseModel):
endpoint_name: str | None = Field(None, description="The name of the endpoint associated with this flow")
description: str | None = Field(None, description="A description of the flow")
data: dict | None = Field(None, description="The data of the component, if is_component is True")
tags: list[str] | None = Field(None, description="The tags of the flow")
@field_validator("data", mode="before")
@classmethod

View file

@ -15,6 +15,7 @@ interface IPostAddFlow {
endpoint_name: string | undefined;
icon: string | undefined;
gradient: string | undefined;
tags: string[] | undefined;
}
export const usePostAddFlow: useMutationFunctionType<
@ -34,6 +35,7 @@ export const usePostAddFlow: useMutationFunctionType<
icon: payload.icon || null,
gradient: payload.gradient || null,
endpoint_name: payload.endpoint_name || null,
tags: payload.tags || null,
});
return response.data;
};

View file

@ -31,7 +31,6 @@ const ExportModal = forwardRef(
currentFlow?.description ?? "",
);
const [open, setOpen] = useState(false);
return (
<BaseModal
size="smaller-h-full"
@ -48,6 +47,7 @@ const ExportModal = forwardRef(
last_tested_version: version,
endpoint_name: currentFlow!.endpoint_name,
is_component: false,
tags: currentFlow!.tags,
},
name!,
description,
@ -65,6 +65,7 @@ const ExportModal = forwardRef(
last_tested_version: version,
endpoint_name: currentFlow!.endpoint_name,
is_component: false,
tags: currentFlow!.tags,
}),
name!,
description,

View file

@ -87,13 +87,11 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
} group justify-between rounded-lg border border-border p-4 hover:border-placeholder-foreground hover:shadow-sm`}
data-testid="list-card"
>
{/* left side */}
<div
className={`flex min-w-0 ${
isComponent ? "cursor-default" : "cursor-pointer"
} items-center gap-4`}
>
{/* Icon */}
<div
className={cn(
`item-center flex justify-center rounded-lg p-3`,
@ -124,7 +122,6 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
</div>
</div>
{/* right side */}
<div className="ml-5 flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>

View file

@ -1707,34 +1707,73 @@ export function extractFieldsFromComponenents(data: APIObjectType) {
});
return fields;
}
/**
* Recursively sorts all object keys and arrays in a JSON structure
* @param obj - The object to sort keys and arrays for
* @returns A new object with sorted keys and arrays
*/
function sortJsonStructure<T>(obj: T): T {
// Handle null case
if (obj === null) {
return obj;
}
// Handle arrays - sort array elements if they are objects
if (Array.isArray(obj)) {
return obj.map((item) => sortJsonStructure(item)) as unknown as T;
}
// Only process actual objects
if (typeof obj !== "object") {
return obj;
}
// Create a new object with sorted keys
return Object.keys(obj)
.sort()
.reduce((result, key) => {
// Recursively sort nested objects and arrays
result[key] = sortJsonStructure(obj[key]);
return result;
}, {} as any);
}
/**
* Downloads the flow as a JSON file with sorted keys and arrays
* @param flow - The flow to download
* @param flowName - The name to use for the flow
* @param flowDescription - Optional description for the flow
*/
export function downloadFlow(
flow: FlowType,
flowName: string,
flowDescription?: string,
) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(
{
...clonedFlow,
name: flowName,
description: flowDescription,
},
null,
2,
),
)}`;
try {
const clonedFlow = cloneDeep(flow);
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `${flowName && flowName != "" ? flowName : flow.name}.json`;
removeFileNameFromComponents(clonedFlow);
// simulate a click on the link element to trigger the download
link.click();
const flowData = {
...clonedFlow,
name: flowName,
description: flowDescription,
};
console.log(flowData);
const sortedData = sortJsonStructure(flowData);
const sortedJsonString = JSON.stringify(sortedData, null, 2);
const dataUri = `data:text/json;chatset=utf-8,${encodeURIComponent(sortedJsonString)}`;
const downloadLink = document.createElement("a");
downloadLink.href = dataUri;
downloadLink.download = `${flowName || flow.name}.json`;
downloadLink.click();
} catch (error) {
console.error("Error downloading flow:", error);
}
}
export function getRandomElement<T>(array: T[]): T {
@ -1760,6 +1799,7 @@ export const createNewFlow = (
is_component: flow?.is_component ?? false,
folder_id: folderId,
endpoint_name: flow?.endpoint_name ?? undefined,
tags: flow?.tags ?? [],
};
};