Merge pull request #116 from webstudio-is/main

This commit is contained in:
Sara Vieira authored and GitHub committed 2022-07-05 22:45:34 +02:00
commit c51025aafe
12 files changed
+273 -90

No files matched your search

+3 -3
View File
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from "react";
import { DndProvider } from "react-dnd";
import { TouchBackend } from "react-dnd-touch-backend";
import store from "immerhin";
import * as db from "~/shared/db";
import {
type OnChangeChildren,
type Data,
@@ -41,7 +42,6 @@ import {
useHandleBreakpoints,
useInitializeBreakpoints,
} from "./shared/breakpoints";
import type { Project } from "~/shared/db/project.server";
import {
rootInstanceContainer,
useBreakpoints,
@@ -102,7 +102,7 @@ const PreviewMode = () => {
type DesignModeProps = {
treeId: Tree["id"];
project: Project;
project: db.project.Project;
};
const dndOptions = { enableMouseEvents: true };
@@ -141,7 +141,7 @@ const DesignMode = ({ treeId, project }: DesignModeProps) => {
};
type CanvasProps = {
data: Data & { project: Project };
data: Data & { project: db.project.Project };
};
export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
+2 -2
View File
@@ -1,9 +1,9 @@
import useInterval from "react-use/lib/useInterval";
import { sync } from "immerhin";
import { enqueue } from "./queue";
import type { Project } from "~/shared/db/project.server";
import * as db from "~/shared/db";
export const useSync = ({ project }: { project: Project }) => {
export const useSync = ({ project }: { project: db.project.Project }) => {
useInterval(() => {
const entries = sync();
if (entries.length === 0) return;
+1 -5
View File
@@ -2,7 +2,7 @@ import { ActionFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { sentryException } from "~/shared/sentry";
import { ensureUserCookie, AUTH_PROVIDERS } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session";
export default function GH() {
return null;
@@ -10,11 +10,7 @@ export default function GH() {
export const action: ActionFunction = async ({ request }) => {
try {
const { userId } = await ensureUserCookie(request);
return await authenticator.authenticate("github", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
throwOnError: true,
});
+1 -5
View File
@@ -1,10 +1,9 @@
import { LoaderFunction, redirect } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { ensureUserCookie, AUTH_PROVIDERS } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session";
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");
@@ -16,9 +15,6 @@ export const loader: LoaderFunction = async ({ request }) => {
);
}
return authenticator.authenticate("github", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
failureRedirect: config.loginPath,
});
+1 -5
View File
@@ -2,7 +2,7 @@ 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, AUTH_PROVIDERS } from "~/shared/session";
import { AUTH_PROVIDERS } from "~/shared/session";
export default function Google() {
return null;
@@ -12,11 +12,7 @@ export const loader: LoaderFunction = () => redirect("/login");
export const action: ActionFunction = async ({ request }) => {
try {
const { userId } = await ensureUserCookie(request);
return await authenticator.authenticate("google", request, {
context: {
userId,
},
successRedirect: config.dashboardPath,
throwOnError: true,
});
-5
View File
@@ -1,15 +1,10 @@
import { LoaderFunction } from "@remix-run/node";
import config from "~/config";
import { authenticator } from "~/services/auth.server";
import { ensureUserCookie } from "~/shared/session";
export const loader: LoaderFunction = async ({ request }) => {
const { userId } = await ensureUserCookie(request);
return authenticator.authenticate("google", request, {
successRedirect: config.dashboardPath,
failureRedirect: config.loginPath,
context: {
userId,
},
});
};
+11 -4
View File
@@ -3,7 +3,7 @@ import type { User } from "@webstudio-is/sdk";
import * as db from "~/shared/db";
import { ensureUserCookie } from "~/shared/session";
import config from "~/config";
import type { Project } from "~/shared/db/project.server";
import { authenticator } from "~/services/auth.server";
const ensureProject = async ({
userId,
@@ -11,7 +11,7 @@ const ensureProject = async ({
}: {
userId: User["id"];
domain: string;
}): Promise<Project> => {
}): Promise<db.project.Project> => {
const projects = await db.project.loadManyByUserId(userId);
if (projects.length !== 0) return projects[0];
@@ -28,9 +28,16 @@ const ensureProject = async ({
*/
export const loader: LoaderFunction = async ({ request, params }) => {
if (params.domain === undefined) return { errors: "Domain required" };
const { headers, userId } = await ensureUserCookie(request);
const user = await authenticator.isAuthenticated(request);
const { headers, userId: generatedUserId } = await ensureUserCookie(request);
try {
const project = await ensureProject({ userId, domain: params.domain });
const userId = await db.user.ensureUser({
userId: user ? user.id : generatedUserId,
});
const project = await ensureProject({
userId,
domain: params.domain,
});
return redirect(`${config.designerPath}/${project?.id}`, { headers });
} catch (error: unknown) {
if (error instanceof Error) {
+6 -10
View File
@@ -4,12 +4,8 @@ import { FormStrategy } from "remix-auth-form";
import { GitHubStrategy } from "remix-auth-github";
import { GoogleStrategy } from "remix-auth-google";
import config from "~/config";
import * as db from "~/shared/db";
import { sessionStorage } from "~/services/session.server";
import {
createOrLoginWithDev,
createOrLoginWithGithub,
createOrLoginWithGoogle,
} from "~/shared/db/user.server";
import { sentryException } from "~/shared/sentry";
import { AUTH_PROVIDERS } from "~/shared/session";
@@ -25,8 +21,8 @@ const github = new GitHubStrategy(
clientSecret: process.env.GH_CLIENT_SECRET as string,
callbackURL: `${url}${config.githubCallbackPath}`,
},
async ({ profile, context }) => {
const user = await createOrLoginWithGithub(profile, context?.userId);
async ({ profile }) => {
const user = await db.user.createOrLoginWithGithub(profile);
return user;
}
@@ -38,8 +34,8 @@ const google = new GoogleStrategy(
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
callbackURL: `${url}${config.googleCallbackPath}`,
},
async ({ profile, context }) => {
const user = await createOrLoginWithGoogle(profile, context?.userId);
async ({ profile }) => {
const user = await db.user.createOrLoginWithGoogle(profile);
return user;
}
@@ -61,7 +57,7 @@ if (process.env.DEV_LOGIN === "true") {
if (secret === process.env.AUTH_SECRET) {
try {
const user = await createOrLoginWithDev(secret);
const user = await db.user.createOrLoginWithDev();
return user;
} catch (error: unknown) {
if (error instanceof Error) {
+1
View File
@@ -2,5 +2,6 @@ export * as tree from "./tree.server";
export * as project from "./project.server";
export * as props from "./props.server";
export * as misc from "./misc.server";
export * as user from "./user.server";
export * from "./canvas.server";
export * as breakpoints from "./breakpoints.server";
+37 -51
View File
@@ -3,45 +3,40 @@ import { GitHubProfile } from "remix-auth-github";
import { GoogleProfile } from "remix-auth-google";
import { prisma } from "./prisma.server";
const genericCreateAccount = async (
userData: {
email: string;
username: string;
image: string;
provider: string;
},
userId: string
): Promise<User> => {
const existingUserWithId = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (existingUserWithId) {
const connectedUser = await prisma.user.update({
where: {
id: userId,
},
data: userData,
});
if (connectedUser.teamId) {
return connectedUser;
}
await prisma.team.create({
data: {
users: {
connect: {
id: connectedUser.id,
},
export const createDemoUser = async (userId: string) => {
await prisma.team.create({
data: {
users: {
create: {
id: userId,
},
},
});
},
});
};
return connectedUser;
export const ensureUser = async ({ userId }: { userId: string }) => {
// Always check if the account userId exists because account could have been deleted
// or we could be in a demo mode with a generated user
const isUserCreated = Boolean(
await prisma.user.findUnique({
where: { id: userId },
})
);
if (isUserCreated) {
return userId;
}
await createDemoUser(userId);
return userId;
};
const genericCreateAccount = async (userData: {
email: string;
username: string;
image: string;
provider: string;
}): Promise<User> => {
const existingUserWithEmail = await prisma.user.findUnique({
where: {
email: userData.email,
@@ -68,18 +63,11 @@ const genericCreateAccount = async (
const newTeam = await prisma.team.create({
data: {
users: {
create: {
id: userId,
...userData,
},
create: userData,
},
},
include: {
users: {
where: {
id: userId,
},
},
users: true,
},
});
@@ -87,8 +75,7 @@ const genericCreateAccount = async (
};
export const createOrLoginWithGithub = async (
profile: GitHubProfile,
userId: string
profile: GitHubProfile
): Promise<User> => {
const userData = {
email: profile._json.email,
@@ -96,13 +83,12 @@ export const createOrLoginWithGithub = async (
image: profile._json.avatar_url,
provider: profile.provider,
};
const newUser = await genericCreateAccount(userData, userId);
const newUser = await genericCreateAccount(userData);
return newUser;
};
export const createOrLoginWithGoogle = async (
profile: GoogleProfile,
userId: string
profile: GoogleProfile
): Promise<User> => {
const userData = {
email: profile._json.email,
@@ -110,11 +96,11 @@ export const createOrLoginWithGoogle = async (
image: profile._json.picture,
provider: profile.provider,
};
const newUser = await genericCreateAccount(userData, userId);
const newUser = await genericCreateAccount(userData);
return newUser;
};
export const createOrLoginWithDev = async (userId: string): Promise<User> => {
export const createOrLoginWithDev = async (): Promise<User> => {
const userData = {
email: "hello@webstudio.is",
username: "admin",
@@ -122,6 +108,6 @@ export const createOrLoginWithDev = async (userId: string): Promise<User> => {
provider: "dev",
};
const newUser = await genericCreateAccount(userData, userId);
const newUser = await genericCreateAccount(userData);
return newUser;
};
@@ -0,0 +1,68 @@
import { useState, useRef, useEffect, RefObject } from "react";
import { createContentController } from "./create-content-controller";
const useContentController = ({
ref,
read,
write,
callback,
}: {
ref: RefObject<HTMLInputElement>;
read: (value: HTMLInputElement) => string;
write: (target: HTMLInputElement, value: string) => void;
callback: (value: { name: string; value: string }) => void;
}) => {
useEffect(() => {
if (ref.current == null) return;
const { disconnectedCallback } = createContentController(ref.current, {
contents: [
{
name: "unit",
match: (value: string) => ["px", "em", "rem", "%"].includes(value),
},
{
name: "number",
match: (value: string) => isNaN(parseFloat(value)) === false,
},
{ name: "unknown", match: (value: string) => Boolean(value) },
],
read,
write,
onMouseMove: callback,
onCaretMove: callback,
});
return () => disconnectedCallback();
}, [ref, read, write, callback]);
};
const ExampleContentController = () => {
const [content, setContent] = useState({ name: "none", value: "" });
const ref = useRef<HTMLInputElement | null>(null);
useContentController({
ref,
callback: (payload) => setContent(payload),
read: (target) => target.value,
write: (target, value) => (target.value = value),
});
return (
<form style={{ fontFamily: "sans-serif" }}>
<fieldset>
<input
defaultValue="100px 20% fit-content calc(0.2px, -99em)"
ref={ref}
style={{ width: "50%" }}
/>
</fieldset>
<fieldset>
<h1>Currently on: {content.name}</h1>
<h1>With a value of: {content.value}</h1>
</fieldset>
</form>
);
};
export const DetectInput = Object.assign(ExampleContentController.bind({}));
export default {
component: ExampleContentController,
};
@@ -0,0 +1,142 @@
/**
* @description
* - detects whether the pointer/keyboard is on any given token as specified by the {contents: []} contract.
* - pointermove/keyup in turn dispatch onMouseMove/onCaretMove with the current state if and when within a configured content token.
* - returns two methods {update, disconnectedCallback} the former of which dispatches a content update(while preserving the caret position if any)
* - read/write methods are called when reading from the passed element or when updating/writing to the same element.
* @example
* const target = document.querySelector('input');
* const {update, disconnectedCallback} = createContentController(target, {
* contents: [
* { name: 'unit', match: (value) => value === 'px' },
* { name: 'number', match: (value) => isNaN(parseFloat(value)) === false },
* { name: 'unknown', match: (value) => true },
* ],
* onMouseMove: ({name, value, position}) => { },
* onCaretMove: ({name, value, position}) => { },
* read: (target) => target.value,
* write: (target, value) => target.value = value,
* });
*/
const canvasContext = globalThis.document
.createElement("canvas")
.getContext("2d") as CanvasRenderingContext2D & { letterSpacing: string };
const measureTextWidth = (value: string): number =>
canvasContext.measureText(value).width;
const measureTextPadding = (node: HTMLElement, value: string) => {
const {
fontSize,
fontFamily,
fontWeight,
textAlign,
letterSpacing,
paddingLeft,
paddingRight,
} = getComputedStyle(node);
if (letterSpacing !== "normal") canvasContext.letterSpacing = letterSpacing;
canvasContext.font = `${fontWeight} ${fontSize} ${fontFamily}`;
return ["left", "start"].includes(textAlign)
? parseFloat(paddingLeft)
: node.clientWidth -
canvasContext.measureText(value).width -
parseFloat(paddingRight);
};
const measureTextMetrics = (value: string, index: number, offset: number) => {
let length = 0;
const DECIMAL_OR_WHITESPACE_OR_NON_WORD = /([-+]?\d*\.?\d+)|([\s\W])/;
return value
.split(DECIMAL_OR_WHITESPACE_OR_NON_WORD)
.filter((value) => value)
.map((value) => {
const width = measureTextWidth(value);
const head = index - offset;
const tail = index - (offset + width);
const props = { head, tail, value, width, length, offset };
length += value.length;
offset += width;
return props;
});
};
export const createContentController = (
targetNode: HTMLInputElement,
{
read = (node: HTMLInputElement) => node.value,
write = (node: HTMLInputElement, value: string) => (node.value = value),
contents = [],
onMouseMove = (value: { name: string; value: string; position: string }) =>
value,
onCaretMove = (value: { name: string; value: string; position: string }) =>
value,
}: {
read: (node: HTMLInputElement) => string;
write: (node: HTMLInputElement, value: string) => void;
contents: {
name: string;
match: (value: string) => boolean;
}[];
onMouseMove: (value: {
name: string;
value: string;
position: string;
}) => void;
onCaretMove: (value: {
name: string;
value: string;
position: string;
}) => void;
}
): {
update: (value: string) => void;
disconnectedCallback: () => void;
} => {
const eventNames = ["keyup", "pointermove"] as const;
const handleEvent = (event: KeyboardEvent | PointerEvent): void => {
let { offsetX } = event as PointerEvent;
const { type } = event;
const targetValue = read(targetNode);
const targetPadding = measureTextPadding(targetNode, targetValue);
if (type === eventNames[0])
offsetX =
targetPadding +
measureTextWidth(
targetValue.slice(0, targetNode.selectionStart || undefined)
);
const targetMetrics = measureTextMetrics(
targetValue,
offsetX,
targetPadding
);
for (const targetMetricsData of targetMetrics) {
const { head, tail, value } = targetMetricsData;
// zero for exact boundary match, added buffer for precision forgiveness, more so from the head
if (head >= -2 && tail <= 0) {
const current = contents.find(({ match }) => match(value));
if (current) {
const { name } = current;
const position = `${head},${tail}`;
if (type === eventNames[0]) {
onCaretMove({ name, value, position });
} else if (type === eventNames[1]) {
onMouseMove({ name, value, position });
}
}
}
}
};
eventNames.forEach((eventName) =>
targetNode.addEventListener(eventName, handleEvent, false)
);
return {
update: (value) => {
const { selectionStart, selectionEnd } = targetNode;
write(targetNode, value);
targetNode.setSelectionRange(selectionStart, selectionEnd);
},
disconnectedCallback: () => {
eventNames.forEach((eventName) =>
targetNode.removeEventListener(eventName, handleEvent, false)
);
},
};
};