补齐图片编辑器素材和画布搜索
普通上传默认落入上传素材文件夹 缺少上传素材文件夹时首次上传前自动创建 素材库和图层侧栏增加前缀优先搜索 更新图片画布技术文档和定向测试
This commit is contained in:
@@ -27,6 +27,7 @@ import { ImageCanvasAssetRowView } from './ImageCanvasAssetRowView';
|
||||
import type {
|
||||
GroupedEditorAssetFolder,
|
||||
UploadFilesOptions,
|
||||
UploadRequestOptions,
|
||||
} from './ImageCanvasAssetLibraryPanelView';
|
||||
|
||||
type ImageCanvasAssetFolderSectionViewProps = {
|
||||
@@ -50,7 +51,7 @@ type ImageCanvasAssetFolderSectionViewProps = {
|
||||
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
requestUpload: (target: UploadTarget, options?: UploadRequestOptions) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
toggleAssetFolder: (folderId: string) => void;
|
||||
startRenamingFolder: (folder: EditorAssetFolder) => void;
|
||||
@@ -220,7 +221,7 @@ export function ImageCanvasAssetFolderSectionView({
|
||||
icon={ImagePlus}
|
||||
onClick={() => {
|
||||
setActiveUploadFolderId(folder.id);
|
||||
requestUpload('asset');
|
||||
requestUpload('asset', { folderId: folder.id });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createAssetMarqueeSelectionRect,
|
||||
createLocalAssetFolder,
|
||||
deleteAssetFolderLocally,
|
||||
filterGroupedAssetsBySearch,
|
||||
getSelectableAssets,
|
||||
groupAssetsByFolder,
|
||||
moveAssetToFolderLocally,
|
||||
@@ -22,7 +23,9 @@ import {
|
||||
resolveAllAssetSelection,
|
||||
resolveAssetFolderIdFromPoint,
|
||||
resolveDefaultAssetFolder,
|
||||
resolvePreferredAssetUploadFolder,
|
||||
resolvePinnedAssetMoveFolderId,
|
||||
resolveUploadAssetFolder,
|
||||
selectUploadedAssetsInRect,
|
||||
toggleAssetFolderCollapsed,
|
||||
toggleAssetSelection,
|
||||
@@ -86,6 +89,48 @@ describe('ImageCanvasAssetLibraryModel', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters grouped assets by folder and asset search text', () => {
|
||||
const groupedAssets = groupAssetsByFolder(
|
||||
[
|
||||
createFolder(),
|
||||
createFolder({ id: 'folder-uploaded', label: '上传素材' }),
|
||||
],
|
||||
[
|
||||
createAsset({ id: 'asset-a', label: '草地角色', folderId: 'project' }),
|
||||
createAsset({
|
||||
id: 'asset-b',
|
||||
label: '音效',
|
||||
folderId: 'folder-uploaded',
|
||||
model: 'audio1.0',
|
||||
}),
|
||||
createAsset({
|
||||
id: 'asset-c',
|
||||
label: '角色音效',
|
||||
folderId: 'folder-uploaded',
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
filterGroupedAssetsBySearch(groupedAssets, 'audio').map((folder) => ({
|
||||
id: folder.id,
|
||||
assets: folder.assets.map((asset) => asset.id),
|
||||
})),
|
||||
).toEqual([{ id: 'folder-uploaded', assets: ['asset-b'] }]);
|
||||
expect(
|
||||
filterGroupedAssetsBySearch(groupedAssets, '音').map((folder) => ({
|
||||
id: folder.id,
|
||||
assets: folder.assets.map((asset) => asset.id),
|
||||
})),
|
||||
).toEqual([{ id: 'folder-uploaded', assets: ['asset-b', 'asset-c'] }]);
|
||||
expect(
|
||||
filterGroupedAssetsBySearch(groupedAssets, '上传').map((folder) => ({
|
||||
id: folder.id,
|
||||
assets: folder.assets.map((asset) => asset.id),
|
||||
})),
|
||||
).toEqual([{ id: 'folder-uploaded', assets: ['asset-b', 'asset-c'] }]);
|
||||
});
|
||||
|
||||
it('renames assets, toggles folders and creates local folders', () => {
|
||||
expect(renameAssetById([createAsset()], 'asset-a', '新名字')[0]).toMatchObject(
|
||||
{ label: '新名字' },
|
||||
@@ -147,6 +192,13 @@ describe('ImageCanvasAssetLibraryModel', () => {
|
||||
'asset-b',
|
||||
]);
|
||||
expect(resolveDefaultAssetFolder(folders)?.id).toBe('project');
|
||||
expect(
|
||||
resolveUploadAssetFolder([
|
||||
createFolder(),
|
||||
createFolder({ id: 'folder-uploaded', label: '上传素材' }),
|
||||
])?.id,
|
||||
).toBe('folder-uploaded');
|
||||
expect(resolvePreferredAssetUploadFolder(folders)?.id).toBe('project');
|
||||
expect(
|
||||
deleteAssetFolderLocally({
|
||||
folders,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AssetMarqueeState,
|
||||
CanvasLayer,
|
||||
EditorAsset,
|
||||
EditorAssetFolder,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
@@ -25,6 +26,8 @@ export type GroupedAssetFolder = EditorAssetFolder & {
|
||||
assets: EditorAsset[];
|
||||
};
|
||||
|
||||
export const EDITOR_UPLOAD_ASSET_FOLDER_LABEL = '上传素材';
|
||||
|
||||
function isPointInRect(point: ClientPoint, rect: ClientRectLike) {
|
||||
return (
|
||||
point.clientX >= rect.left &&
|
||||
@@ -183,6 +186,104 @@ export function groupAssetsByFolder(
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeSearchQuery(query: string) {
|
||||
return query.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function matchesSearchText(query: string, values: Array<string | null | undefined>) {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
return values.some((value) => value?.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
function startsWithSearchText(query: string, values: Array<string | null | undefined>) {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
return values.some((value) => value?.trim().toLowerCase().startsWith(query));
|
||||
}
|
||||
|
||||
function buildAssetSearchValues(asset: EditorAsset) {
|
||||
return [
|
||||
asset.label,
|
||||
asset.prompt,
|
||||
asset.actualPrompt,
|
||||
asset.model,
|
||||
asset.provider,
|
||||
asset.taskId,
|
||||
asset.assetKind,
|
||||
asset.mediaType,
|
||||
];
|
||||
}
|
||||
|
||||
function buildLayerSearchValues(layer: CanvasLayer) {
|
||||
return [
|
||||
layer.title,
|
||||
layer.prompt,
|
||||
layer.actualPrompt,
|
||||
layer.model,
|
||||
layer.provider,
|
||||
layer.taskId,
|
||||
layer.assetKind,
|
||||
layer.mediaType,
|
||||
];
|
||||
}
|
||||
|
||||
function assetMatchesSearch(asset: EditorAsset, query: string) {
|
||||
return matchesSearchText(query, buildAssetSearchValues(asset));
|
||||
}
|
||||
|
||||
function assetStartsWithSearch(asset: EditorAsset, query: string) {
|
||||
return startsWithSearchText(query, buildAssetSearchValues(asset));
|
||||
}
|
||||
|
||||
export function layerMatchesSearch(layer: CanvasLayer, query: string) {
|
||||
return matchesSearchText(query, buildLayerSearchValues(layer));
|
||||
}
|
||||
|
||||
export function layerStartsWithSearch(layer: CanvasLayer, query: string) {
|
||||
return startsWithSearchText(query, buildLayerSearchValues(layer));
|
||||
}
|
||||
|
||||
export function filterGroupedAssetsBySearch(
|
||||
groupedAssets: GroupedAssetFolder[],
|
||||
query: string,
|
||||
) {
|
||||
const normalizedQuery = normalizeSearchQuery(query);
|
||||
if (!normalizedQuery) {
|
||||
return groupedAssets;
|
||||
}
|
||||
|
||||
return groupedAssets
|
||||
.map((folder) => {
|
||||
const folderMatches =
|
||||
startsWithSearchText(normalizedQuery, [folder.label]) ||
|
||||
matchesSearchText(normalizedQuery, [folder.label]);
|
||||
const prefixAssets = folder.assets.filter((asset) =>
|
||||
assetStartsWithSearch(asset, normalizedQuery),
|
||||
);
|
||||
const matchingAssets = folder.assets.filter((asset) =>
|
||||
assetMatchesSearch(asset, normalizedQuery),
|
||||
);
|
||||
return {
|
||||
...folder,
|
||||
assets: folderMatches
|
||||
? folder.assets
|
||||
: [
|
||||
...prefixAssets,
|
||||
...matchingAssets.filter(
|
||||
(asset) =>
|
||||
!prefixAssets.some(
|
||||
(prefixAsset) => prefixAsset.id === asset.id,
|
||||
),
|
||||
),
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter((folder) => folder.assets.length > 0);
|
||||
}
|
||||
|
||||
export function getSelectableAssets(assets: EditorAsset[]) {
|
||||
return assets.filter((asset) => asset.sourceKind === 'uploaded');
|
||||
}
|
||||
@@ -306,6 +407,20 @@ export function resolveDefaultAssetFolder(assetFolders: EditorAssetFolder[]) {
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveUploadAssetFolder(assetFolders: EditorAssetFolder[]) {
|
||||
return (
|
||||
assetFolders.find(
|
||||
(folder) => folder.label.trim() === EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePreferredAssetUploadFolder(
|
||||
assetFolders: EditorAssetFolder[],
|
||||
) {
|
||||
return resolveUploadAssetFolder(assetFolders) ?? resolveDefaultAssetFolder(assetFolders);
|
||||
}
|
||||
|
||||
export function deleteAssetFolderLocally({
|
||||
folders,
|
||||
assets,
|
||||
|
||||
@@ -36,6 +36,11 @@ export type UploadFilesOptions = {
|
||||
addToCanvas?: boolean;
|
||||
};
|
||||
|
||||
export type UploadRequestOptions = {
|
||||
folderId?: string;
|
||||
addToCanvas?: boolean;
|
||||
};
|
||||
|
||||
export type ImageCanvasAssetLibraryPanelViewProps = {
|
||||
assetListRef: RefObject<HTMLDivElement | null>;
|
||||
assetPointerDragRef: { current: AssetPointerDragState | null };
|
||||
@@ -75,7 +80,7 @@ export type ImageCanvasAssetLibraryPanelViewProps = {
|
||||
) => void;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
requestUpload: (target: UploadTarget, options?: UploadRequestOptions) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
commitNewAssetFolder: () => void | Promise<void>;
|
||||
toggleAssetFolder: (folderId: string) => void;
|
||||
|
||||
@@ -226,6 +226,37 @@ describe('ImageCanvasSidebarView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('filters assets from the sidebar search field', () => {
|
||||
const folder = createFolder();
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
{...createSidebarProps({
|
||||
assets: [
|
||||
createAsset({ id: 'asset-a', label: '账号素材A' }),
|
||||
createAsset({ id: 'asset-b', label: '火焰按钮' }),
|
||||
],
|
||||
groupedAssets: [
|
||||
{
|
||||
...folder,
|
||||
assets: [
|
||||
createAsset({ id: 'asset-a', label: '账号素材A' }),
|
||||
createAsset({ id: 'asset-b', label: '火焰按钮' }),
|
||||
],
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||||
fireEvent.change(within(sidebar).getByLabelText('搜索素材'), {
|
||||
target: { value: '火焰' },
|
||||
});
|
||||
|
||||
expect(within(sidebar).getByRole('button', { name: '添加火焰按钮' })).toBeTruthy();
|
||||
expect(within(sidebar).queryByRole('button', { name: '添加账号素材A' })).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the layer panel with grouped and locked layer details', () => {
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
@@ -255,6 +286,32 @@ describe('ImageCanvasSidebarView', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('filters canvas layers from the sidebar search field', () => {
|
||||
render(
|
||||
<ImageCanvasSidebarView
|
||||
{...createSidebarProps({
|
||||
activeSidebarPanel: 'layers',
|
||||
layers: [
|
||||
createLayer({ id: 'layer-a', title: '角色草图', zIndex: 3 }),
|
||||
createLayer({ id: 'layer-b', title: '背景山脉', zIndex: 2 }),
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||||
fireEvent.change(within(sidebar).getByLabelText('搜索画布图层'), {
|
||||
target: { value: '背景' },
|
||||
});
|
||||
|
||||
expect(
|
||||
within(sidebar).getByRole('button', { name: '选择图层背景山脉' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(sidebar).queryByRole('button', { name: '选择图层角色草图' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps sidebar tab switching and repeated-click closing behavior', () => {
|
||||
render(<SidebarTabsHarness />);
|
||||
|
||||
|
||||
@@ -9,12 +9,20 @@ import type {
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import {
|
||||
ImageCanvasAssetLibraryPanelView,
|
||||
type GroupedEditorAssetFolder,
|
||||
type UploadFilesOptions,
|
||||
type UploadRequestOptions,
|
||||
} from './ImageCanvasAssetLibraryPanelView';
|
||||
import {
|
||||
filterGroupedAssetsBySearch,
|
||||
layerMatchesSearch,
|
||||
layerStartsWithSearch,
|
||||
} from './ImageCanvasAssetLibraryModel';
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type {
|
||||
AssetMarqueeState,
|
||||
@@ -78,7 +86,7 @@ export type ImageCanvasSidebarViewProps = {
|
||||
) => void;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
requestUpload: (target: UploadTarget, options?: UploadRequestOptions) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
commitNewAssetFolder: () => void | Promise<void>;
|
||||
toggleAssetFolder: (folderId: string) => void;
|
||||
@@ -111,7 +119,6 @@ export function ImageCanvasSidebarView({
|
||||
assetListRef,
|
||||
assetPointerDragRef,
|
||||
suppressAssetClickRef,
|
||||
assets,
|
||||
groupedAssets,
|
||||
assetFolders,
|
||||
layers,
|
||||
@@ -163,6 +170,38 @@ export function ImageCanvasSidebarView({
|
||||
resolveContextMenuPosition,
|
||||
getCanvasPointFromClient,
|
||||
}: ImageCanvasSidebarViewProps) {
|
||||
const [assetSearchQuery, setAssetSearchQuery] = useState('');
|
||||
const [layerSearchQuery, setLayerSearchQuery] = useState('');
|
||||
const filteredGroupedAssets = useMemo(
|
||||
() => filterGroupedAssetsBySearch(groupedAssets, assetSearchQuery),
|
||||
[assetSearchQuery, groupedAssets],
|
||||
);
|
||||
const filteredAssetsCount = useMemo(
|
||||
() =>
|
||||
filteredGroupedAssets.reduce(
|
||||
(totalCount, folder) => totalCount + folder.assets.length,
|
||||
0,
|
||||
),
|
||||
[filteredGroupedAssets],
|
||||
);
|
||||
const normalizedLayerSearchQuery = layerSearchQuery.trim().toLowerCase();
|
||||
const filteredLayers = useMemo(
|
||||
() =>
|
||||
normalizedLayerSearchQuery
|
||||
? [
|
||||
...layers.filter((layer) =>
|
||||
layerStartsWithSearch(layer, normalizedLayerSearchQuery),
|
||||
),
|
||||
...layers.filter(
|
||||
(layer) =>
|
||||
!layerStartsWithSearch(layer, normalizedLayerSearchQuery) &&
|
||||
layerMatchesSearch(layer, normalizedLayerSearchQuery),
|
||||
),
|
||||
]
|
||||
: layers,
|
||||
[layers, normalizedLayerSearchQuery],
|
||||
);
|
||||
|
||||
if (!activeSidebarPanel) {
|
||||
return null;
|
||||
}
|
||||
@@ -175,7 +214,9 @@ export function ImageCanvasSidebarView({
|
||||
{activeSidebarPanel === 'assets' ? '素材' : '图层'}
|
||||
</h2>
|
||||
<div className="image-canvas-editor__sidebar-count">
|
||||
{activeSidebarPanel === 'assets' ? assets.length : layers.length}
|
||||
{activeSidebarPanel === 'assets'
|
||||
? filteredAssetsCount
|
||||
: filteredLayers.length}
|
||||
</div>
|
||||
</div>
|
||||
{activeSidebarPanel === 'assets' ? (
|
||||
@@ -210,12 +251,34 @@ export function ImageCanvasSidebarView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="image-canvas-editor__sidebar-search">
|
||||
<PlatformTextField
|
||||
aria-label={
|
||||
activeSidebarPanel === 'assets' ? '搜索素材' : '搜索画布图层'
|
||||
}
|
||||
placeholder={activeSidebarPanel === 'assets' ? '搜索素材' : '搜索图层'}
|
||||
value={
|
||||
activeSidebarPanel === 'assets' ? assetSearchQuery : layerSearchQuery
|
||||
}
|
||||
size="xs"
|
||||
density="compact"
|
||||
className="image-canvas-editor__sidebar-search-input"
|
||||
onChange={(event) => {
|
||||
if (activeSidebarPanel === 'assets') {
|
||||
setAssetSearchQuery(event.target.value);
|
||||
} else {
|
||||
setLayerSearchQuery(event.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{activeSidebarPanel === 'assets' ? (
|
||||
<ImageCanvasAssetLibraryPanelView
|
||||
assetListRef={assetListRef}
|
||||
assetPointerDragRef={assetPointerDragRef}
|
||||
suppressAssetClickRef={suppressAssetClickRef}
|
||||
groupedAssets={groupedAssets}
|
||||
groupedAssets={filteredGroupedAssets}
|
||||
assetFolders={assetFolders}
|
||||
isAssetSelectionMode={isAssetSelectionMode}
|
||||
selectedAssetIds={selectedAssetIds}
|
||||
@@ -258,7 +321,7 @@ export function ImageCanvasSidebarView({
|
||||
/>
|
||||
) : (
|
||||
<ImageCanvasLayerPanelView
|
||||
layers={layers}
|
||||
layers={filteredLayers}
|
||||
selectedLayerId={selectedLayerId}
|
||||
selectedLayerIds={selectedLayerIds}
|
||||
setImageContextMenu={setImageContextMenu}
|
||||
|
||||
@@ -105,6 +105,16 @@ describe('ImageCanvasUploadModel', () => {
|
||||
activeUploadFolderId: 'characters',
|
||||
}),
|
||||
).toBe('project');
|
||||
expect(
|
||||
resolveUploadFolderId({
|
||||
assetFolders: [
|
||||
...folders,
|
||||
createFolder({ id: 'uploaded', label: '上传素材', systemDefault: false }),
|
||||
],
|
||||
activeUploadFolderId: 'characters',
|
||||
preferUploadFolder: true,
|
||||
}),
|
||||
).toBe('uploaded');
|
||||
});
|
||||
|
||||
it('creates uploading asset placeholders', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
GenerateDialogState,
|
||||
QuickEditPanelState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { resolvePreferredAssetUploadFolder } from './ImageCanvasAssetLibraryModel';
|
||||
import { appendLimitedQuickEditReferences } from './ImageCanvasGenerationModel';
|
||||
|
||||
type CanvasSize = { width: number; height: number };
|
||||
@@ -73,12 +74,19 @@ export function resolveUploadFolderId({
|
||||
assetFolders,
|
||||
requestedFolderId,
|
||||
activeUploadFolderId,
|
||||
preferUploadFolder = false,
|
||||
}: {
|
||||
assetFolders: EditorAssetFolder[];
|
||||
requestedFolderId?: string;
|
||||
activeUploadFolderId: string;
|
||||
preferUploadFolder?: boolean;
|
||||
}) {
|
||||
const targetFolderId = requestedFolderId ?? activeUploadFolderId;
|
||||
const targetFolderId =
|
||||
requestedFolderId ??
|
||||
(preferUploadFolder
|
||||
? resolvePreferredAssetUploadFolder(assetFolders)?.id
|
||||
: activeUploadFolderId) ??
|
||||
activeUploadFolderId;
|
||||
return assetFolders.some((folder) => folder.id === targetFolderId)
|
||||
? targetFolderId
|
||||
: 'project';
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
import { useImageCanvasUploadWorkflow } from './useImageCanvasUploadWorkflow';
|
||||
|
||||
const createEditorAssetMock = vi.hoisted(() => vi.fn());
|
||||
const createEditorAssetFolderMock = vi.hoisted(() => vi.fn());
|
||||
const createEditorProjectResourceMock = vi.hoisted(() => vi.fn());
|
||||
const uploadEditorSeedanceReferenceFileMock = vi.hoisted(() => vi.fn());
|
||||
const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn());
|
||||
@@ -35,6 +36,7 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
return {
|
||||
...actual,
|
||||
createEditorAsset: createEditorAssetMock,
|
||||
createEditorAssetFolder: createEditorAssetFolderMock,
|
||||
createEditorProjectResource: createEditorProjectResourceMock,
|
||||
};
|
||||
});
|
||||
@@ -338,6 +340,12 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
objectKey: 'object-key-uploaded',
|
||||
assetObjectId: 'asset-object-uploaded',
|
||||
}));
|
||||
createEditorAssetFolderMock.mockResolvedValue({
|
||||
folderId: 'folder-uploaded',
|
||||
label: '上传素材',
|
||||
collapsed: false,
|
||||
systemDefault: false,
|
||||
});
|
||||
createEditorProjectResourceMock.mockImplementation(
|
||||
async (projectId, input) => ({
|
||||
resourceId: `resource-${input.objectKey ?? input.imageSrc.slice(0, 8)}`,
|
||||
@@ -421,9 +429,10 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('assets').textContent).toContain(
|
||||
'persisted-上传素材.png:上传素材.png:project:ready:-',
|
||||
'persisted-上传素材.png:上传素材.png:folder-uploaded:ready:-',
|
||||
);
|
||||
});
|
||||
expect(createEditorAssetFolderMock).toHaveBeenCalledWith('上传素材', 101);
|
||||
});
|
||||
|
||||
it('opens login instead of the asset file picker when protected data is unavailable', () => {
|
||||
@@ -483,7 +492,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('assets').textContent).toContain(
|
||||
'upload-1:画布素材.png:project:uploading:上传中',
|
||||
'upload-1:画布素材.png:folder-uploaded:uploading:上传中',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-upload-1:画布素材.png:upload-1:-160:-107.5',
|
||||
@@ -496,7 +505,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
|
||||
deferredAsset.resolve({
|
||||
assetId: 'asset-persisted-canvas',
|
||||
folderId: 'project',
|
||||
folderId: 'folder-uploaded',
|
||||
label: '画布素材.png',
|
||||
imageSrc: 'data:image/png;base64,Y2FudmFz',
|
||||
width: 420,
|
||||
@@ -508,7 +517,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('assets').textContent).toContain(
|
||||
'asset-persisted-canvas:画布素材.png:project:ready:-',
|
||||
'asset-persisted-canvas:画布素材.png:folder-uploaded:ready:-',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-upload-1:画布素材.png:asset-persisted-canvas:-160:-107.5',
|
||||
@@ -533,7 +542,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('assets').textContent).toContain(
|
||||
'persisted-原始尺寸素材.png:原始尺寸素材.png:project:ready:-:1536x1024',
|
||||
'persisted-原始尺寸素材.png:原始尺寸素材.png:folder-uploaded:ready:-:1536x1024',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-upload-1:原始尺寸素材.png:persisted-原始尺寸素材.png:-718:-462:1536x1024',
|
||||
@@ -558,7 +567,7 @@ describe('useImageCanvasUploadWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId('assets').textContent).toContain(
|
||||
'upload-1:上传素材.png:project:failed:请先登录',
|
||||
'upload-1:上传素材.png:folder-uploaded:failed:请先登录',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ApiClientError } from '../../services/apiClient';
|
||||
import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
|
||||
import {
|
||||
createEditorAsset,
|
||||
createEditorAssetFolder,
|
||||
createEditorProjectResource,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { uploadEditorSeedanceReferenceFile } from '../../services/image-editor/editorReferenceUploadClient';
|
||||
@@ -26,6 +27,11 @@ import type {
|
||||
QuickEditPanelState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
|
||||
resolvePreferredAssetUploadFolder,
|
||||
resolveUploadAssetFolder,
|
||||
} from './ImageCanvasAssetLibraryModel';
|
||||
import {
|
||||
SEEDANCE_REFERENCE_FILE_LIMITS,
|
||||
isImageFile,
|
||||
@@ -64,6 +70,7 @@ type UploadFilesOptions = {
|
||||
|
||||
type UploadRequestOptions = {
|
||||
addToCanvas?: boolean;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
type UploadAssetFileOptions = UploadFilesOptions & {
|
||||
@@ -128,6 +135,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
const canAccessProtectedDataRef = useRef(canAccessProtectedData);
|
||||
const uploadTargetRef = useRef<UploadTarget>('asset');
|
||||
const uploadRequestOptionsRef = useRef<UploadRequestOptions>({});
|
||||
const uploadAssetFolderCreatePromiseRef = useRef<Promise<string> | null>(null);
|
||||
const [uploadTarget, setUploadTargetState] = useState<UploadTarget>('asset');
|
||||
|
||||
canAccessProtectedDataRef.current = canAccessProtectedData;
|
||||
@@ -814,11 +822,48 @@ export function useImageCanvasUploadWorkflow({
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadFolderId = resolveUploadFolderId({
|
||||
let uploadFolderId = resolveUploadFolderId({
|
||||
assetFolders,
|
||||
requestedFolderId: options.folderId,
|
||||
activeUploadFolderId,
|
||||
preferUploadFolder: !options.folderId,
|
||||
});
|
||||
if (!options.folderId && !resolveUploadAssetFolder(assetFolders)) {
|
||||
if (!uploadAssetFolderCreatePromiseRef.current) {
|
||||
uploadAssetFolderCreatePromiseRef.current = createEditorAssetFolder(
|
||||
EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
|
||||
assetFolders.length + 100,
|
||||
)
|
||||
.then((folder) => {
|
||||
setAssetFolders((currentFolders) =>
|
||||
currentFolders.some(
|
||||
(currentFolder) => currentFolder.id === folder.folderId,
|
||||
)
|
||||
? currentFolders
|
||||
: [
|
||||
...currentFolders,
|
||||
{
|
||||
id: folder.folderId,
|
||||
label: folder.label,
|
||||
collapsed: folder.collapsed,
|
||||
systemDefault: folder.systemDefault,
|
||||
persisted: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
return folder.folderId;
|
||||
})
|
||||
.catch(() => {
|
||||
const fallbackFolder =
|
||||
resolvePreferredAssetUploadFolder(assetFolders);
|
||||
return fallbackFolder?.id ?? 'project';
|
||||
})
|
||||
.finally(() => {
|
||||
uploadAssetFolderCreatePromiseRef.current = null;
|
||||
});
|
||||
}
|
||||
uploadFolderId = await uploadAssetFolderCreatePromiseRef.current;
|
||||
}
|
||||
const uploadIndex = options.uploadIndex;
|
||||
let imageDimensions: { width: number; height: number } | null = null;
|
||||
if (mediaType === 'image') {
|
||||
@@ -1072,6 +1117,7 @@ export function useImageCanvasUploadWorkflow({
|
||||
} else {
|
||||
const requestOptions = uploadRequestOptionsRef.current;
|
||||
addUploadedFiles(files, {
|
||||
folderId: requestOptions.folderId,
|
||||
addToCanvas:
|
||||
requestOptions.addToCanvas ?? activeTool === 'upload',
|
||||
});
|
||||
|
||||
@@ -4255,6 +4255,18 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__sidebar-search {
|
||||
border-bottom: 1px solid var(--image-canvas-brand-border-soft);
|
||||
padding: 0.55rem 0.65rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__sidebar-search-input {
|
||||
height: 2rem;
|
||||
border-radius: 0.45rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-list {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user