src/features/asset/components/AssetLibrary.tsx
line 57
import { useAssetSelection } from "../hooks/useAssetSelection";
import { useAssetFolderOrganization } from "../hooks/folder/useAssetFolderOrganization";
import { withConcurrency } from "../hooks/folder/useFolderAssetImport";
import { MAX_FOLDER_ASSET_ENTRIES } from "../utils/folder/folderImport";
import { useAssetFolderViewState } from "../hooks/folder/useAssetFolderViewState";
import { sortAssetFolderCards } from "../hooks/folder/useAssetFolders";
line 105
}
/** Uploads in flight at once from one pick or drop. */
const UPLOAD_CONCURRENCY = 4;
export function AssetLibrary({ brandId }: { brandId: string }) {
const t = useTranslations("asset.library");
line 499
return;
}
// Said BEFORE anything is sent, with the number, so a big drop is not a silent
// partial upload. Same ceiling as a folder import.
if (files.length > MAX_FOLDER_ASSET_ENTRIES) {
errorToast.show(
new Error(
t("tooManyUploadsDetail", {
count: files.length,
limit: MAX_FOLDER_ASSET_ENTRIES,
})
),
{ title: t("tooManyUploads", { limit: MAX_FOLDER_ASSET_ENTRIES }) }
);
return;
}
setUploadingFiles(files.map((f) => f.name));
const remaining = new Set(files.map((f) => f.name));
const uploadFolderId = openFolderId;
const outcomes = await Promise.allSettled(
files.map(async (file) => {
try {
// By NAME, not `file.type`: a `.mov` the OS never mapped arrives with `type
// === ""` and would otherwise be uploaded as `mode: "plain"` when it should
// declare itself a video.
const uploadMode: AssetUploadMode =
familyForName(file.name) === "video" ? "video" : "plain";
const result = await uploadBrandAsset(brandId, file, uploadMode);
if (uploadFolderId) {
await setAssetFolderId(brandId, result.assetId, uploadFolderId);
}
} finally {
remaining.delete(file.name);
setUploadingFiles([...remaining]);
const uploadOne = async (file: File): Promise<void> => {
try {
// By NAME, not `file.type`: a `.mov` the OS never mapped arrives with `type
// === ""` and would otherwise be uploaded as `mode: "plain"` when it should
// declare itself a video.
const uploadMode: AssetUploadMode =
familyForName(file.name) === "video" ? "video" : "plain";
const result = await uploadBrandAsset(brandId, file, uploadMode);
if (uploadFolderId) {
await setAssetFolderId(brandId, result.assetId, uploadFolderId);
}
})
);
} finally {
remaining.delete(file.name);
setUploadingFiles([...remaining]);
}
};
const outcomes: PromiseSettledResult<void>[] = [];
// A few at a time, not all at once, so a big drop does not open hundreds of
// requests together. Pacing does not cap requests per minute: where the API's
// rate limit is on, a large batch can still fail partway, and the toast below
// counts the files that did.
await withConcurrency(files, UPLOAD_CONCURRENCY, async (file, index) => {
outcomes[index] = await uploadOne(file).then(
(): PromiseFulfilledResult<void> => ({
status: "fulfilled",
value: undefined,
}),
(reason: unknown): PromiseRejectedResult => ({
status: "rejected",
reason,
})
);
});
setUploadingFiles([]);
const rejected = outcomes.filter(
src/features/asset/hooks/folder/useFolderAssetImport.tsx
line 116
/** Runs `worker` over `items` with at most `limit` in flight at once. */
async function withConcurrency<T>(
export async function withConcurrency<T>(
items: readonly T[],
limit: number,
src/i18n/locales/vi.json
line 1299
"uploadsFailed": "{count} lượt tải lên thất bại",
"checkGuidelinesFailed": "Không kiểm tra được nguyên tắc thương hiệu",
"subtitle": "Logo, ảnh sản phẩm, font để AI dùng làm đầu vào. Ấn phẩm AI tạo nằm ở Trang chủ."
"subtitle": "Logo, ảnh sản phẩm, font để AI dùng làm đầu vào. Ấn phẩm AI tạo nằm ở Trang chủ.",
"tooManyUploads": "Tối đa {limit} tệp mỗi lần tải lên",
"tooManyUploadsDetail": "Bạn đã chọn {count} tệp. Mỗi lần chỉ tải lên tối đa {limit} tệp. Chưa tệp nào được tải lên, hãy chia nhỏ rồi thử lại."
},
"bulkDelete": {