Skip to content

Commit fb466ca

Browse files
authored
feat: show when asset is used (#5365)
Ref #572 #3261 - made settings gear visible when asset is used anywhere - when asset is used delete button shows dialog with all usages <img width="235" height="149" alt="image" src="https://github.com/user-attachments/assets/50fc7e45-fb84-451d-9825-592b3ef45388" /> <img width="277" height="288" alt="image" src="https://github.com/user-attachments/assets/37187a5c-92b5-413c-bcdf-caaf506525a4" /> <img width="428" height="246" alt="Screenshot 2025-08-09 at 11 30 37" src="https://github.com/user-attachments/assets/e77d262a-3de0-4d46-b681-b38becb55862" />
1 parent 2a13c3d commit fb466ca

File tree

7 files changed

+408
-119
lines changed

7 files changed

+408
-119
lines changed

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

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,125 @@ import { useMemo } from "react";
22
import { computed } from "nanostores";
33
import { useStore } from "@nanostores/react";
44
import warnOnce from "warn-once";
5-
import type { Asset } from "@webstudio-is/sdk";
5+
import invariant from "tiny-invariant";
6+
import type { Asset, Page } from "@webstudio-is/sdk";
67
import type { AssetType } from "@webstudio-is/asset-uploader";
78
import { Box, toast, css, theme } from "@webstudio-is/design-system";
89
import { sanitizeS3Key } from "@webstudio-is/asset-uploader";
10+
import { Image, wsImageLoader } from "@webstudio-is/image";
11+
import type { ImageValue, StyleValue } from "@webstudio-is/css-engine";
912
import { restAssetsUploadPath, restAssetsPath } from "~/shared/router-utils";
10-
import type {
11-
AssetContainer,
12-
UploadedAssetContainer,
13-
UploadingAssetContainer,
14-
} from "./types";
13+
import { fetch } from "~/shared/fetch.client";
1514
import type { ActionData } from "~/builder/shared/assets";
1615
import {
1716
$assets,
1817
$authToken,
18+
$pages,
1919
$project,
20+
$props,
21+
$styles,
2022
$uploadingFilesDataStore,
2123
type UploadingFileData,
2224
} from "~/shared/nano-states";
2325
import { serverSyncStore } from "~/shared/sync";
26+
import type {
27+
AssetContainer,
28+
UploadedAssetContainer,
29+
UploadingAssetContainer,
30+
} from "./types";
2431
import {
2532
getFileName,
2633
getMimeType,
2734
getSha256Hash,
2835
getSha256HashOfFile,
2936
uploadingFileDataToAsset,
3037
} from "./asset-utils";
31-
import { Image, wsImageLoader } from "@webstudio-is/image";
32-
import invariant from "tiny-invariant";
33-
import { fetch } from "~/shared/fetch.client";
38+
import { mapGetOrInsert } from "~/shared/shim";
39+
40+
export type AssetUsage =
41+
| { type: "favicon" }
42+
| { type: "socialImage"; pageId: Page["id"] }
43+
| { type: "marketplaceThumbnail"; pageId: Page["id"] }
44+
| { type: "prop"; propId: string }
45+
| { type: "style"; styleDeclKey: string };
46+
47+
const traverseStyleValue = (
48+
styleValue: StyleValue,
49+
callback: (value: ImageValue) => void
50+
) => {
51+
if (styleValue.type === "image") {
52+
callback(styleValue);
53+
}
54+
if (styleValue.type === "tuple") {
55+
for (const item of styleValue.value) {
56+
traverseStyleValue(item, callback);
57+
}
58+
}
59+
if (styleValue.type === "layers") {
60+
for (const item of styleValue.value) {
61+
traverseStyleValue(item, callback);
62+
}
63+
}
64+
};
65+
66+
export const $usagesByAssetId = computed(
67+
[$pages, $props, $styles],
68+
(pages, props, styles) => {
69+
const usagesByAsset = new Map<Asset["id"], AssetUsage[]>();
70+
if (pages?.meta?.faviconAssetId) {
71+
const usages = mapGetOrInsert(
72+
usagesByAsset,
73+
pages.meta.faviconAssetId,
74+
[]
75+
);
76+
usages.push({ type: "favicon" });
77+
}
78+
if (pages) {
79+
for (const page of [pages.homePage, ...pages.pages]) {
80+
if (page.meta.socialImageAssetId) {
81+
const usages = mapGetOrInsert(
82+
usagesByAsset,
83+
page.meta.socialImageAssetId,
84+
[]
85+
);
86+
usages.push({ type: "socialImage", pageId: page.id });
87+
}
88+
if (page.marketplace?.thumbnailAssetId) {
89+
const usages = mapGetOrInsert(
90+
usagesByAsset,
91+
page.marketplace.thumbnailAssetId,
92+
[]
93+
);
94+
usages.push({ type: "marketplaceThumbnail", pageId: page.id });
95+
}
96+
}
97+
}
98+
for (const prop of props.values()) {
99+
if (
100+
prop.type === "asset" &&
101+
// ignore width and height properties which are specific to size
102+
prop.name !== "width" &&
103+
prop.name !== "height"
104+
) {
105+
const usages = mapGetOrInsert(usagesByAsset, prop.value, []);
106+
usages.push({ type: "prop", propId: prop.id });
107+
}
108+
}
109+
for (const [styleDeclKey, styleDecl] of styles) {
110+
traverseStyleValue(styleDecl.value, (imageValue) => {
111+
if (imageValue.value.type === "asset") {
112+
const usages = mapGetOrInsert(
113+
usagesByAsset,
114+
imageValue.value.value,
115+
[]
116+
);
117+
usages.push({ type: "style", styleDeclKey });
118+
}
119+
});
120+
}
121+
return usagesByAsset;
122+
}
123+
);
34124

35125
export const deleteAssets = (assetIds: Asset["id"][]) => {
36126
serverSyncStore.createTransaction([$assets], (assets) => {

apps/builder/app/builder/shared/image-manager/image-info-tigger.tsx

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)