diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4061709a56..cef4574ccc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,7 @@ ## Before merging -- [ ] tested locally and on preview environment +- [ ] tested locally and on preview environment (preview dev login: 5de6e) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio-designer/blob/main/apps/designer/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env.example` and the `designer/env-check.js` if mandatory diff --git a/apps/designer/.env.example b/apps/designer/.env.example index 354ce8b964..c04c47f861 100644 --- a/apps/designer/.env.example +++ b/apps/designer/.env.example @@ -11,9 +11,10 @@ FILE_UPLOAD_PATH="uploads" # S3 envs S3_BUCKET= S3_REGION= +S3_ACL="public-read" S3_ENDPOINT= S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= # if using r2 this your worker url -WORKER_URL= \ No newline at end of file +ASSET_CDN_URL= \ No newline at end of file diff --git a/apps/designer/app/designer/features/sidebar-left/panels/asset-manager.tsx b/apps/designer/app/designer/features/sidebar-left/panels/asset-manager.tsx index cbda921c0e..80db7345aa 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/asset-manager.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/asset-manager.tsx @@ -1,44 +1,49 @@ import { ImageIcon } from "~/shared/icons"; -import { Button, Flex, Grid, Heading } from "~/shared/design-system"; -import { useRef } from "react"; -import { Form, useSubmit } from "@remix-run/react"; -import type { Asset } from "@webstudio-is/prisma-client"; +import { Flex, Grid, Heading } from "~/shared/design-system"; +import { useEffect, useState } from "react"; +import { useActionData } from "@remix-run/react"; +import { Asset } from "@webstudio-is/prisma-client"; import { AssetManagerImage } from "./components/image"; -export const TabContent = ({ assets }: { assets: Array }) => { - const inputRef = useRef(null); - const submit = useSubmit(); +import { AddAnAssetForm } from "./components/add-an-asset-form"; +import { UploadingAsset } from "../types"; + +export const TabContent = ({ + assets: baseAssets, +}: { + assets: Array; +}) => { + const newImages = useActionData(); + + const [assets, setAssets] = useState(baseAssets); + + useEffect(() => { + if (newImages?.length) { + setAssets((currentAssets) => [ + ...newImages, + ...currentAssets.filter((asset) => asset.status !== "uploading"), + ]); + } + }, [newImages]); return ( Assets -
{ - if (inputRef.current?.files) { - submit(event.currentTarget); - event.currentTarget.reset(); - } - }} - > - - -
+ ) => + setAssets((assets) => [...uploadedAssets, ...assets]) + } + />
{assets.map((asset) => ( - + ))}
diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/add-an-asset-form.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/add-an-asset-form.tsx new file mode 100644 index 0000000000..a19c673c17 --- /dev/null +++ b/apps/designer/app/designer/features/sidebar-left/panels/components/add-an-asset-form.tsx @@ -0,0 +1,66 @@ +import { Form, useSubmit } from "@remix-run/react"; +import { Asset } from "@webstudio-is/prisma-client"; +import ObjectID from "bson-objectid"; +import { ChangeEvent, useRef } from "react"; +import { Button } from "~/shared/design-system"; +import { UploadingAsset } from "../../types"; + +const readImages = async (fileList: FileList) => { + const images = []; + for (const file of fileList) { + const path = await new Promise((resolve) => { + const reader = new FileReader(); + reader.addEventListener("load", (event) => { + const dataUri = event?.target?.result; + + resolve(dataUri); + }); + reader.readAsDataURL(file); + }); + + images.push({ + path: path as string, + name: file.name, + id: ObjectID().toString(), + status: "uploading" as Asset["status"], + alt: file.name, + }); + } + + return images; +}; + +export const AddAnAssetForm = ({ + onSubmit, +}: { + onSubmit: (uploadedAssets: Array) => void; +}) => { + const submit = useSubmit(); + const inputRef = useRef(null); + + const onFormChange = async (event: ChangeEvent) => { + const newFiles = inputRef?.current?.files; + if (newFiles) { + submit(event.currentTarget); + const parsedFiles = await readImages(newFiles); + onSubmit(parsedFiles); + event.currentTarget.reset(); + } + }; + + return ( +
+ + +
+ ); +}; diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/image.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/image.tsx index c4e5e75195..369b850e82 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/components/image.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/components/image.tsx @@ -1,11 +1,12 @@ -import { Asset } from "@webstudio-is/prisma-client"; import { useEffect, useState } from "react"; -import { Box } from "~/shared/design-system"; -import placholderImage from "~/shared/images/image-placeholder.svg"; +import { Box, ProgressBar } from "~/shared/design-system"; +import { useInterval } from "react-use"; +import placeholderImage from "~/shared/images/image-placeholder.svg"; import brokenImage from "~/shared/images/broken-image-placeholder.svg"; +import { Asset } from "@webstudio-is/prisma-client"; const useImageWithFallback = ({ path }: { path: string }) => { - const [src, setSrc] = useState(placholderImage); + const [src, setSrc] = useState(placeholderImage); useEffect(() => { const newImage = new Image(); @@ -18,21 +19,61 @@ const useImageWithFallback = ({ path }: { path: string }) => { }; export const AssetManagerImage = ({ - asset: { path, alt }, + path, + alt, + status, }: { - asset: Asset; + path: string; + alt?: string; + status?: Asset["status"]; }) => { + const isUploading = status === "uploading"; const src = useImageWithFallback({ path }); + const [progressBarPercentage, setProgressBarPercentage] = useState(0); + + // @todo rewrite this fake indication to show real progress + useInterval( + () => { + setProgressBarPercentage((percentage) => + percentage < 60 ? percentage + 1 : percentage + ); + }, + isUploading ? 100 : null + ); + return ( + > + + {isUploading && ( + + )} + ); }; diff --git a/apps/designer/app/designer/features/sidebar-left/types.ts b/apps/designer/app/designer/features/sidebar-left/types.ts index dc658bb5f1..29fd62a28d 100644 --- a/apps/designer/app/designer/features/sidebar-left/types.ts +++ b/apps/designer/app/designer/features/sidebar-left/types.ts @@ -1,3 +1,9 @@ +import { Asset } from "@webstudio-is/prisma-client"; import { panels } from "./panels"; export type TabName = keyof typeof panels | "none"; + +export type UploadingAsset = Pick< + Asset, + "id" | "status" | "name" | "path" | "alt" +>; diff --git a/apps/designer/app/routes/designer/$id.tsx b/apps/designer/app/routes/designer/$id.tsx index 50f6f2e296..322389f433 100644 --- a/apps/designer/app/routes/designer/$id.tsx +++ b/apps/designer/app/routes/designer/$id.tsx @@ -42,9 +42,10 @@ export const action: ActionFunction = async ({ request, params }) => { projectId: params.id, dirname: __dirname, }); - return { - assets, - }; + return assets.map((asset: Asset) => ({ + ...asset, + status: "uploaded", + })); } catch (error) { if (error instanceof Error) { return { diff --git a/apps/designer/app/services/auth.server.ts b/apps/designer/app/services/auth.server.ts index 05c08e740d..0e07fe3d77 100644 --- a/apps/designer/app/services/auth.server.ts +++ b/apps/designer/app/services/auth.server.ts @@ -55,7 +55,7 @@ if (process.env.DEV_LOGIN === "true") { new FormStrategy(async ({ form }) => { const secret = form.get("secret"); - if (secret === process.env.AUTH_SECRET) { + if (secret === process.env.AUTH_SECRET?.slice(0, 4)) { try { const user = await db.user.createOrLoginWithDev(); return user; diff --git a/apps/designer/app/shared/design-system/index.ts b/apps/designer/app/shared/design-system/index.ts index d94c0e1062..9c912df80b 100644 --- a/apps/designer/app/shared/design-system/index.ts +++ b/apps/designer/app/shared/design-system/index.ts @@ -39,6 +39,7 @@ export { Tooltip } from "./components/tooltip"; export { Button } from "./components/button"; export { IconButton } from "./components/icon-button"; export { Box } from "./components/box"; +export { ProgressBar } from "./components/progress-bar"; export { Popover, PopoverContent, diff --git a/apps/designer/docs/test-cases.md b/apps/designer/docs/test-cases.md index fb4c81f185..b991c78e17 100644 --- a/apps/designer/docs/test-cases.md +++ b/apps/designer/docs/test-cases.md @@ -64,7 +64,7 @@ - Open the login page when logged in to make sure you are redirected to the dashboard - Ensure you can view a built site in an incognito tab - Ensure you can view a forked site in an incognito tab - - Click on dev login, add the AUTH_SECRET value in the `.env` value and make sure user is authenticated and redirected to the dashboard + - Click on dev login, add the first four characters in AUTH_SECRET value in the `.env` value and make sure user is authenticated and redirected to the dashboard - Click on dev login and write a wrong value to make sure you get an error message and stay in the login page. - Click on login with GitHub and make sure that you are redirected to the dashboard - Click on login with Google and make sure that you are redirected to the dashboard diff --git a/packages/asset-uploader/src/db/create.ts b/packages/asset-uploader/src/db/create.ts index 0cb2be322b..6b882fdaac 100644 --- a/packages/asset-uploader/src/db/create.ts +++ b/packages/asset-uploader/src/db/create.ts @@ -1,5 +1,6 @@ import { type Location, prisma, Project } from "@webstudio-is/prisma-client"; import sharp from "sharp"; +import { getAssetPath } from "../helpers/get-asset-path"; export const create = async ( projectId: Project["id"], @@ -24,5 +25,8 @@ export const create = async ( }, }); - return newAsset; + return { + ...newAsset, + path: getAssetPath(newAsset), + }; }; diff --git a/packages/asset-uploader/src/helpers/get-asset-path.test.ts b/packages/asset-uploader/src/helpers/get-asset-path.test.ts index 4c56a26a43..0701701e96 100644 --- a/packages/asset-uploader/src/helpers/get-asset-path.test.ts +++ b/packages/asset-uploader/src/helpers/get-asset-path.test.ts @@ -22,7 +22,7 @@ describe("getAssetPath", () => { ...OLD_ENV, S3_ACCESS_KEY_ID: "testid", S3_SECRET_ACCESS_KEY: "testkey", - WORKER_URL: "", + ASSET_CDN_URL: "", }; }); @@ -87,7 +87,7 @@ describe("getAssetPath", () => { process.env.S3_ENDPOINT = "https://userid.r2.cloudflarestorage.com"; process.env.S3_BUCKET = "bucket"; process.env.S3_REGION = "auto"; - process.env.WORKER_URL = "https://worker-test.workers.dev"; + process.env.ASSET_CDN_URL = "https://worker-test.workers.dev"; const { getAssetPath } = require("./get-asset-path"); expect( getAssetPath({ diff --git a/packages/asset-uploader/src/helpers/get-asset-path.ts b/packages/asset-uploader/src/helpers/get-asset-path.ts index 9a69969b63..168fdc6c52 100644 --- a/packages/asset-uploader/src/helpers/get-asset-path.ts +++ b/packages/asset-uploader/src/helpers/get-asset-path.ts @@ -13,10 +13,10 @@ export const getAssetPath = (asset: DbAsset) => { if (asset.location === Location.REMOTE && s3Envs.success) { const s3Url = new URL(s3Envs.data.S3_ENDPOINT); - if (s3Envs.data.WORKER_URL) { - const r2Url = new URL(s3Envs.data.WORKER_URL); - r2Url.pathname = asset.name; - return r2Url.toString(); + if (s3Envs.data.ASSET_CDN_URL) { + const cndUrl = new URL(s3Envs.data.ASSET_CDN_URL); + cndUrl.pathname = asset.name; + return cndUrl.toString(); } s3Url.hostname = `${s3Envs.data.S3_BUCKET}.${s3Url.hostname}`; s3Url.pathname = asset.name; diff --git a/packages/asset-uploader/src/schema.ts b/packages/asset-uploader/src/schema.ts index 0a8e3a2301..45399df529 100644 --- a/packages/asset-uploader/src/schema.ts +++ b/packages/asset-uploader/src/schema.ts @@ -20,8 +20,8 @@ export const s3EnvVariables = z.object({ S3_ACCESS_KEY_ID: z.string(), S3_SECRET_ACCESS_KEY: z.string(), S3_BUCKET: z.string(), - S3_ACL: z.string().optional().default("public-read"), - WORKER_URL: z.string().optional(), + S3_ACL: z.string().optional(), + ASSET_CDN_URL: z.string().optional(), }); export const fsEnvVariables = z.object({ diff --git a/packages/asset-uploader/src/targets/disk/upload.ts b/packages/asset-uploader/src/targets/disk/upload.ts index 98fb57d26d..ebb0fd6b01 100644 --- a/packages/asset-uploader/src/targets/disk/upload.ts +++ b/packages/asset-uploader/src/targets/disk/upload.ts @@ -29,5 +29,5 @@ export const uploadToDisk = async ({ return newAsset; }); - await Promise.all(allInfo); + return await Promise.all(allInfo); }; diff --git a/packages/asset-uploader/src/targets/s3/handler.ts b/packages/asset-uploader/src/targets/s3/handler.ts index ab99c93665..a2573ff9c1 100644 --- a/packages/asset-uploader/src/targets/s3/handler.ts +++ b/packages/asset-uploader/src/targets/s3/handler.ts @@ -43,8 +43,8 @@ export const s3UploadHandler: S3UploadHandler = async ({ const [name, extension] = getFilenameAndExtension(filename); const key = encodeURI(`${name}_${Date.now()}.${extension}`); - // r2 does not have ACL support so in that case we pass nothing there - const ACL = s3Envs.WORKER_URL ? {} : { ACL: s3Envs.S3_ACL }; + // if there is no ACL passed we do not default since some providers do not support it + const ACL = s3Envs.S3_ACL ? { ACL: s3Envs.S3_ACL } : {}; const params: PutObjectCommandInput = { Bucket: s3Envs.S3_BUCKET, diff --git a/packages/prisma-client/src/types.ts b/packages/prisma-client/src/types.ts index 6fe42524f1..fbaabd8d76 100644 --- a/packages/prisma-client/src/types.ts +++ b/packages/prisma-client/src/types.ts @@ -2,5 +2,8 @@ import type { Asset as DbAsset } from "@prisma/client"; export { Location } from "@prisma/client"; export type { InstanceProps, Project, User, Breakpoints } from "@prisma/client"; -export type Asset = DbAsset & { path: string }; +export type Asset = DbAsset & { + path: string; + status?: "uploading" | "uploaded"; +}; export type { DbAsset };