Merge branch 'main' into 68-move-content-editable-toolbar-to-workspace-tools

This commit is contained in:
Oleg Isonen committed 2022-06-30 18:03:11 +02:00
commit 5c7da8feeb
15 files changed
+287 -105

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
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=
GH_CLIENT_ID=
DEV_LOGIN=true
-10
View File
@@ -3,7 +3,6 @@ on: [push]
jobs:
checks:
env:
DATABASE_URL: mongo://
NODE_ENV: test
runs-on: ${{ matrix.os }}
strategy:
@@ -13,16 +12,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v2
# - name: Install deps and build (with cache)
# uses: bahmutov/npm-install@v1
- run: yarn install
- name: Next.js cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/yarn.lock') }}-nextjs
- run: yarn checks
- run: yarn build
+16 -5
View File
@@ -1,7 +1,14 @@
import { LinksFunction, MetaFunction } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { useState } from "react";
import { Button, Card, Flex, Heading, TextField } from "~/shared/design-system";
import {
Button,
Card,
Flex,
Heading,
Text,
TextField,
} from "~/shared/design-system";
import interStyles from "~/shared/font-faces/inter.css";
import { GoogleIcon, GithubIcon, CommitIcon } from "~/shared/icons";
@@ -24,7 +31,7 @@ export const meta: MetaFunction = () => {
return { title: "Webstudio Login" };
};
export const Login = () => {
export const Login = ({ errorMessage }: { errorMessage: string }) => {
const [isDevLogin, setIsDevLogin] = useState(false);
const loaderData = useLoaderData();
@@ -35,10 +42,14 @@ export const Login = () => {
align="center"
justify="center"
>
<Card css={{ width: "$10", padding: "$5", zoom: 1.4 }} variant="active">
<Card css={{ width: 200, padding: "$5", zoom: 1.4 }} variant="active">
<Flex direction="column" gap="2" align="center">
<Heading>Login</Heading>
{errorMessage.length ? (
<Text css={{ textAlign: "center" }} variant="red">
{errorMessage}
</Text>
) : null}
<Flex gap="2" direction="column" align="center">
<Form action="/auth/github" method="post">
<Button type="submit">
@@ -67,7 +78,7 @@ export const Login = () => {
minLength={2}
required
autoFocus
placeholder="Place your AUTH_SECRET"
placeholder="Place your auth secret here"
/>
</Form>
) : (
+19 -6
View File
@@ -1,14 +1,27 @@
import { ActionFunction } from "@remix-run/node";
import { ActionFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
export default function Dev() {
return null;
}
export const action: ActionFunction = ({ request }) => {
return authenticator.authenticate("dev", request, {
successRedirect: config.dashboardPath,
failureRedirect: config.loginPath,
});
export const action: ActionFunction = async ({ request }) => {
try {
return await authenticator.authenticate("dev", request, {
successRedirect: config.dashboardPath,
throwOnError: true,
});
} catch (error: unknown) {
// all redirects are basically errors and in that case we don't want to catch it
if (error instanceof Response) return error;
if (error instanceof Error) {
return redirect(
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_DEV}&message=${
error?.message || ""
}`
);
}
}
};
+29 -9
View File
@@ -1,19 +1,39 @@
import { ActionFunction } from "@remix-run/node";
import { ActionFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { sentryException } from "~/shared/sentry";
import { ensureUserCookie } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
export default function GH() {
return null;
}
export const action: ActionFunction = async ({ request }) => {
const { userId } = await ensureUserCookie(request);
return authenticator.authenticate("github", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
failureRedirect: config.loginPath,
});
try {
const { userId } = await ensureUserCookie(request);
return await authenticator.authenticate("github", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
throwOnError: true,
});
} catch (error: unknown) {
// all redirects are basically errors and in that case we don't want to catch it
if (error instanceof Response) return error;
if (error instanceof Error) {
sentryException({
message: error.message,
extra: {
loginMethod: AUTH_PROVIDERS.LOGIN_GITHUB,
},
});
return redirect(
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GITHUB}&message=${
error?.message || ""
}`
);
}
}
};
+12 -1
View File
@@ -1,10 +1,21 @@
import { LoaderFunction } from "@remix-run/node";
import { LoaderFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { ensureUserCookie } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
export const loader: LoaderFunction = async ({ request }) => {
const { userId } = await ensureUserCookie(request);
const url = new URL(request.url);
const error = url.searchParams.get("error");
const error_description = url.searchParams.get("error_description");
if (error) {
return redirect(
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GITHUB}&message=${
error_description || error
}`
);
}
return authenticator.authenticate("github", request, {
context: {
userId,
+28 -8
View File
@@ -1,7 +1,9 @@
import { ActionFunction, LoaderFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { sentryException } from "~/shared/sentry";
import { ensureUserCookie } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
export default function Google() {
return null;
@@ -10,12 +12,30 @@ export default function Google() {
export const loader: LoaderFunction = () => redirect("/login");
export const action: ActionFunction = async ({ request }) => {
const { userId } = await ensureUserCookie(request);
return authenticator.authenticate("google", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
failureRedirect: config.loginPath,
});
try {
const { userId } = await ensureUserCookie(request);
return await authenticator.authenticate("google", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
throwOnError: true,
});
} catch (error: unknown) {
// all redirects are basically errors and in that case we don't want to catch it
if (error instanceof Response) return error;
if (error instanceof Error) {
sentryException({
message: error.message,
extra: {
loginMethod: AUTH_PROVIDERS.LOGIN_GOOGLE,
},
});
return redirect(
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GOOGLE}&message=${
error?.message || ""
}`
);
}
}
};
+3 -1
View File
@@ -5,6 +5,7 @@ import config from "~/config";
import env from "~/env.server";
import { Login, links } from "~/auth/";
import { useLoginErrorMessage } from "~/shared/session/useLoginErrorMessage";
export { links };
@@ -18,7 +19,8 @@ export const loader: LoaderFunction = async ({ request }) => {
};
const LoginRoute = () => {
return <Login />;
const errorMessage = useLoginErrorMessage();
return <Login errorMessage={errorMessage} />;
};
export default LoginRoute;
+4 -3
View File
@@ -11,6 +11,7 @@ import {
createOrLoginWithGoogle,
} from "~/shared/db/user.server";
import { sentryException } from "~/shared/sentry";
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
const url = `${
process.env.VERCEL_URL
@@ -49,8 +50,8 @@ const google = new GoogleStrategy(
export const authenticator = new Authenticator<User>(sessionStorage);
if (process.env.GH_CLIENT_ID && process.env.GH_CLIENT_SECRET) {
authenticator.use(github, "github");
authenticator.use(google, "google");
}
authenticator.use(google, "google");
if (process.env.DEV_LOGIN === "true") {
authenticator.use(
@@ -66,14 +67,14 @@ if (process.env.DEV_LOGIN === "true") {
sentryException({
message: error.message,
extra: {
loginMethod: "dev",
loginMethod: AUTH_PROVIDERS.LOGIN_DEV,
},
});
}
}
}
throw new Error("Wrong code");
throw new Error("The dev login code is incorrect");
}),
"dev"
);
+15 -14
View File
@@ -1,16 +1,17 @@
import {
PrismaClient,
Prisma,
PrismaClientKnownRequestError,
} from "@webstudio-is/sdk/lib/prisma.server";
import { PrismaClient, Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
declare global {
// allow global `var` declarations
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
// this fixes the issue with `warn(prisma-client) There are already 10 instances of Prisma Client actively running.`
// explanation here
// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
export const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
export const prisma = new PrismaClient();
export { Prisma, PrismaClientKnownRequestError };
const main = async () => {
await prisma.$connect();
};
main().catch((error) => {
throw error;
});
@@ -1,39 +0,0 @@
import {useRef, useEffect} from 'react';
import {numericGestureControl} from './numericGestureControl.js';
const useNumericGestureControl = ({ref, value, direction}) => {
useEffect(() => {
const {disconnectedCallback} = numericGestureControl(ref.current, {
initialValue: ref.current.value = value,
direction: direction,
onValueChange: (event) => {
event.preventDefault();
event.target.value = event.value;
event.target.select();
}
});
return () => disconnectedCallback();
}, [direction]);
}
const Input = ({value, direction}) => {
const ref = useRef();
useNumericGestureControl({ref, value, direction});
return <input defaultValue={value} ref={ref} />;
}
export const NumericInput = Object.assign(Input.bind({}), {args: { value: 0, direction: 'horizontal' }});
export default {
title: 'numericGestureControl',
component: Input,
argTypes: {
value: {
control: { type: 'number' },
},
direction: {
options: ['horizontal', 'vertical'],
control: { type: 'radio' },
},
},
};
@@ -0,0 +1,61 @@
import { useRef, useEffect, RefObject } from "react";
import {
numericGestureControl,
type Value,
type Direction,
} from "./numericGestureControl";
const useNumericGestureControl = ({
ref,
value,
direction,
}: {
ref: RefObject<HTMLInputElement>;
value: Value;
direction: Direction;
}) => {
useEffect(() => {
if (ref.current === null) return;
ref.current.value = String(value);
const { disconnectedCallback } = numericGestureControl(ref.current, {
initialValue: value,
direction: direction,
onValueChange: (event) => {
event.preventDefault();
event.target.value = String(event.value);
event.target.select();
},
});
return () => disconnectedCallback();
}, [direction, value, ref]);
};
const Input = ({
value,
direction,
}: {
value: Value;
direction: Direction;
}) => {
const ref = useRef<HTMLInputElement | null>(null);
useNumericGestureControl({ ref, value, direction });
return <input defaultValue={value} ref={ref} />;
};
export const NumericInput = Object.assign(Input.bind({}), {
args: { value: 0, direction: "horizontal" },
});
export default {
title: "numericGestureControl",
component: Input,
argTypes: {
value: {
control: { type: "number" },
},
direction: {
options: ["horizontal", "vertical"],
control: { type: "radio" },
},
},
};
@@ -13,15 +13,34 @@
* }
* });
*/
export type Direction = "horizontal" | "vertical";
export type Value = number;
type Options = {
initialValue: Value;
direction: Direction;
onValueChange: (event: {
target: HTMLInputElement;
value: Value;
preventDefault: () => void;
}) => void;
};
export const numericGestureControl = (
targetNode,
{ initialValue = 0, direction = "horizontal", onValueChange = ({}) => null }
targetNode: HTMLInputElement,
{
initialValue = 0,
direction = "horizontal",
onValueChange = () => null,
}: Options
) => {
const { ownerDocument } = targetNode;
const eventNames = ["pointerup", "pointerdown", "pointermove"];
const eventNames = ["pointerup", "pointerdown", "pointermove"] as const;
const state = {
value: initialValue,
cursor: cursorNode.cloneNode(true),
cursor: cursorNode.cloneNode(true) as HTMLElement,
offset: 0,
velocity: direction === "horizontal" ? 1 : -1,
position: direction === "horizontal" ? "left" : "top",
@@ -31,7 +50,7 @@ export const numericGestureControl = (
direction === "horizontal" ? "0 1px" : "1px 0"
} 1.1px rgba(0,0,0,.4))`,
};
const handleEvent = (event) => {
const handleEvent = (event: PointerEvent) => {
const { type, pressure, offsetX, offsetY, movementY, movementX } = event;
const offset = direction === "horizontal" ? offsetX : offsetY;
const movement = direction === "horizontal" ? movementX : -movementY;
@@ -42,7 +61,6 @@ export const numericGestureControl = (
}
case "pointerdown": {
const handlePointerLockEvent = () => {
const { pointerLockElement } = ownerDocument;
switch (ownerDocument.pointerLockElement) {
case targetNode: {
state.offset = offset;
@@ -77,7 +95,9 @@ export const numericGestureControl = (
else if (state.offset > globalThis.innerWidth) state.offset = 1;
state.value += movement;
state.offset += movement * state.velocity;
state.cursor.style[state.position] = `${state.offset}px`;
state.cursor.style[
state.position as "top" | "left"
] = `${state.offset}px`;
onValueChange({
target: targetNode,
value: state.value,
@@ -109,4 +129,4 @@ const cursorNode = new Range().createContextualFragment(`
<path d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" fill="#111" fill-rule="evenodd"></path>
</g>
</svg>
`).firstElementChild;
`).firstElementChild as HTMLElement;
@@ -0,0 +1,60 @@
import isNil from "lodash/isNil";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { sentryException } from "../sentry";
export const AUTH_PROVIDERS = {
LOGIN_DEV: "login_dev",
LOGIN_GITHUB: "login_github",
LOGIN_GOOGLE: "login_google",
};
export const LOGIN_ERROR_MESSAGES = {
[AUTH_PROVIDERS.LOGIN_DEV]: "There has been an issue logging you in with dev",
[AUTH_PROVIDERS.LOGIN_GITHUB]:
"There has been an issue logging you in with Github",
[AUTH_PROVIDERS.LOGIN_GOOGLE]:
"There has been an issue logging you in with Google",
};
export const useLoginErrorMessage = (): string => {
const [searchParams, setSearchParams] = useSearchParams();
const [messageToReturn, setMessageToReturn] = useState("");
useEffect(() => {
const error = searchParams.get("error");
const message = searchParams.get("message");
const hasMessageToShow =
error !== null && !isNil(message) && message !== "";
if (hasMessageToShow) {
sentryException({
message: message,
});
setMessageToReturn(message);
setSearchParams({});
return;
}
switch (error) {
case AUTH_PROVIDERS.LOGIN_DEV:
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_DEV]);
break;
case AUTH_PROVIDERS.LOGIN_GITHUB:
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GITHUB]);
break;
case AUTH_PROVIDERS.LOGIN_GOOGLE:
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GOOGLE]);
break;
default:
break;
}
setSearchParams({});
}, [searchParams, setSearchParams]);
return messageToReturn;
};
+10
View File
@@ -57,3 +57,13 @@
- Click on the canvas closes breakpoints selector
- Styles added on each breakpoint are only applied to each breakpoint, verify that by resizing the canvas in preview mode
- Resizing canvas width in preview mode doesn't limit resize slider to each breakpoint
1. Auth
- Try to access the dashboard when not logged in to make sure you will be redirected to the login page
- 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 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