diff --git a/apps/builder/app/builder/features/pages/image-info.tsx b/apps/builder/app/builder/features/pages/image-info.tsx index a325b4d933ba..1f56351dc761 100644 --- a/apps/builder/app/builder/features/pages/image-info.tsx +++ b/apps/builder/app/builder/features/pages/image-info.tsx @@ -12,6 +12,7 @@ import { ImageIcon, } from "@webstudio-is/icons"; import type { ImageAsset } from "@webstudio-is/sdk"; +import { formatAssetName } from "~/builder/shared/assets/asset-utils"; import { getFormattedAspectRatio } from "~/builder/shared/image-manager/utils"; type ImageInfoProps = { @@ -42,10 +43,14 @@ export const ImageInfo = ({ asset, onDelete }: ImageInfoProps) => { gap={2} align={"center"} > - + - - {asset.name} + + {formatAssetName(asset)} diff --git a/apps/builder/app/builder/features/pages/page-settings.stories.tsx b/apps/builder/app/builder/features/pages/page-settings.stories.tsx index 68ea06950edd..b354f13190ef 100644 --- a/apps/builder/app/builder/features/pages/page-settings.stories.tsx +++ b/apps/builder/app/builder/features/pages/page-settings.stories.tsx @@ -72,6 +72,8 @@ $project.set({ projectId: "projectId", id: "imageId", name: "very-very-very-long-long-image-name.jpg", + filename: null, + description: null, }, latestBuildVirtual: null, domainsVirtual: [], diff --git a/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx b/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx index f545927ae763..21394e714b1a 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx @@ -7,6 +7,7 @@ import { $assets } from "~/shared/nano-states"; import { ImageManager } from "~/builder/shared/image-manager"; import { type ControlProps } from "../shared"; import { acceptToMimeCategories } from "@webstudio-is/asset-uploader"; +import { formatAssetName } from "~/builder/shared/assets/asset-utils"; // tests whether we can use ImageManager for the given "accept" value const isImageAccept = (accept?: string) => { @@ -52,7 +53,7 @@ export const SelectAsset = ({ prop, onChange, accept }: Props) => { } > diff --git a/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx b/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx index 452c2f89ee0b..cf11144bf9c5 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx @@ -107,7 +107,6 @@ const imageAsset = (name = "cat", format = "jpg"): Asset => ({ format: format, size: 100000, createdAt: new Date().toISOString(), - description: null, meta: { width: 128, height: 180 }, }); diff --git a/apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx b/apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx index 802de9607b51..f2f5f722f0e6 100644 --- a/apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx +++ b/apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx @@ -14,6 +14,7 @@ import { getRepeatedStyleItem, setRepeatedStyleItem, } from "../../shared/repeated-style"; +import { formatAssetName } from "~/builder/shared/assets/asset-utils"; const isValidURL = (value: string) => { try { @@ -115,7 +116,7 @@ export const ImageControl = ({ color="neutral" css={{ maxWidth: "100%", justifySelf: "right" }} > - {asset?.name ?? "Choose image..."} + {asset ? formatAssetName(asset) : "Choose image..."} diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx index 2d09ba76d08e..f1c40316831f 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx @@ -12,6 +12,7 @@ import brokenImage from "~/shared/images/broken-image-placeholder.svg"; import { humanizeString } from "~/shared/string-utils"; import { useComputedStyles } from "../../shared/model"; import { getComputedRepeatedItem } from "../../shared/repeated-style"; +import { formatAssetName } from "~/builder/shared/assets/asset-utils"; export const repeatedProperties = [ "background-image", @@ -89,7 +90,7 @@ export const getBackgroundLabel = ( ) { const asset = assets.get(backgroundImageStyle.value.value); if (asset) { - return asset.name; + return formatAssetName(asset); } } diff --git a/apps/builder/app/builder/shared/assets/asset-utils.test.ts b/apps/builder/app/builder/shared/assets/asset-utils.test.ts new file mode 100644 index 000000000000..d57d001776bf --- /dev/null +++ b/apps/builder/app/builder/shared/assets/asset-utils.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; +import { parseAssetName } from "./asset-utils"; + +test("parse asset name", () => { + expect(parseAssetName("hello_hash.ext")).toEqual({ + basename: "hello", + hash: "hash", + ext: "ext", + }); + expect(parseAssetName("hello.ext")).toEqual({ + basename: "hello", + hash: "", + ext: "ext", + }); + expect(parseAssetName("hello_hash1.ext_hash2")).toEqual({ + basename: "hello", + hash: "hash1", + ext: "ext_hash2", + }); + expect(parseAssetName("hello_hash1_hash2")).toEqual({ + basename: "hello_hash1", + hash: "hash2", + ext: "", + }); +}); diff --git a/apps/builder/app/builder/shared/assets/asset-utils.ts b/apps/builder/app/builder/shared/assets/asset-utils.ts index c457dbc3807d..8d5f73a07fb1 100644 --- a/apps/builder/app/builder/shared/assets/asset-utils.ts +++ b/apps/builder/app/builder/shared/assets/asset-utils.ts @@ -175,3 +175,31 @@ export const uploadingFileDataToAsset = ( return asset; }; + +type ParsedAssetName = { + basename: string; + hash: string; + ext: string; +}; + +export const parseAssetName = (name: string): ParsedAssetName => { + let hash = ""; + let ext = ""; + const lastDotAt = name.lastIndexOf("."); + if (lastDotAt > -1) { + ext = name.slice(lastDotAt + 1); + name = name.slice(0, lastDotAt); + } + const lastUnderscoreAt = name.lastIndexOf("_"); + if (lastUnderscoreAt > -1) { + hash = name.slice(lastUnderscoreAt + 1); + name = name.slice(0, lastUnderscoreAt); + } + return { basename: name, hash, ext }; +}; + +export const formatAssetName = (asset: Pick) => { + const { basename, ext } = parseAssetName(asset.name); + const formattedName = `${asset.filename ?? basename}.${ext}`; + return formattedName; +}; diff --git a/apps/builder/app/builder/shared/assets/types.ts b/apps/builder/app/builder/shared/assets/types.ts index 8649ef3c58ad..c5b41b5d91aa 100644 --- a/apps/builder/app/builder/shared/assets/types.ts +++ b/apps/builder/app/builder/shared/assets/types.ts @@ -2,7 +2,7 @@ import type { Asset } from "@webstudio-is/sdk"; type PreviewAsset = Pick< Asset, - "name" | "id" | "format" | "description" | "type" + "name" | "filename" | "id" | "format" | "description" | "type" >; export type UploadedAssetContainer = { diff --git a/apps/builder/app/builder/shared/assets/use-assets.tsx b/apps/builder/app/builder/shared/assets/use-assets.tsx index 786623d4a651..ea1659864eae 100644 --- a/apps/builder/app/builder/shared/assets/use-assets.tsx +++ b/apps/builder/app/builder/shared/assets/use-assets.tsx @@ -29,6 +29,7 @@ import type { UploadingAssetContainer, } from "./types"; import { + formatAssetName, getFileName, getMimeType, getSha256Hash, @@ -253,6 +254,27 @@ const getVideoDimensions = async (file: File) => { }); }; +const deduplicateAssetName = (name: string) => { + const existingNames = new Set(); + for (const asset of $assets.get().values()) { + existingNames.add(formatAssetName(asset)); + } + // eslint-disable-next-line no-constant-condition + for (let index = 0; true; index += 1) { + const suffix = index === 0 ? "" : `_${index}`; + const lastDotAt = name.lastIndexOf("."); + if (lastDotAt === -1) { + return name; + } + const basename = name.slice(0, lastDotAt); + const ext = name.slice(lastDotAt); + const nameWithSuffix = basename + suffix + ext; + if (!existingNames.has(nameWithSuffix)) { + return nameWithSuffix; + } + } +}; + const uploadAsset = async ({ authToken, projectId, @@ -275,7 +297,10 @@ const uploadAsset = async ({ metaFormData.append("type", mimeType); // sanitizeS3Key here is just because of https://github.com/remix-run/remix/issues/4443 // should be removed after fix - metaFormData.append("filename", sanitizeS3Key(fileName)); + metaFormData.append( + "filename", + deduplicateAssetName(sanitizeS3Key(fileName)) + ); const authHeaders = new Headers(); if (authToken !== undefined) { diff --git a/apps/builder/app/builder/shared/image-manager/filename.tsx b/apps/builder/app/builder/shared/image-manager/filename.tsx deleted file mode 100644 index 885f58348836..000000000000 --- a/apps/builder/app/builder/shared/image-manager/filename.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Flex, Text } from "@webstudio-is/design-system"; -import type { ComponentProps } from "react"; - -type FilenameProps = Omit, "children"> & { - children: string; -}; - -export const Filename = ({ children, ...props }: FilenameProps) => { - const parts = children.split("."); - const extension = parts.length > 1 ? parts.pop() : ""; - const baseName = parts.join("."); - - return ( - - - {baseName} - - {extension} - - ); -}; diff --git a/apps/builder/app/builder/shared/image-manager/image-info.tsx b/apps/builder/app/builder/shared/image-manager/image-info.tsx index 575627ed9213..e794c4f44e5f 100644 --- a/apps/builder/app/builder/shared/image-manager/image-info.tsx +++ b/apps/builder/app/builder/shared/image-manager/image-info.tsx @@ -1,3 +1,6 @@ +import isValidFilename from "valid-filename"; +import { useEffect, useRef, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; import prettyBytes from "pretty-bytes"; import { useStore } from "@nanostores/react"; import { getMimeByExtension } from "@webstudio-is/asset-uploader"; @@ -11,6 +14,9 @@ import { DialogTrigger, Flex, Grid, + InputErrorsTooltip, + InputField, + Label, Popover, PopoverContent, PopoverTitle, @@ -31,7 +37,9 @@ import { TrashIcon, } from "@webstudio-is/icons"; import type { Asset, Instance } from "@webstudio-is/sdk"; +import { hyphenateProperty } from "@webstudio-is/css-engine"; import { + $assets, $authPermit, $editingPageId, $instances, @@ -40,16 +48,17 @@ import { $styles, $styleSourceSelections, } from "~/shared/nano-states"; -import { deleteAssets, $usagesByAssetId, type AssetUsage } from "../assets"; -import { getFormattedAspectRatio } from "./utils"; -import { hyphenateProperty } from "@webstudio-is/css-engine"; import { $openProjectSettings } from "~/shared/nano-states/project-settings"; import { $awareness, findAwarenessByInstanceId, selectPage, } from "~/shared/awareness"; +import { updateWebstudioData } from "~/shared/instance-utils"; +import { deleteAssets, $usagesByAssetId, type AssetUsage } from "../assets"; import { $activeInspectorPanel, setActiveSidebarPanel } from "../nano-states"; +import { parseAssetName } from "../assets/asset-utils"; +import { getFormattedAspectRatio } from "./utils"; const buttonLinkClass = css({ all: "unset", @@ -198,6 +207,48 @@ const UsageDot = styled(Box, { pointerEvents: "none", }); +const useLocalValue = ( + savedValue: Type, + onSave: (value: Type) => void +) => { + const [localValue, setLocalValue] = useState(savedValue); + + const save = () => { + if (localValue !== savedValue) { + // To synchronize with setState immediately followed by save + onSave(localValue); + } + }; + + const saveDebounced = useDebouncedCallback(save, 500); + const updateLocalValue = (value: Type) => { + setLocalValue(value); + saveDebounced(); + }; + + // onBlur will not trigger if control is unmounted when props panel is closed or similar. + // So we're saving at the unmount + // store save in ref to access latest saved value from render + // instead of stale one + const saveRef = useRef(save); + saveRef.current = save; + useEffect(() => { + // access ref in the moment of unmount + return () => saveRef.current(); + }, []); + + return [ + /** + * Contains: + * - either the latest `savedValue` + * - or the latest value set via `set()` + * (whichever changed most recently) + */ + localValue, + updateLocalValue, + ] as const; +}; + const ImageInfoContent = ({ asset, usages, @@ -206,17 +257,41 @@ const ImageInfoContent = ({ usages: AssetUsage[]; }) => { const { size, meta, id, name } = asset; - - const parts = name.split("."); - const extension = "." + parts.pop(); + const { basename, ext } = parseAssetName(name); + const [filenameError, setFilenameError] = useState(); + const [filename, setFilename] = useLocalValue( + asset.filename ?? basename, + (newFilename) => { + const assetId = asset.id; + // validate filename + if (!isValidFilename(newFilename)) { + setFilenameError("Invalid filename"); + return; + } + // validate duplicates + for (const asset of $assets.get().values()) { + if (asset.id !== assetId) { + const filename = + asset.filename ?? parseAssetName(asset.name).basename; + if (newFilename === filename) { + setFilenameError("Filename already used"); + return; + } + } + } + updateWebstudioData((data) => { + const asset = data.assets.get(assetId); + if (asset) { + asset.filename = newFilename; + } + }); + } + ); const authPermit = useStore($authPermit); return ( <> - - {name} - - {getMimeByExtension(extension)} + {getMimeByExtension(`.${ext}`)} {"width" in meta && "height" in meta && ( @@ -265,6 +340,24 @@ const ImageInfoContent = ({ + + + + + { + setFilename(event.target.value); + setFilenameError(undefined); + }} + /> + + + {authPermit === "view" ? ( @@ -350,7 +443,7 @@ export const ImageInfo = ({ asset }: { asset: Asset }) => { icon={} /> - + Asset Details diff --git a/apps/builder/app/builder/shared/image-manager/image-thumbnail.tsx b/apps/builder/app/builder/shared/image-manager/image-thumbnail.tsx index a12b02972a82..098cf4401fbd 100644 --- a/apps/builder/app/builder/shared/image-manager/image-thumbnail.tsx +++ b/apps/builder/app/builder/shared/image-manager/image-thumbnail.tsx @@ -1,13 +1,16 @@ import type { KeyboardEvent, FocusEvent } from "react"; -import { Box, styled } from "@webstudio-is/design-system"; +import { Box, Flex, styled, Text } from "@webstudio-is/design-system"; import { UploadingAnimation } from "./uploading-animation"; import { ImageInfo, imageInfoCssVars } from "./image-info"; import type { AssetContainer } from "~/builder/shared/assets"; -import { Filename } from "./filename"; import { Image } from "./image"; import brokenImage from "~/shared/images/broken-image-placeholder.svg"; import { theme } from "@webstudio-is/design-system"; -import { isVideoFormat } from "../assets/asset-utils"; +import { + formatAssetName, + isVideoFormat, + parseAssetName, +} from "../assets/asset-utils"; const StyledWebstudioImage = styled(Image, { position: "absolute", @@ -113,17 +116,16 @@ export const ImageThumbnail = ({ onChange, state, }: ImageThumbnailProps) => { - const { asset, status } = assetContainer; - - const { name, description } = asset; - - const isUploading = status === "uploading"; + const { asset } = assetContainer; + const { basename, ext } = parseAssetName(asset.name); + const alt = asset.description ?? formatAssetName(asset); + const isUploading = assetContainer.status === "uploading"; return ( { onSelect?.(assetContainer); @@ -145,33 +147,30 @@ export const ImageThumbnail = ({ onChange?.(assetContainer); }} > - {isVideoFormat(assetContainer.asset.format) && + {isVideoFormat(asset.format) && assetContainer.status === "uploading" ? ( ) : ( )} - - {name} - + + + {asset.filename ?? basename} + + .{ext} + {assetContainer.status === "uploaded" && ( )} diff --git a/apps/builder/package.json b/apps/builder/package.json index 64df11041e65..0b7198a9fdb4 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -121,6 +121,7 @@ "title-case": "^4.3.2", "urlpattern-polyfill": "^10.0.0", "use-debounce": "^10.0.4", + "valid-filename": "^4.0.0", "warn-once": "^0.1.1", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" diff --git a/packages/asset-uploader/src/db/load.ts b/packages/asset-uploader/src/db/load.ts index 4e851fb6fdec..3a3cad0ed63c 100644 --- a/packages/asset-uploader/src/db/load.ts +++ b/packages/asset-uploader/src/db/load.ts @@ -32,6 +32,8 @@ export const loadAssetsByProject = async ( ` assetId:id, projectId, + filename, + description, file:File!inner (*) ` ) @@ -42,9 +44,17 @@ export const loadAssetsByProject = async ( .order("id"); const result: Asset[] = []; - for (const { assetId, projectId, file } of assets.data ?? []) { + for (const { + assetId, + projectId, + filename, + description, + file, + } of assets.data ?? []) { if (file) { - result.push(formatAsset({ assetId, projectId, file })); + result.push( + formatAsset({ assetId, projectId, filename, description, file }) + ); } } diff --git a/packages/asset-uploader/src/patch.ts b/packages/asset-uploader/src/patch.ts index da90fceb044a..32669d2fb147 100644 --- a/packages/asset-uploader/src/patch.ts +++ b/packages/asset-uploader/src/patch.ts @@ -29,7 +29,10 @@ export const patchAssets = async ( for (const asset of assetsList) { assets.set(asset.id, asset); } - const patchedAssets = Assets.parse(applyPatches(assets, patches)); + const patchedAssets = applyPatches(assets, patches); + // validate assets without recreating objects + // we expect referencial equality to find updated assets + Assets.parse(patchedAssets); // delete assets no longer existing in patched version const deletedAssetIds: Asset["id"][] = []; @@ -42,6 +45,19 @@ export const patchAssets = async ( deleteAssets({ projectId, ids: deletedAssetIds }, context); } + // update assets + for (const asset of assets.values()) { + const patchedAsset = patchedAssets.get(asset.id); + if (asset !== patchedAsset && patchedAsset) { + const { filename, description } = patchedAsset; + await context.postgrest.client + .from("Asset") + .update({ filename, description }) + .eq("id", asset.id) + .eq("projectId", asset.projectId); + } + } + // add new assets found in patched version const addedAssets: Asset[] = []; for (const [assetId, asset] of patchedAssets) { diff --git a/packages/asset-uploader/src/upload.ts b/packages/asset-uploader/src/upload.ts index b5ef5c748cdd..993407457b8f 100644 --- a/packages/asset-uploader/src/upload.ts +++ b/packages/asset-uploader/src/upload.ts @@ -3,11 +3,11 @@ import { authorizeProject, AuthorizationError, } from "@webstudio-is/trpc-interface/index.server"; +import type { Asset } from "@webstudio-is/sdk"; import type { AssetClient } from "./client"; import { getUniqueFilename } from "./utils/get-unique-filename"; import { sanitizeS3Key } from "./utils/sanitize-s3-key"; import { formatAsset } from "./utils/format-asset"; -import type { Asset } from "@webstudio-is/sdk"; type UploadData = { projectId: string; @@ -141,6 +141,8 @@ export const uploadFile = async ( return formatAsset({ assetId: "", projectId: file.data.uploaderProjectId as string, + filename: null, + description: null, file: file.data, }); } catch (error) { diff --git a/packages/asset-uploader/src/utils/format-asset.ts b/packages/asset-uploader/src/utils/format-asset.ts index 39219fb9f491..08b5f5a00e7c 100644 --- a/packages/asset-uploader/src/utils/format-asset.ts +++ b/packages/asset-uploader/src/utils/format-asset.ts @@ -4,10 +4,14 @@ import { type Asset, ImageMeta } from "@webstudio-is/sdk"; export const formatAsset = ({ assetId, projectId, + filename, + description, file, }: { assetId: string; projectId: string; + filename: string | null; + description: string | null; file: { name: string; format: string; @@ -23,8 +27,9 @@ export const formatAsset = ({ return { id: assetId, name: file.name, - description: file.description, projectId, + filename: filename ?? undefined, + description: description ?? undefined, size: file.size, type: "font", createdAt: file.createdAt, @@ -36,8 +41,9 @@ export const formatAsset = ({ return { id: assetId, name: file.name, - description: file.description, projectId, + filename: filename ?? undefined, + description: description ?? undefined, size: file.size, type: "image", format: file.format, diff --git a/packages/postgrest/src/__generated__/db-types.ts b/packages/postgrest/src/__generated__/db-types.ts index f224dd88e80a..24f69967cca9 100644 --- a/packages/postgrest/src/__generated__/db-types.ts +++ b/packages/postgrest/src/__generated__/db-types.ts @@ -17,10 +17,10 @@ export type Database = { Functions: { graphql: { Args: { + extensions?: Json; operationName?: string; query?: string; variables?: Json; - extensions?: Json; }; Returns: Json; }; @@ -69,16 +69,22 @@ export type Database = { }; Asset: { Row: { + description: string | null; + filename: string | null; id: string; name: string; projectId: string; }; Insert: { + description?: string | null; + filename?: string | null; id: string; name: string; projectId: string; }; Update: { + description?: string | null; + filename?: string | null; id?: string; name?: string; projectId?: string; @@ -877,10 +883,10 @@ export type Database = { Functions: { clone_project: { Args: { + domain: string; project_id: string; - user_id: string; title: string; - domain: string; + user_id: string; }; Returns: { createdAt: string; @@ -894,7 +900,7 @@ export type Database = { }; }; create_production_build: { - Args: { project_id: string; deployment: string }; + Args: { deployment: string; project_id: string }; Returns: string; }; database_cleanup: { @@ -943,7 +949,7 @@ export type Database = { }[]; }; restore_development_build: { - Args: { project_id: string; from_build_id: string }; + Args: { from_build_id: string; project_id: string }; Returns: string; }; }; @@ -968,21 +974,28 @@ export type Database = { }; }; -type DefaultSchema = Database[Extract]; +type DatabaseWithoutInternals = Omit; + +type DefaultSchema = DatabaseWithoutInternals[Extract< + keyof Database, + "public" +>]; export type Tables< DefaultSchemaTableNameOrOptions extends | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database; + schema: keyof DatabaseWithoutInternals; } - ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals; +} + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { Row: infer R; } ? R @@ -1000,14 +1013,16 @@ export type Tables< export type TablesInsert< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema["Tables"] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database; + schema: keyof DatabaseWithoutInternals; } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals; +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { Insert: infer I; } ? I @@ -1023,14 +1038,16 @@ export type TablesInsert< export type TablesUpdate< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema["Tables"] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database; + schema: keyof DatabaseWithoutInternals; } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, -> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { +> = DefaultSchemaTableNameOrOptions extends { + schema: keyof DatabaseWithoutInternals; +} + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { Update: infer U; } ? U @@ -1046,14 +1063,16 @@ export type TablesUpdate< export type Enums< DefaultSchemaEnumNameOrOptions extends | keyof DefaultSchema["Enums"] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof Database; + schema: keyof DatabaseWithoutInternals; } - ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] : never = never, -> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] +> = DefaultSchemaEnumNameOrOptions extends { + schema: keyof DatabaseWithoutInternals; +} + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] : never; @@ -1061,14 +1080,16 @@ export type Enums< export type CompositeTypes< PublicCompositeTypeNameOrOptions extends | keyof DefaultSchema["CompositeTypes"] - | { schema: keyof Database }, + | { schema: keyof DatabaseWithoutInternals }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database; + schema: keyof DatabaseWithoutInternals; } - ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] : never = never, -> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } - ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] +> = PublicCompositeTypeNameOrOptions extends { + schema: keyof DatabaseWithoutInternals; +} + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] : never; diff --git a/packages/prisma-client/prisma/migrations/20250810123401_asset_filename_description/migration.sql b/packages/prisma-client/prisma/migrations/20250810123401_asset_filename_description/migration.sql new file mode 100644 index 000000000000..00c1c5801eef --- /dev/null +++ b/packages/prisma-client/prisma/migrations/20250810123401_asset_filename_description/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Asset" + ADD COLUMN "description" TEXT, + ADD COLUMN "filename" TEXT; diff --git a/packages/prisma-client/prisma/schema.prisma b/packages/prisma-client/prisma/schema.prisma index a3a8fde15054..0748f0c7848a 100644 --- a/packages/prisma-client/prisma/schema.prisma +++ b/packages/prisma-client/prisma/schema.prisma @@ -48,6 +48,8 @@ model Asset { projectId String file File @relation(fields: [name], references: [name]) name String + filename String? + description String? Project Project[] DashboardProject DashboardProject[] diff --git a/packages/sdk/src/schema/assets.ts b/packages/sdk/src/schema/assets.ts index 150bc8746987..f9c85c7478ac 100644 --- a/packages/sdk/src/schema/assets.ts +++ b/packages/sdk/src/schema/assets.ts @@ -8,7 +8,8 @@ const baseAsset = { projectId: z.string(), size: z.number(), name: z.string(), - description: z.union([z.string(), z.null()]), + filename: z.string().optional(), + description: z.union([z.string().optional(), z.null()]), createdAt: z.string(), }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e377927fd2f..887d18862258 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -424,6 +424,9 @@ importers: use-debounce: specifier: ^10.0.4 version: 10.0.4(react@18.3.0-canary-14898b6a9-20240318) + valid-filename: + specifier: ^4.0.0 + version: 4.0.0 warn-once: specifier: ^0.1.1 version: 0.1.1 @@ -6569,6 +6572,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -9103,6 +9110,10 @@ packages: typescript: optional: true + valid-filename@4.0.0: + resolution: {integrity: sha512-VEYTpTVPMgO799f2wI7zWf0x2C54bPX6NAfbZ2Z8kZn76p+3rEYCTYVYzMUcVSMvakxMQTriBf24s3+WeXJtEg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -14148,6 +14159,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + filename-reserved-regex@3.0.0: {} + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -16999,6 +17012,10 @@ snapshots: optionalDependencies: typescript: 5.8.2 + valid-filename@4.0.0: + dependencies: + filename-reserved-regex: 3.0.0 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.1.1