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:
parent
1cc49962d5
commit
e5f654a752
5 changed files with 66 additions and 24 deletions
|
|
@ -200,6 +200,7 @@ class FlowRead(FlowBase):
|
||||||
id: UUID
|
id: UUID
|
||||||
user_id: UUID | None = Field()
|
user_id: UUID | None = Field()
|
||||||
folder_id: UUID | None = Field()
|
folder_id: UUID | None = Field()
|
||||||
|
tags: list[str] | None = Field(None, description="The tags of the flow")
|
||||||
|
|
||||||
|
|
||||||
class FlowHeader(BaseModel):
|
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")
|
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")
|
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")
|
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")
|
@field_validator("data", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ interface IPostAddFlow {
|
||||||
endpoint_name: string | undefined;
|
endpoint_name: string | undefined;
|
||||||
icon: string | undefined;
|
icon: string | undefined;
|
||||||
gradient: string | undefined;
|
gradient: string | undefined;
|
||||||
|
tags: string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePostAddFlow: useMutationFunctionType<
|
export const usePostAddFlow: useMutationFunctionType<
|
||||||
|
|
@ -34,6 +35,7 @@ export const usePostAddFlow: useMutationFunctionType<
|
||||||
icon: payload.icon || null,
|
icon: payload.icon || null,
|
||||||
gradient: payload.gradient || null,
|
gradient: payload.gradient || null,
|
||||||
endpoint_name: payload.endpoint_name || null,
|
endpoint_name: payload.endpoint_name || null,
|
||||||
|
tags: payload.tags || null,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ const ExportModal = forwardRef(
|
||||||
currentFlow?.description ?? "",
|
currentFlow?.description ?? "",
|
||||||
);
|
);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
size="smaller-h-full"
|
size="smaller-h-full"
|
||||||
|
|
@ -48,6 +47,7 @@ const ExportModal = forwardRef(
|
||||||
last_tested_version: version,
|
last_tested_version: version,
|
||||||
endpoint_name: currentFlow!.endpoint_name,
|
endpoint_name: currentFlow!.endpoint_name,
|
||||||
is_component: false,
|
is_component: false,
|
||||||
|
tags: currentFlow!.tags,
|
||||||
},
|
},
|
||||||
name!,
|
name!,
|
||||||
description,
|
description,
|
||||||
|
|
@ -65,6 +65,7 @@ const ExportModal = forwardRef(
|
||||||
last_tested_version: version,
|
last_tested_version: version,
|
||||||
endpoint_name: currentFlow!.endpoint_name,
|
endpoint_name: currentFlow!.endpoint_name,
|
||||||
is_component: false,
|
is_component: false,
|
||||||
|
tags: currentFlow!.tags,
|
||||||
}),
|
}),
|
||||||
name!,
|
name!,
|
||||||
description,
|
description,
|
||||||
|
|
|
||||||
|
|
@ -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`}
|
} group justify-between rounded-lg border border-border p-4 hover:border-placeholder-foreground hover:shadow-sm`}
|
||||||
data-testid="list-card"
|
data-testid="list-card"
|
||||||
>
|
>
|
||||||
{/* left side */}
|
|
||||||
<div
|
<div
|
||||||
className={`flex min-w-0 ${
|
className={`flex min-w-0 ${
|
||||||
isComponent ? "cursor-default" : "cursor-pointer"
|
isComponent ? "cursor-default" : "cursor-pointer"
|
||||||
} items-center gap-4`}
|
} items-center gap-4`}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`item-center flex justify-center rounded-lg p-3`,
|
`item-center flex justify-center rounded-lg p-3`,
|
||||||
|
|
@ -124,7 +122,6 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* right side */}
|
|
||||||
<div className="ml-5 flex items-center gap-2">
|
<div className="ml-5 flex items-center gap-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -1707,34 +1707,73 @@ export function extractFieldsFromComponenents(data: APIObjectType) {
|
||||||
});
|
});
|
||||||
return fields;
|
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(
|
export function downloadFlow(
|
||||||
flow: FlowType,
|
flow: FlowType,
|
||||||
flowName: string,
|
flowName: string,
|
||||||
flowDescription?: string,
|
flowDescription?: string,
|
||||||
) {
|
) {
|
||||||
let clonedFlow = cloneDeep(flow);
|
try {
|
||||||
|
const clonedFlow = cloneDeep(flow);
|
||||||
|
|
||||||
removeFileNameFromComponents(clonedFlow);
|
removeFileNameFromComponents(clonedFlow);
|
||||||
// create a data URI with the current flow data
|
|
||||||
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
const flowData = {
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
...clonedFlow,
|
...clonedFlow,
|
||||||
name: flowName,
|
name: flowName,
|
||||||
description: flowDescription,
|
description: flowDescription,
|
||||||
},
|
};
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
// create a link element and set its properties
|
console.log(flowData);
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = jsonString;
|
|
||||||
link.download = `${flowName && flowName != "" ? flowName : flow.name}.json`;
|
|
||||||
|
|
||||||
// simulate a click on the link element to trigger the download
|
const sortedData = sortJsonStructure(flowData);
|
||||||
link.click();
|
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 {
|
export function getRandomElement<T>(array: T[]): T {
|
||||||
|
|
@ -1760,6 +1799,7 @@ export const createNewFlow = (
|
||||||
is_component: flow?.is_component ?? false,
|
is_component: flow?.is_component ?? false,
|
||||||
folder_id: folderId,
|
folder_id: folderId,
|
||||||
endpoint_name: flow?.endpoint_name ?? undefined,
|
endpoint_name: flow?.endpoint_name ?? undefined,
|
||||||
|
tags: flow?.tags ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue