Merge pull request #136 from webstudio-is/upload-assets-pt1

This commit is contained in:
Sara Vieira authored and GitHub committed 2022-07-16 18:17:50 +02:00
commit ded239fcee
20 files changed
+248 -20

No files matched your search

+1
View File
@@ -40,3 +40,4 @@ yarn-error.log*
/data
*.db
*.db-journal
uploads
+5 -1
View File
@@ -2,4 +2,8 @@ DATABASE_URL=postgresql://user:pass@localhost/webstudio
AUTH_SECRET="# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32"
GH_CLIENT_SECRET=
GH_CLIENT_ID=
DEV_LOGIN=true
DEV_LOGIN=true
# relative to the public directory
FILE_UPLOAD_PATH="uploads"
+2
View File
@@ -8,6 +8,8 @@ const config = {
googleCallbackPath: "/google/callback",
githubCallbackPath: "/github/callback",
authPath: "/auth",
defaultUploadPath: "uploads",
};
export default config;
export type Config = typeof config;
+8 -2
View File
@@ -3,6 +3,7 @@ import { DndProvider } from "react-dnd";
import { TouchBackend } from "react-dnd-touch-backend";
import {
type Project,
type Asset,
useSubscribe,
usePublish,
} from "@webstudio-is/react-sdk";
@@ -176,9 +177,10 @@ const dndOptions = { enableMouseEvents: true };
type DesignerProps = {
config: Config;
project: Project;
assets: Array<Asset>;
};
export const Designer = ({ config, project }: DesignerProps) => {
export const Designer = ({ config, project, assets }: DesignerProps) => {
useSubscribeSyncStatus();
useSubscribeRootInstance();
useSubscribeSelectedInstanceData();
@@ -225,7 +227,11 @@ export const Designer = ({ config, project }: DesignerProps) => {
</Workspace>
</Main>
<SidePanel gridArea="sidebar" isPreviewMode={isPreviewMode}>
<SidebarLeft onDragChange={setIsDragging} publish={publish} />
<SidebarLeft
assets={assets}
onDragChange={setIsDragging}
publish={publish}
/>
</SidePanel>
<SidePanel
gridArea="inspector"
@@ -0,0 +1,55 @@
import { ImageIcon } from "~/shared/icons";
import { Button, Flex, Grid, Heading } from "~/shared/design-system";
import { useRef } from "react";
import { Form } from "@remix-run/react";
import { type Asset } from "@webstudio-is/react-sdk";
import { Image } from "~/shared/design-system/components/image";
export const TabContent = ({ assets }: { assets: Array<Asset> }) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const submitButtonRef = useRef<HTMLButtonElement | null>(null);
const onChange = () => {
if (submitButtonRef.current) {
submitButtonRef.current.click();
}
};
return (
<Flex gap="3" direction="column" css={{ padding: "$1" }}>
<Flex justify="between">
<Heading>Assets</Heading>
<Form method="post" encType="multipart/form-data">
<input
accept="image/*"
type="file"
name="image"
multiple
ref={inputRef}
onChange={onChange}
style={{ display: "none" }}
/>
<button
ref={submitButtonRef}
type="submit"
style={{ display: "none" }}
/>
<Button onClick={() => inputRef?.current?.click()}>
Upload Image
</Button>
</Form>
</Flex>
<Grid columns={2} gap={2}>
{assets.map((asset) => (
<Image
key={asset.id}
src={asset.path}
alt={asset.alt || asset.name}
/>
))}
</Grid>
</Flex>
);
};
export const icon = <ImageIcon />;
@@ -1,2 +1,5 @@
export * as components from "./components";
export * as tree from "./tree";
import * as components from "./components";
import * as tree from "./tree";
import * as imageUpload from "./image-upload";
export const panels = { components, tree, imageUpload } as const;
@@ -1,5 +1,9 @@
import { useState, useCallback } from "react";
import { useSubscribe, type Publish } from "@webstudio-is/react-sdk";
import {
useSubscribe,
type Publish,
type Asset,
} from "@webstudio-is/react-sdk";
import {
Box,
SidebarTabs,
@@ -8,8 +12,9 @@ import {
SidebarTabsTrigger,
} from "~/shared/design-system";
import { useSelectedInstanceData } from "../../shared/nano-states";
import * as panels from "./panels";
import { panels } from "./panels";
import type { TabName } from "./types";
import { isFeatureEnabled } from "~/shared/feature-flags";
const sidebarTabsContentStyle = {
position: "absolute",
@@ -26,9 +31,14 @@ const none = { TabContent: () => null };
type SidebarLeftProps = {
onDragChange: (isDragging: boolean) => void;
publish: Publish;
assets: Array<Asset>;
};
export const SidebarLeft = ({ onDragChange, publish }: SidebarLeftProps) => {
export const SidebarLeft = ({
onDragChange,
publish,
assets,
}: SidebarLeftProps) => {
const [selectedInstanceData] = useSelectedInstanceData();
const [activeTab, setActiveTab] = useState<TabName>("none");
const [isDragging, setIsDragging] = useState(false);
@@ -55,11 +65,17 @@ export const SidebarLeft = ({ onDragChange, publish }: SidebarLeftProps) => {
[onDragChange]
);
const enabledPanels = (
isFeatureEnabled("assets")
? Object.keys(panels)
: Object.keys(panels).filter((panel) => panel !== "imageUpload")
) as Array<TabName>;
return (
<Box css={{ position: "relative", zIndex: 1 }}>
<SidebarTabs activationMode="manual" value={activeTab}>
<SidebarTabsList>
{(Object.keys(panels) as Array<TabName>).map((tabName: TabName) => (
{enabledPanels.map((tabName: TabName) => (
<SidebarTabsTrigger
aria-label={tabName}
key={tabName}
@@ -83,6 +99,7 @@ export const SidebarLeft = ({ onDragChange, publish }: SidebarLeftProps) => {
}}
>
<TabContent
assets={assets}
selectedInstanceData={selectedInstanceData}
publish={publish}
onSetActiveTab={setActiveTab}
@@ -1,3 +1,14 @@
import * as tabs from "./panels";
import { panels } from "./panels";
import { z } from "zod";
export type TabName = keyof typeof tabs | "none";
export type TabName = keyof typeof panels | "none";
const SingleImageInUpload = z.object({
name: z.string(),
type: z.string(),
});
export const ImagesUpload = z.array(SingleImageInUpload);
export type ImagesUpload = z.infer<typeof ImagesUpload>;
export type SingleImageInUpload = z.infer<typeof SingleImageInUpload>;
+1 -1
View File
@@ -4,7 +4,7 @@ const env = {
SENTRY_DSN: process.env.SENTRY_DSN,
VERCEL_ENV: process.env.VERCEL_ENV,
DEBUG: process.env.DEBUG,
FEATURE: process.env.FEATURE,
FEATURES: process.env.FEATURES,
} as const;
export default env;
+54 -3
View File
@@ -1,30 +1,81 @@
import { useLoaderData } from "@remix-run/react";
import { LoaderFunction } from "@remix-run/node";
import type { Project } from "@webstudio-is/react-sdk";
import path from "path";
import {
ActionFunction,
LoaderFunction,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
} from "@remix-run/node";
import type { Project, Asset } from "@webstudio-is/react-sdk";
import { Designer, links } from "~/designer";
import * as db from "~/shared/db";
import config from "~/config";
import env from "~/env.server";
import { ImagesUpload } from "~/designer/features/sidebar-left/types";
export { links };
export const loader: LoaderFunction = async ({ params }) => {
if (params.id === undefined) throw new Error("Project id undefined");
const project = await db.project.loadById(params.id);
const assets = await db.assets.loadByProject(params.id);
if (project === null) {
return { errors: `Project "${params.id}" not found` };
}
return { config, project, env };
return { config, assets, project, env };
};
type Data = {
config: typeof config;
project: Project;
assets: Asset[];
};
type Error = {
errors: "string";
};
export const action: ActionFunction = async ({ request, params }) => {
if (params.id === undefined) throw new Error("Project id undefined");
const uploads = path.join(__dirname, "../public");
const folderInPublic =
process.env.FILE_UPLOAD_PATH || config.defaultUploadPath;
const directory = path.join(uploads, folderInPublic);
try {
const formData = await unstable_parseMultipartFormData(
request,
unstable_createFileUploadHandler({
maxPartSize: 10_000_000,
directory,
file: ({ filename }) => filename,
})
);
const imagesInfo = ImagesUpload.parse(formData.getAll("image"));
imagesInfo.forEach((image) => {
const data = {
type: image.type,
name: image.name,
path: `${path.join("/", folderInPublic, image.name)}`,
};
const projectId = params.id as string;
db.assets.create(projectId, data);
});
return {
ok: true,
};
} catch (error) {
if (error instanceof Error) {
return {
errors: error.message,
};
}
}
};
const DesignerRoute = () => {
const data = useLoaderData<Data | Error>();
if ("errors" in data) {
@@ -0,0 +1,28 @@
import { type Project } from "@webstudio-is/react-sdk";
import { prisma } from "./prisma.server";
export const loadByProject = async (projectId?: Project["id"]) => {
if (typeof projectId !== "string") {
throw new Error("Tree ID required");
}
const assets = await prisma.asset.findMany({
where: { projectId },
});
return assets;
};
export const create = async (
projectId: Project["id"],
values: { type: string; name: string; path: string }
) => {
const newAsset = await prisma.asset.create({
data: {
...values,
projectId,
},
});
return newAsset;
};
+1
View File
@@ -5,3 +5,4 @@ export * as misc from "./misc.server";
export * as user from "./user.server";
export * from "./canvas.server";
export * as breakpoints from "./breakpoints.server";
export * as assets from "./assets.server";
@@ -9,7 +9,7 @@ const parse = (flags?: string | null): Array<Name> =>
const readLocal = (): Array<Name> => {
try {
const flags = localStorage.getItem("feature");
const flags = localStorage.getItem("features");
return parse(flags);
} catch (_error) {
// Not having feature in localStorage or not having localStorage implemented, both should not throw.
@@ -21,12 +21,12 @@ const readLocal = (): Array<Name> => {
* Returns true/false if the feature is turned on.
* A feature can be turned on:
* - by default directly in ./flags
* - by providing an environment variable server-side (locally or on the server): FEATURE="something1, something2" yarn dev
* - by setting it in the browser console: localStorage.feature = 'something1, something2', browser defined flag will override server-side flag
* - by providing an environment variable server-side (locally or on the server): FEATURES="something1, something2" yarn dev
* - by setting it in the browser console: localStorage.features = 'something1, something2', browser defined flag will override server-side flag
*/
export const isFeatureEnabled = (name: Name): boolean => {
const defaultValue = flags[name];
const envValue = parse(env.FEATURE).includes(name);
const envValue = parse(env.FEATURES).includes(name);
const localValue = readLocal().includes(name);
// Any source can enable feature, first `true` value will result in enabling a feature.
// This also means you can't disable a feature if its already enabled in default value.
@@ -1 +1,2 @@
export const assets = false;
export const example = false;
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "Asset" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"name" TEXT NOT NULL,
"path" TEXT NOT NULL,
"size" DOUBLE PRECISION NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Asset_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Asset" ADD CONSTRAINT "Asset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `size` on the `Asset` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Asset" DROP COLUMN "size";
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Asset" ADD COLUMN "alt" TEXT;
+11
View File
@@ -13,7 +13,17 @@ datasource db {
model Team {
id String @id @default(uuid())
users User[]
}
model Asset {
id String @id @default(uuid())
project Project @relation(fields: [projectId], references: [id])
projectId String
type String
name String
path String
alt String?
createdAt DateTime @default(now())
}
model User {
@@ -37,6 +47,7 @@ model Project {
prodTreeIdHistory String @default("[]")
User User? @relation(fields: [userId], references: [id])
userId String?
Asset Asset[]
}
model Tree {
@@ -13,7 +13,17 @@ datasource db {
model Team {
id String @id @default(uuid())
users User[]
}
model Asset {
id String @id @default(uuid())
project Project @relation(fields: [projectId], references: [id])
projectId String
type String
name String
path String
alt String?
createdAt DateTime @default(now())
}
model User {
@@ -37,6 +47,7 @@ model Project {
prodTreeIdHistory String @default("[]")
User User? @relation(fields: [userId], references: [id])
userId String?
Asset Asset[]
}
model Tree {
+2 -1
View File
@@ -3,6 +3,7 @@ import type {
Project,
User,
Breakpoints,
Asset,
} from "@prisma/client";
import type { UserProp } from "../user-props";
import type { Instance } from "./instance";
@@ -16,4 +17,4 @@ export type InstanceProps = Omit<DbInstanceProps, "props"> & {
props: Array<UserProp>;
};
export type { DbInstanceProps, Project, User, Breakpoints };
export type { DbInstanceProps, Project, User, Breakpoints, Asset };