mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
Merge branch 'main' into fix-sync-status-icon
This commit is contained in:
17 files changed
+189
-61
No files matched your search
@@ -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
|
||||
@@ -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=
|
||||
ASSET_CDN_URL=
|
||||
@@ -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<Asset> }) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const submit = useSubmit();
|
||||
import { AddAnAssetForm } from "./components/add-an-asset-form";
|
||||
import { UploadingAsset } from "../types";
|
||||
|
||||
export const TabContent = ({
|
||||
assets: baseAssets,
|
||||
}: {
|
||||
assets: Array<Asset | UploadingAsset>;
|
||||
}) => {
|
||||
const newImages = useActionData();
|
||||
|
||||
const [assets, setAssets] = useState(baseAssets);
|
||||
|
||||
useEffect(() => {
|
||||
if (newImages?.length) {
|
||||
setAssets((currentAssets) => [
|
||||
...newImages,
|
||||
...currentAssets.filter((asset) => asset.status !== "uploading"),
|
||||
]);
|
||||
}
|
||||
}, [newImages]);
|
||||
|
||||
return (
|
||||
<Flex gap="3" direction="column" css={{ padding: "$1" }}>
|
||||
<Flex justify="between">
|
||||
<Heading>Assets</Heading>
|
||||
<Form
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
onChange={(event) => {
|
||||
if (inputRef.current?.files) {
|
||||
submit(event.currentTarget);
|
||||
event.currentTarget.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
accept="image/*"
|
||||
type="file"
|
||||
name="image"
|
||||
multiple
|
||||
ref={inputRef}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Button type="button" onClick={() => inputRef?.current?.click()}>
|
||||
Upload Image
|
||||
</Button>
|
||||
</Form>
|
||||
<AddAnAssetForm
|
||||
onSubmit={(uploadedAssets: Array<UploadingAsset>) =>
|
||||
setAssets((assets) => [...uploadedAssets, ...assets])
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Grid columns={2} gap={2}>
|
||||
{assets.map((asset) => (
|
||||
<AssetManagerImage key={asset.id} asset={asset} />
|
||||
<AssetManagerImage
|
||||
key={asset.id}
|
||||
path={asset.path}
|
||||
alt={asset.alt || asset.name}
|
||||
status={asset.status}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Flex>
|
||||
|
||||
+66
@@ -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<UploadingAsset>) => void;
|
||||
}) => {
|
||||
const submit = useSubmit();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onFormChange = async (event: ChangeEvent<HTMLFormElement>) => {
|
||||
const newFiles = inputRef?.current?.files;
|
||||
if (newFiles) {
|
||||
submit(event.currentTarget);
|
||||
const parsedFiles = await readImages(newFiles);
|
||||
onSubmit(parsedFiles);
|
||||
event.currentTarget.reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form method="post" encType="multipart/form-data" onChange={onFormChange}>
|
||||
<input
|
||||
accept="image/*"
|
||||
type="file"
|
||||
name="image"
|
||||
multiple
|
||||
ref={inputRef}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Button type="button" onClick={() => inputRef?.current?.click()}>
|
||||
Upload Image
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Box
|
||||
title={alt || ""}
|
||||
css={{
|
||||
aspectRatio: "1/1",
|
||||
backgroundImage: `url(${src})`,
|
||||
backgroundSize: "contain",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "center",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: "0 $2",
|
||||
position: "relative",
|
||||
}}
|
||||
></Box>
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
backgroundImage: `url("${src}")`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundSize: "contain",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "center",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
...(isUploading ? { filter: "blur(1px)", opacity: 0.7 } : {}),
|
||||
}}
|
||||
></Box>
|
||||
{isUploading && (
|
||||
<ProgressBar
|
||||
value={progressBarPercentage}
|
||||
max={60}
|
||||
css={{ width: "100%", height: "$1" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
>;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -29,5 +29,5 @@ export const uploadToDisk = async ({
|
||||
return newAsset;
|
||||
});
|
||||
|
||||
await Promise.all(allInfo);
|
||||
return await Promise.all(allInfo);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user