feat(scripts/data-resources): merge tree + add parent_path filter
- Backend: GET /api/v1/data-resources accepts parent_path; LIKE
'{ws_id}/%/{escaped}/%' AND NOT LIKE '{ws_id}/%/{escaped}/%/%' on
StorageObjects.object_key (workspace-wide, escapes _ and %, mirrors
list_scripts parent_path semantics). 13 new tests in
test_resources.py (helper unit / SQL compile / SQLite behavioral).
- Frontend: listResources gains parentPath arg, propagated through
WorkspaceBoundApi + AuthContext binding. WorkspaceTreeGroup title
count and ScriptExplorer header count now include dataResources.
memberScriptGroups backfills data-only owners so users with only
data resources still render a group. loadDataResources accepts an
optional parentPath, default empty preserves prior behavior.
This commit is contained in:
@@ -94,6 +94,14 @@ export function ScriptExplorer({
|
||||
byOwner.set(item.owner_user_id, list);
|
||||
}
|
||||
|
||||
// data-only owner(只有数据资源、没有 scripts 的用户)也要出现在分组里,
|
||||
// 因为 data resources 与 scripts 共享同一棵目录树。
|
||||
for (const ownerUserId of dataByOwner.keys()) {
|
||||
if (!byOwner.has(ownerUserId)) {
|
||||
byOwner.set(ownerUserId, []);
|
||||
}
|
||||
}
|
||||
|
||||
const groups: {
|
||||
user: AuthUser | null;
|
||||
scripts: ScriptItem[];
|
||||
@@ -143,7 +151,7 @@ export function ScriptExplorer({
|
||||
<div className="explorer__header">
|
||||
<div>
|
||||
<h2>脚本目录</h2>
|
||||
<span>{scripts.length} 个工作副本</span>
|
||||
<span>{scripts.length + dataResources.length} 个工作副本</span>
|
||||
</div>
|
||||
<div className="explorer__actions">
|
||||
<button
|
||||
|
||||
@@ -223,7 +223,8 @@ export function useApi(): WorkspaceBoundApi {
|
||||
return useMemo<WorkspaceBoundApi>(() => ({
|
||||
listScripts: (parentPath) => rawApi.listScripts(workspaceId, parentPath),
|
||||
countScripts: () => rawApi.countScripts(workspaceId),
|
||||
listResources: (opts) => rawApi.listResources(workspaceId, opts),
|
||||
listResources: (parentPath, opts) =>
|
||||
rawApi.listResources(workspaceId, parentPath, opts),
|
||||
createScript: (input) => rawApi.createScript(workspaceId, input),
|
||||
uploadScript: (file, parentPath, visibility) =>
|
||||
rawApi.uploadScript(workspaceId, file, parentPath, visibility),
|
||||
|
||||
@@ -150,7 +150,7 @@ export function WorkspaceTreeGroup({
|
||||
<Icon name="chevron" size={14} />
|
||||
<Icon name="folder" size={17} />
|
||||
<span>{title}</span>
|
||||
<em>{scripts.length}</em>
|
||||
<em>{scripts.length + (dataResources ?? []).length}</em>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="tree-group__items">
|
||||
|
||||
@@ -106,7 +106,7 @@ type State = {
|
||||
setKeyword: (keyword: string) => void;
|
||||
reset: () => void;
|
||||
load: (silent?: boolean) => Promise<void>;
|
||||
loadDataResources: () => Promise<void>;
|
||||
loadDataResources: (parentPath?: string) => Promise<void>;
|
||||
selectScript: (id: string | null) => void;
|
||||
openTab: (id: string) => void;
|
||||
closeTab: (id: string, event?: { stopPropagation: () => void }) => Promise<void>;
|
||||
@@ -383,11 +383,11 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
loadDataResources: async () => {
|
||||
loadDataResources: async (parentPath = "") => {
|
||||
const api = requireApi();
|
||||
set({ dataResourcesLoading: true });
|
||||
try {
|
||||
const list = await api.listResources();
|
||||
const list = await api.listResources(parentPath);
|
||||
set({ dataResources: Array.isArray(list) ? list : [] });
|
||||
} catch {
|
||||
set({ dataResources: [] });
|
||||
|
||||
@@ -437,9 +437,14 @@ export type ResourceItem = {
|
||||
|
||||
export async function listResources(
|
||||
workspaceId: string,
|
||||
parentPath: string = "",
|
||||
opts?: { visibility?: string; keyword?: string },
|
||||
): Promise<ResourceItem[]> {
|
||||
// Empty parentPath omits the query string entirely so the backend's
|
||||
// workspace-wide (root-level) filter is applied symmetrically with
|
||||
// non-empty paths, matching listScripts.
|
||||
const parameters = new URLSearchParams();
|
||||
if (parentPath) parameters.set("parent_path", parentPath);
|
||||
if (opts?.visibility) parameters.set("visibility", opts.visibility);
|
||||
if (opts?.keyword) parameters.set("keyword", opts.keyword);
|
||||
const query = parameters.toString();
|
||||
@@ -1484,7 +1489,8 @@ export type WorkspaceBoundApi = {
|
||||
) => Promise<ScriptItem[]>;
|
||||
countScripts: () => Promise<number>;
|
||||
listResources: (
|
||||
opts?: Parameters<typeof listResources>[1],
|
||||
parentPath?: Parameters<typeof listResources>[1],
|
||||
opts?: Parameters<typeof listResources>[2],
|
||||
) => Promise<ResourceItem[]>;
|
||||
createScript: (
|
||||
input: Parameters<typeof createScript>[1],
|
||||
|
||||
Reference in New Issue
Block a user