Skip to content

Commit 76c436c

Browse files
authored
feat: allow changing asset name (#5367)
Ref #3262 #3272 This solves a couple of issues. 1. Gives a user ability to change name of uploaded asset. 2. strips internal suffix from filenames in UI <img width="626" height="541" alt="magic_1" src="https://github.com/user-attachments/assets/f9c91ac8-cc67-4905-9e86-bf0cbbeae14a" />
1 parent fb466ca commit 76c436c

File tree

30 files changed

+342
-119
lines changed

30 files changed

+342
-119
lines changed

apps/builder/app/builder/features/pages/image-info.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ImageIcon,
1313
} from "@webstudio-is/icons";
1414
import type { ImageAsset } from "@webstudio-is/sdk";
15+
import { formatAssetName } from "~/builder/shared/assets/asset-utils";
1516
import { getFormattedAspectRatio } from "~/builder/shared/image-manager/utils";
1617

1718
type ImageInfoProps = {
@@ -42,10 +43,14 @@ export const ImageInfo = ({ asset, onDelete }: ImageInfoProps) => {
4243
gap={2}
4344
align={"center"}
4445
>
45-
<Grid flow={"column"} gap={1} align={"center"}>
46+
<Grid
47+
gap={1}
48+
align="center"
49+
css={{ gridTemplateColumns: "max-content 1fr" }}
50+
>
4651
<ImageIcon />
47-
<Text truncate variant={"labelsTitleCase"}>
48-
{asset.name}
52+
<Text truncate variant={"labelsSentenceCase"}>
53+
{formatAssetName(asset)}
4954
</Text>
5055
</Grid>
5156
<Grid columns={2} gap={1} align={"center"}>

apps/builder/app/builder/features/pages/page-settings.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ $project.set({
7272
projectId: "projectId",
7373
id: "imageId",
7474
name: "very-very-very-long-long-image-name.jpg",
75+
filename: null,
76+
description: null,
7577
},
7678
latestBuildVirtual: null,
7779
domainsVirtual: [],

apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { $assets } from "~/shared/nano-states";
77
import { ImageManager } from "~/builder/shared/image-manager";
88
import { type ControlProps } from "../shared";
99
import { acceptToMimeCategories } from "@webstudio-is/asset-uploader";
10+
import { formatAssetName } from "~/builder/shared/assets/asset-utils";
1011

1112
// tests whether we can use ImageManager for the given "accept" value
1213
const isImageAccept = (accept?: string) => {
@@ -52,7 +53,7 @@ export const SelectAsset = ({ prop, onChange, accept }: Props) => {
5253
}
5354
>
5455
<Button color="neutral" css={{ flex: 1 }}>
55-
{asset?.name ?? "Choose source"}
56+
{asset ? formatAssetName(asset) : "Choose source"}
5657
</Button>
5758
</FloatingPanel>
5859
</Flex>

apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ const imageAsset = (name = "cat", format = "jpg"): Asset => ({
107107
format: format,
108108
size: 100000,
109109
createdAt: new Date().toISOString(),
110-
description: null,
111110
meta: { width: 128, height: 180 },
112111
});
113112

apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getRepeatedStyleItem,
1515
setRepeatedStyleItem,
1616
} from "../../shared/repeated-style";
17+
import { formatAssetName } from "~/builder/shared/assets/asset-utils";
1718

1819
const isValidURL = (value: string) => {
1920
try {
@@ -115,7 +116,7 @@ export const ImageControl = ({
115116
color="neutral"
116117
css={{ maxWidth: "100%", justifySelf: "right" }}
117118
>
118-
{asset?.name ?? "Choose image..."}
119+
{asset ? formatAssetName(asset) : "Choose image..."}
119120
</Button>
120121
</FloatingPanel>
121122
</Flex>

apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import brokenImage from "~/shared/images/broken-image-placeholder.svg";
1212
import { humanizeString } from "~/shared/string-utils";
1313
import { useComputedStyles } from "../../shared/model";
1414
import { getComputedRepeatedItem } from "../../shared/repeated-style";
15+
import { formatAssetName } from "~/builder/shared/assets/asset-utils";
1516

1617
export const repeatedProperties = [
1718
"background-image",
@@ -89,7 +90,7 @@ export const getBackgroundLabel = (
8990
) {
9091
const asset = assets.get(backgroundImageStyle.value.value);
9192
if (asset) {
92-
return asset.name;
93+
return formatAssetName(asset);
9394
}
9495
}
9596

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect, test } from "vitest";
2+
import { parseAssetName } from "./asset-utils";
3+
4+
test("parse asset name", () => {
5+
expect(parseAssetName("hello_hash.ext")).toEqual({
6+
basename: "hello",
7+
hash: "hash",
8+
ext: "ext",
9+
});
10+
expect(parseAssetName("hello.ext")).toEqual({
11+
basename: "hello",
12+
hash: "",
13+
ext: "ext",
14+
});
15+
expect(parseAssetName("hello_hash1.ext_hash2")).toEqual({
16+
basename: "hello",
17+
hash: "hash1",
18+
ext: "ext_hash2",
19+
});
20+
expect(parseAssetName("hello_hash1_hash2")).toEqual({
21+
basename: "hello_hash1",
22+
hash: "hash2",
23+
ext: "",
24+
});
25+
});

apps/builder/app/builder/shared/assets/asset-utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,31 @@ export const uploadingFileDataToAsset = (
175175

176176
return asset;
177177
};
178+
179+
type ParsedAssetName = {
180+
basename: string;
181+
hash: string;
182+
ext: string;
183+
};
184+
185+
export const parseAssetName = (name: string): ParsedAssetName => {
186+
let hash = "";
187+
let ext = "";
188+
const lastDotAt = name.lastIndexOf(".");
189+
if (lastDotAt > -1) {
190+
ext = name.slice(lastDotAt + 1);
191+
name = name.slice(0, lastDotAt);
192+
}
193+
const lastUnderscoreAt = name.lastIndexOf("_");
194+
if (lastUnderscoreAt > -1) {
195+
hash = name.slice(lastUnderscoreAt + 1);
196+
name = name.slice(0, lastUnderscoreAt);
197+
}
198+
return { basename: name, hash, ext };
199+
};
200+
201+
export const formatAssetName = (asset: Pick<Asset, "name" | "filename">) => {
202+
const { basename, ext } = parseAssetName(asset.name);
203+
const formattedName = `${asset.filename ?? basename}.${ext}`;
204+
return formattedName;
205+
};

apps/builder/app/builder/shared/assets/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Asset } from "@webstudio-is/sdk";
22

33
type PreviewAsset = Pick<
44
Asset,
5-
"name" | "id" | "format" | "description" | "type"
5+
"name" | "filename" | "id" | "format" | "description" | "type"
66
>;
77

88
export type UploadedAssetContainer = {

apps/builder/app/builder/shared/assets/use-assets.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
UploadingAssetContainer,
3030
} from "./types";
3131
import {
32+
formatAssetName,
3233
getFileName,
3334
getMimeType,
3435
getSha256Hash,
@@ -253,6 +254,27 @@ const getVideoDimensions = async (file: File) => {
253254
});
254255
};
255256

257+
const deduplicateAssetName = (name: string) => {
258+
const existingNames = new Set();
259+
for (const asset of $assets.get().values()) {
260+
existingNames.add(formatAssetName(asset));
261+
}
262+
// eslint-disable-next-line no-constant-condition
263+
for (let index = 0; true; index += 1) {
264+
const suffix = index === 0 ? "" : `_${index}`;
265+
const lastDotAt = name.lastIndexOf(".");
266+
if (lastDotAt === -1) {
267+
return name;
268+
}
269+
const basename = name.slice(0, lastDotAt);
270+
const ext = name.slice(lastDotAt);
271+
const nameWithSuffix = basename + suffix + ext;
272+
if (!existingNames.has(nameWithSuffix)) {
273+
return nameWithSuffix;
274+
}
275+
}
276+
};
277+
256278
const uploadAsset = async ({
257279
authToken,
258280
projectId,
@@ -275,7 +297,10 @@ const uploadAsset = async ({
275297
metaFormData.append("type", mimeType);
276298
// sanitizeS3Key here is just because of https://github.com/remix-run/remix/issues/4443
277299
// should be removed after fix
278-
metaFormData.append("filename", sanitizeS3Key(fileName));
300+
metaFormData.append(
301+
"filename",
302+
deduplicateAssetName(sanitizeS3Key(fileName))
303+
);
279304

280305
const authHeaders = new Headers();
281306
if (authToken !== undefined) {

0 commit comments

Comments
 (0)