mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
Merge pull request #277 from webstudio-is/project-package
Project package
This commit is contained in:
71 files changed
+1781
-1257
No files matched your search
@@ -1,30 +1,30 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import store from "immerhin";
|
||||
import * as db from "~/shared/db";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
import {
|
||||
type OnChangeChildren,
|
||||
createElementsTree,
|
||||
type Data,
|
||||
globalStyles,
|
||||
type OnChangeChildren,
|
||||
type Tree,
|
||||
useAllUserProps,
|
||||
globalStyles,
|
||||
createElementsTree,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import { useShortcuts } from "./shared/use-shortcuts";
|
||||
import {
|
||||
usePopulateRootInstance,
|
||||
useInsertInstance,
|
||||
useDeleteInstance,
|
||||
useReparentInstance,
|
||||
usePublishSelectedInstanceData,
|
||||
usePublishRootInstance,
|
||||
useUpdateSelectedInstance,
|
||||
usePublishSelectedInstanceDataRect,
|
||||
usePublishHoveredInstanceRect,
|
||||
useInsertInstance,
|
||||
usePopulateRootInstance,
|
||||
usePublishHoveredInstanceData,
|
||||
usePublishHoveredInstanceRect,
|
||||
usePublishRootInstance,
|
||||
usePublishSelectedInstanceData,
|
||||
usePublishSelectedInstanceDataRect,
|
||||
usePublishTextEditingInstanceId,
|
||||
useReparentInstance,
|
||||
useSetHoveredInstance,
|
||||
useUnselectInstance,
|
||||
usePublishTextEditingInstanceId,
|
||||
useUpdateSelectedInstance,
|
||||
} from "./shared/instance";
|
||||
import { useUpdateStyle } from "./shared/style";
|
||||
import { useTrackSelectedElement } from "./shared/use-track-selected-element";
|
||||
@@ -83,7 +83,7 @@ const useSubscribePreviewMode = () => {
|
||||
|
||||
type DesignModeProps = {
|
||||
treeId: Tree["id"];
|
||||
project: db.project.Project;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
const DesignMode = ({ treeId, project }: DesignModeProps) => {
|
||||
@@ -112,7 +112,7 @@ const DesignMode = ({ treeId, project }: DesignModeProps) => {
|
||||
};
|
||||
|
||||
type CanvasProps = {
|
||||
data: Data & { project: db.project.Project };
|
||||
data: Data & { project: Project };
|
||||
};
|
||||
|
||||
export const Canvas = ({ data }: CanvasProps): JSX.Element | null => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import useInterval from "react-use/lib/useInterval";
|
||||
import { sync } from "immerhin";
|
||||
import { enqueue } from "./queue";
|
||||
import * as db from "~/shared/db";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
|
||||
export const useSync = ({ project }: { project: db.project.Project }) => {
|
||||
export const useSync = ({ project }: { project: Project }) => {
|
||||
useInterval(() => {
|
||||
const entries = sync();
|
||||
if (entries.length === 0) return;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSubscribe, usePublish, type Publish } from "~/shared/pubsub";
|
||||
import * as db from "~/shared/db";
|
||||
import { type Publish, usePublish, useSubscribe } from "~/shared/pubsub";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
import type { Config } from "~/config";
|
||||
import { Box, Flex, Grid, type CSS } from "@webstudio-is/design-system";
|
||||
import { Box, type CSS, Flex, Grid } from "@webstudio-is/design-system";
|
||||
import interStyles from "~/shared/font-faces/inter.css";
|
||||
import { SidebarLeft } from "./features/sidebar-left";
|
||||
import { Inspector } from "./features/inspector";
|
||||
@@ -16,19 +16,19 @@ import designerStyles from "./designer.css";
|
||||
import { Footer } from "./features/footer";
|
||||
import { TreePrevew } from "./features/tree-preview";
|
||||
import {
|
||||
useUpdateCanvasWidth,
|
||||
useSubscribeBreakpoints,
|
||||
useUpdateCanvasWidth,
|
||||
} from "./features/breakpoints";
|
||||
import {
|
||||
CanvasIframe,
|
||||
useReadCanvasRect,
|
||||
Workspace,
|
||||
CanvasIframe,
|
||||
} from "./features/workspace";
|
||||
import { usePublishShortcuts } from "./shared/shortcuts";
|
||||
import {
|
||||
useDragAndDropState,
|
||||
useIsPreviewMode,
|
||||
useRootInstance,
|
||||
useDragAndDropState,
|
||||
} from "~/shared/nano-states";
|
||||
import { useClientSettings } from "./shared/client-settings";
|
||||
import { Navigator } from "./features/sidebar-left";
|
||||
@@ -211,7 +211,7 @@ const NavigatorPanel = ({ publish, isPreviewMode }: NavigatorPanelProps) => {
|
||||
|
||||
type DesignerProps = {
|
||||
config: Config;
|
||||
project: db.project.Project;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
export const Designer = ({ config, project }: DesignerProps) => {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { type Breakpoint } from "@webstudio-is/react-sdk";
|
||||
import { useSubscribe, type Publish } from "~/shared/pubsub";
|
||||
import { type Publish, useSubscribe } from "~/shared/pubsub";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuArrow,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuCheckboxItem,
|
||||
__DEPRECATED__Text,
|
||||
DropdownMenu,
|
||||
DropdownMenuArrow,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Flex,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { sort } from "~/shared/breakpoints";
|
||||
import { useSelectedBreakpoint } from "../../shared/nano-states";
|
||||
import { BreakpointsEditor } from "./breakpoints-editor";
|
||||
import { Preview } from "./preview";
|
||||
@@ -21,11 +20,12 @@ import { ZoomSetting } from "./zoom-setting";
|
||||
import { TriggerButton } from "./trigger-button";
|
||||
import { WidthSetting } from "./width-setting";
|
||||
import {
|
||||
useSubscribeZoomFromShortcut,
|
||||
useSubscribeSelectBreakpointFromShortcut,
|
||||
useSubscribeZoomFromShortcut,
|
||||
} from "./use-subscribe-shortcuts";
|
||||
import { ConfirmationDialog } from "./confirmation-dialog";
|
||||
import { useBreakpoints } from "~/shared/nano-states";
|
||||
import { utils } from "@webstudio-is/project";
|
||||
|
||||
declare module "~/shared/pubsub" {
|
||||
export interface PubsubMap {
|
||||
@@ -105,7 +105,7 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
|
||||
nextBreakpoints.splice(index, 1);
|
||||
setBreakpoints(nextBreakpoints);
|
||||
if (breakpointToDelete === selectedBreakpoint) {
|
||||
setSelectedBreakpoint(sort(nextBreakpoints)[0]);
|
||||
setSelectedBreakpoint(utils.breakpoints.sort(nextBreakpoints)[0]);
|
||||
}
|
||||
publish({
|
||||
type: "breakpointDelete",
|
||||
@@ -163,7 +163,7 @@ export const Breakpoints = ({ publish }: BreakpointsProps) => {
|
||||
)}
|
||||
{view === "selector" && (
|
||||
<>
|
||||
{sort(breakpoints).map((breakpoint) => {
|
||||
{utils.breakpoints.sort(breakpoints).map((breakpoint) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={breakpoint === selectedBreakpoint}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import { useSelectedBreakpoint } from "~/designer/shared/nano-states";
|
||||
import { sort } from "~/shared/breakpoints";
|
||||
import { useBreakpoints } from "~/shared/nano-states";
|
||||
|
||||
import { utils } from "@webstudio-is/project";
|
||||
export const useSubscribeBreakpoints = () => {
|
||||
const [breakpoints, setBreakpoints] = useBreakpoints();
|
||||
const [selectedBreakpoint, setSelectedBreakpoint] = useSelectedBreakpoint();
|
||||
@@ -13,7 +12,7 @@ export const useSubscribeBreakpoints = () => {
|
||||
useEffect(() => {
|
||||
// Set the initial selected breakpoint
|
||||
if (selectedBreakpoint === undefined && breakpoints.length !== 0) {
|
||||
setSelectedBreakpoint(sort(breakpoints)[0]);
|
||||
setSelectedBreakpoint(utils.breakpoints.sort(breakpoints)[0]);
|
||||
}
|
||||
|
||||
// Breakpoints must have changed, lets update the selected breakpoint
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useSubscribe } from "~/shared/pubsub";
|
||||
import { sort } from "~/shared/breakpoints";
|
||||
import { useBreakpoints } from "~/shared/nano-states";
|
||||
import { useZoom, useSelectedBreakpoint } from "../../shared/nano-states";
|
||||
import { minZoom } from "./zoom-setting";
|
||||
import { utils } from "@webstudio-is/project";
|
||||
|
||||
export const useSubscribeSelectBreakpointFromShortcut = () => {
|
||||
const [breakpoints] = useBreakpoints();
|
||||
const [, setSelectedBreakpoint] = useSelectedBreakpoint();
|
||||
useSubscribe("selectBreakpointFromShortcut", (index) => {
|
||||
const breakpoint = sort(breakpoints)[index - 1];
|
||||
const breakpoint = utils.breakpoints.sort(breakpoints)[index - 1];
|
||||
if (breakpoint) setSelectedBreakpoint(breakpoint);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
useCanvasWidth,
|
||||
useSelectedBreakpoint,
|
||||
} from "~/designer/shared/nano-states";
|
||||
import { Slider, __DEPRECATED__Text, Flex } from "@webstudio-is/design-system";
|
||||
import { sort } from "~/shared/breakpoints";
|
||||
import { __DEPRECATED__Text, Flex, Slider } from "@webstudio-is/design-system";
|
||||
import { useBreakpoints, useIsPreviewMode } from "~/shared/nano-states";
|
||||
import { utils } from "@webstudio-is/project";
|
||||
|
||||
// Doesn't make sense to allow resizing the canvas lower/higher than this.
|
||||
export const minWidth = 50;
|
||||
@@ -18,7 +18,10 @@ const useNextBreakpoint = () => {
|
||||
const [selectedBreakpoint] = useSelectedBreakpoint();
|
||||
const [breakpoints] = useBreakpoints();
|
||||
|
||||
const sortedBreakpoints = useMemo(() => sort(breakpoints), [breakpoints]);
|
||||
const sortedBreakpoints = useMemo(
|
||||
() => utils.breakpoints.sort(breakpoints),
|
||||
[breakpoints]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (selectedBreakpoint === undefined) return;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { RocketIcon, ExternalLinkIcon } from "@webstudio-is/icons";
|
||||
import { ExternalLinkIcon, RocketIcon } from "@webstudio-is/icons";
|
||||
import {
|
||||
__DEPRECATED__Text,
|
||||
Button,
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
PopoverPortal,
|
||||
TextField,
|
||||
Text,
|
||||
__DEPRECATED__Text,
|
||||
Label,
|
||||
Link,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
TextField,
|
||||
useId,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { useIsPublishDialogOpen } from "../../shared/nano-states";
|
||||
import env from "~/shared/env";
|
||||
import * as db from "~/shared/db";
|
||||
type PublishButtonProps = { project: db.project.Project };
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
type PublishButtonProps = { project: Project };
|
||||
|
||||
const getHost = () => {
|
||||
if (env.PUBLISHER_ENDPOINT && env.PUBLISHER_HOST) {
|
||||
@@ -60,7 +60,7 @@ const Content = ({ project }: PublishButtonProps) => {
|
||||
gap: "$0",
|
||||
}}
|
||||
>
|
||||
<Text truncate>{`${domain}.${getHost()}`} </Text>
|
||||
<Text truncate>{`${domain}.${getHost()}`}</Text>
|
||||
<ExternalLinkIcon />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -4,15 +4,15 @@ import {
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
TextField,
|
||||
} from "@webstudio-is/design-system";
|
||||
import { Share1Icon } from "@webstudio-is/icons";
|
||||
import * as db from "~/shared/db";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
import { useIsShareDialogOpen } from "../../shared/nano-states";
|
||||
|
||||
type ShareButtonProps = { path: string; project: db.project.Project };
|
||||
type ShareButtonProps = { path: string; project: Project };
|
||||
|
||||
const Content = ({ path, project }: ShareButtonProps) => {
|
||||
if (typeof location === "undefined") {
|
||||
|
||||
@@ -7,12 +7,12 @@ import { PublishButton } from "./publish";
|
||||
import { SyncStatus } from "./sync-status";
|
||||
import { Menu } from "./menu";
|
||||
import { Breakpoints } from "../breakpoints";
|
||||
import * as db from "~/shared/db";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
|
||||
type TopbarProps = {
|
||||
config: Config;
|
||||
css: CSS;
|
||||
project: db.project.Project;
|
||||
project: Project;
|
||||
publish: Publish;
|
||||
};
|
||||
export const Topbar = ({ config, css, project, publish }: TopbarProps) => {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import {
|
||||
redirect,
|
||||
type LoaderFunction,
|
||||
type ActionFunction,
|
||||
json,
|
||||
type LoaderFunction,
|
||||
redirect,
|
||||
} from "@remix-run/node";
|
||||
import { Dashboard, links } from "~/dashboard";
|
||||
import * as db from "~/shared/db";
|
||||
import { db } from "@webstudio-is/project";
|
||||
import config, { type Config } from "~/config";
|
||||
import { ensureUserCookie } from "~/shared/session";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { zfd } from "zod-form-data";
|
||||
import { User, type Project } from "@webstudio-is/prisma-client";
|
||||
import { type Project, User } from "@webstudio-is/prisma-client";
|
||||
|
||||
export { links };
|
||||
const schema = zfd.formData({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { LoaderFunction } from "@remix-run/node";
|
||||
import { Designer, links } from "~/designer";
|
||||
import * as db from "~/shared/db";
|
||||
import { db } from "@webstudio-is/project";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
import config from "~/config";
|
||||
import env from "~/env.server";
|
||||
import { ErrorMessage } from "~/shared/error";
|
||||
import { action, useAction } from "./_assets";
|
||||
|
||||
export { links, action };
|
||||
export { action, links };
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
if (params.id === undefined) throw new Error("Project id undefined");
|
||||
@@ -20,7 +21,7 @@ export const loader: LoaderFunction = async ({ params }) => {
|
||||
|
||||
type Data = {
|
||||
config: typeof config;
|
||||
project: db.project.Project;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
type Error = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { redirect, type LoaderFunction } from "@remix-run/node";
|
||||
import { type LoaderFunction, redirect } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { Root, type Data } from "@webstudio-is/react-sdk";
|
||||
import { type Data, Root } from "@webstudio-is/react-sdk";
|
||||
import config from "~/config";
|
||||
import * as db from "~/shared/db";
|
||||
import { db } from "@webstudio-is/project";
|
||||
import { Canvas as CanvasDocument } from "~/shared/documents/canvas";
|
||||
import env, { Env } from "~/env.server";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import type { Breakpoint } from "@webstudio-is/react-sdk";
|
||||
import * as db from "~/shared/db";
|
||||
import { Breakpoint } from "@webstudio-is/react-sdk";
|
||||
import { db } from "@webstudio-is/project";
|
||||
|
||||
export type ErrorData = {
|
||||
errors: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import type { Breakpoint } from "@webstudio-is/react-sdk";
|
||||
import * as db from "~/shared/db";
|
||||
import { db } from "@webstudio-is/project";
|
||||
|
||||
export type ErrorData = {
|
||||
errors: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ActionFunction } from "@remix-run/node";
|
||||
import * as db from "~/shared/db";
|
||||
import { db } from "@webstudio-is/project";
|
||||
import { type SyncItem } from "immerhin";
|
||||
import { type Tree } from "@webstudio-is/react-sdk";
|
||||
import { type Project } from "@webstudio-is/prisma-client";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type LoaderFunction, redirect } from "@remix-run/node";
|
||||
import type { User } from "@webstudio-is/prisma-client";
|
||||
import * as db from "~/shared/db";
|
||||
import { db as projectDb, type Project } from "@webstudio-is/project";
|
||||
import * as userDb from "~/shared/db";
|
||||
import { ensureUserCookie } from "~/shared/session";
|
||||
import config from "~/config";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
@@ -11,11 +12,11 @@ const ensureProject = async ({
|
||||
}: {
|
||||
userId: User["id"];
|
||||
domain: string;
|
||||
}): Promise<db.project.Project> => {
|
||||
const projects = await db.project.loadManyByUserId(userId);
|
||||
}): Promise<Project> => {
|
||||
const projects = await projectDb.project.loadManyByUserId(userId);
|
||||
if (projects.length !== 0) return projects[0];
|
||||
|
||||
return await db.project.clone(domain, userId);
|
||||
return await projectDb.project.clone(domain, userId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,7 @@ export const loader: LoaderFunction = async ({ request, params }) => {
|
||||
const user = await authenticator.isAuthenticated(request);
|
||||
const { headers, userId: generatedUserId } = await ensureUserCookie(request);
|
||||
try {
|
||||
const userId = await db.user.ensureUser({
|
||||
const userId = await userDb.user.ensureUser({
|
||||
userId: user ? user.id : generatedUserId,
|
||||
});
|
||||
const project = await ensureProject({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type LoaderFunction } from "@remix-run/node";
|
||||
import { type InstanceProps } from "@webstudio-is/react-sdk";
|
||||
import * as db from "~/shared/db";
|
||||
|
||||
import { db } from "@webstudio-is/project";
|
||||
type ErrorData = {
|
||||
errors: string;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type LoaderFunction } from "@remix-run/node";
|
||||
import type { Tree } from "@webstudio-is/react-sdk";
|
||||
import * as db from "~/shared/db";
|
||||
|
||||
import { db } from "@webstudio-is/project";
|
||||
export type ErrorData = {
|
||||
errors: string;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type Data } from "@webstudio-is/react-sdk";
|
||||
import * as db from ".";
|
||||
import { Project } from "./project.server";
|
||||
|
||||
import { db } from "@webstudio-is/project";
|
||||
import type { Project } from "@webstudio-is/project";
|
||||
export type CanvasData = Data & { project: Project };
|
||||
|
||||
export type ErrorData = {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
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";
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as db from "./";
|
||||
import { db } from "@webstudio-is/project";
|
||||
|
||||
/**
|
||||
* Conceptually publishing is cloning all data that affects user site
|
||||
* and referencing it under a new tree id.
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@webstudio-is/prisma-client": "*",
|
||||
"@webstudio-is/react-sdk": "*",
|
||||
"@webstudio-is/css-vars": "*",
|
||||
"@webstudio-is/project": "*",
|
||||
"bson-objectid": "^2.0.2",
|
||||
"camelcase": "^6.3.0",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -28,4 +28,3 @@ export const loadProject = async ({
|
||||
breakpoints,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Webstudio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Webstudio Project
|
||||
|
||||
Project related functionalities.
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./src/index";
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("@webstudio-is/jest-config");
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@webstudio-is/project",
|
||||
"version": "0.0.1",
|
||||
"description": "Webstudio Project",
|
||||
"author": "Webstudio <github@webstudio.is>",
|
||||
"homepage": "https://webstudio.is",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"checks": "yarn typecheck && yarn lint && yarn test",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --watch --out-dir=lib",
|
||||
"build": "rm -fr lib && tsc",
|
||||
"lint": "eslint ./src --ext .ts,.tsx --max-warnings 0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@webstudio-is/asset-uploader": "*",
|
||||
"@webstudio-is/prisma-client": "*",
|
||||
"@webstudio-is/react-sdk": "*",
|
||||
"nanoid": "^4.0.0",
|
||||
"slugify": "^1.6.5",
|
||||
"zod": "^3.19.1",
|
||||
"tsup": "^6.1.3",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/*",
|
||||
"README.md",
|
||||
"!*.test.*"
|
||||
],
|
||||
"license": "MIT",
|
||||
"private": false
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,5 @@
|
||||
import * as project from "./project";
|
||||
import * as tree from "./tree";
|
||||
import * as props from "./props";
|
||||
import * as breakpoints from "./breakpoints";
|
||||
export { project, tree, breakpoints, props };
|
||||
@@ -7,17 +7,11 @@ import {
|
||||
Prisma,
|
||||
Project as BaseProject,
|
||||
} from "@webstudio-is/prisma-client";
|
||||
import * as db from ".";
|
||||
import { formatAsset } from "@webstudio-is/asset-uploader";
|
||||
import type { Asset } from "@webstudio-is/asset-uploader";
|
||||
|
||||
import * as db from "./index";
|
||||
import type { Project } from "../index";
|
||||
const TreeHistorySchema = z.array(z.string());
|
||||
|
||||
export type Project = Omit<BaseProject, "prodTreeIdHistory" | "assets"> & {
|
||||
prodTreeIdHistory: z.infer<typeof TreeHistorySchema>;
|
||||
assets?: Array<Asset>;
|
||||
};
|
||||
|
||||
const parseProject = (project: BaseProject): Project => {
|
||||
const prodTreeIdHistory = JSON.parse(project.prodTreeIdHistory);
|
||||
return {
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@webstudio-is/react-sdk";
|
||||
import { applyPatches, type Patch } from "immer";
|
||||
import { prisma } from "@webstudio-is/prisma-client";
|
||||
import { Project } from "./project.server";
|
||||
import type { Project } from "./types";
|
||||
|
||||
export const loadByProject = async (
|
||||
project: Project | null,
|
||||
@@ -6,19 +6,18 @@ import {
|
||||
} from "@webstudio-is/react-sdk";
|
||||
import { applyPatches, type Patch } from "immer";
|
||||
import { prisma } from "@webstudio-is/prisma-client";
|
||||
import { createInstance, populateInstance } from "~/shared/tree-utils";
|
||||
import { sort } from "~/shared/breakpoints";
|
||||
import { Tree as DbTree } from "@prisma/client";
|
||||
import { Project } from "./project.server";
|
||||
import type { Project } from "../index";
|
||||
import { utils } from "../index";
|
||||
|
||||
export const createRootInstance = (breakpoints: Array<Breakpoint>) => {
|
||||
// Take the smallest breakpoint as default
|
||||
const defaultBreakpoint = sort(breakpoints)[0];
|
||||
const defaultBreakpoint = utils.breakpoints.sort(breakpoints)[0];
|
||||
if (defaultBreakpoint === undefined) {
|
||||
throw new Error("A breakpoint with minWidth 0 is required");
|
||||
}
|
||||
const instance = createInstance({ component: "Body" });
|
||||
return populateInstance(instance, defaultBreakpoint.id);
|
||||
const instance = utils.tree.createInstance({ component: "Body" });
|
||||
return utils.tree.populateInstance(instance, defaultBreakpoint.id);
|
||||
};
|
||||
|
||||
export const create = async (root: Instance): Promise<DbTree> => {
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { Project as BaseProject } from "@webstudio-is/prisma-client";
|
||||
import type { Asset } from "@webstudio-is/asset-uploader";
|
||||
const TreeHistorySchema = z.array(z.string());
|
||||
|
||||
export type Project = Omit<BaseProject, "prodTreeIdHistory" | "assets"> & {
|
||||
prodTreeIdHistory: z.infer<typeof TreeHistorySchema>;
|
||||
assets?: Array<Asset>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as dbFunctions from "./db";
|
||||
import * as breakpointsUtils from "./shared/breakpoints";
|
||||
import * as treeUtils from "./shared/tree-utils";
|
||||
|
||||
export const utils = { breakpoints: breakpointsUtils, tree: treeUtils };
|
||||
export const db = dbFunctions;
|
||||
|
||||
export * from "./db/types";
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1 @@
|
||||
export * from "./instance-data";
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
type Instance,
|
||||
type InstanceProps,
|
||||
type CssRule,
|
||||
type Style,
|
||||
type StyleProperty,
|
||||
type StyleValue,
|
||||
type Breakpoint,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
|
||||
export type SelectedInstanceData = {
|
||||
id: Instance["id"];
|
||||
component: Instance["component"];
|
||||
cssRules: Array<CssRule>;
|
||||
browserStyle: Style;
|
||||
props: InstanceProps;
|
||||
};
|
||||
|
||||
export type HoveredInstanceData = {
|
||||
id: Instance["id"];
|
||||
component: Instance["component"];
|
||||
};
|
||||
|
||||
type StyleUpdate = {
|
||||
property: StyleProperty;
|
||||
value: StyleValue;
|
||||
};
|
||||
|
||||
export type StyleUpdates = {
|
||||
id: Instance["id"];
|
||||
updates: Array<StyleUpdate>;
|
||||
breakpoint: Breakpoint;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete instance div 1 > div 2 > [div 3] 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Delete instance div 1 > div 2, [div 3] 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,90 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Insert instance after a nested div using "end": div 1 > div 2, [div 3] 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Insert instance after a nested div using index: div 1 > div 2, [div 3] 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Insert instance before a nested div using index: div 1 > [div 3], div 2 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Insert instance inside a nested div using "end": div 1 > div 2 > [div 3] 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,58 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Reparent instance new parent 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "4",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Reparent instance same parent different position 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "3",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "4",
|
||||
},
|
||||
],
|
||||
"component": "Box",
|
||||
"cssRules": Array [],
|
||||
"id": "1",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { cloneInstance } from "./clone-instance";
|
||||
|
||||
const getIds = (instance: Instance, ids: Array<Instance["id"]> = []) => {
|
||||
ids.push(instance.id);
|
||||
for (const child of instance.children) {
|
||||
if (typeof child !== "string") getIds(child);
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
describe("Clone instance", () => {
|
||||
test("ensure new ids", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ids = getIds(instance);
|
||||
const clone = cloneInstance(instance);
|
||||
const clonedIds = getIds(clone);
|
||||
ids.forEach((id, index) => {
|
||||
expect(id).not.toBe(clonedIds[index]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import ObjectId from "bson-objectid";
|
||||
import produce from "immer";
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
const updateIds = (instance: Instance) => {
|
||||
instance.id = ObjectId().toString();
|
||||
for (const child of instance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
updateIds(child);
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneInstance = (instance: Instance): Instance =>
|
||||
produce(updateIds)(instance);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createInstance } from "./create-instance";
|
||||
|
||||
describe("create instance", () => {
|
||||
test("instance without id should be auto generated", () => {
|
||||
const createdInstance = createInstance({
|
||||
component: "Box",
|
||||
});
|
||||
|
||||
expect(typeof createdInstance.id).toBe("string");
|
||||
});
|
||||
|
||||
test("instance without children should be empty by default", () => {
|
||||
const createdInstance = createInstance({
|
||||
component: "Box",
|
||||
});
|
||||
expect(createdInstance.children.length).toBe(0);
|
||||
});
|
||||
|
||||
test("instance without style should be an object after creation", () => {
|
||||
const createdInstance = createInstance({
|
||||
component: "Box",
|
||||
});
|
||||
expect(Array.isArray(createdInstance.cssRules)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("instance with children ", () => {
|
||||
const createdInstance = createInstance({
|
||||
component: "Box",
|
||||
children: [
|
||||
{
|
||||
id: "1",
|
||||
component: "Box",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
cssRules: [],
|
||||
});
|
||||
expect(createdInstance.children).toEqual([
|
||||
{
|
||||
id: "1",
|
||||
component: "Box",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import ObjectId from "bson-objectid";
|
||||
import {
|
||||
type CssRule,
|
||||
type Instance,
|
||||
components,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
|
||||
export const createInstance = ({
|
||||
component,
|
||||
id,
|
||||
children,
|
||||
cssRules,
|
||||
}: {
|
||||
component: Instance["component"];
|
||||
id?: Instance["id"];
|
||||
children?: Instance["children"];
|
||||
cssRules?: Array<CssRule>;
|
||||
}): Instance => {
|
||||
const componentMeta = components[component];
|
||||
return {
|
||||
component,
|
||||
id: id === undefined ? ObjectId().toString() : id,
|
||||
cssRules: cssRules ?? [],
|
||||
children: children === undefined ? componentMeta.children ?? [] : children,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { deleteInstanceMutable } from "./delete-instance";
|
||||
|
||||
describe("Delete instance", () => {
|
||||
test("div 1 > div 2 > [div 3]", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
deleteInstanceMutable(instance, "3");
|
||||
expect(instance).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("div 1 > div 2, [div 3]", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
deleteInstanceMutable(instance, "3");
|
||||
expect(instance).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
export const deleteInstanceMutable = (
|
||||
instance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
): boolean => {
|
||||
if (instance.id === instanceId) return true;
|
||||
|
||||
for (const child of instance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
const shouldDelete = deleteInstanceMutable(child, instanceId);
|
||||
if (shouldDelete === true) {
|
||||
const index = instance.children.indexOf(child);
|
||||
instance.children.splice(index, 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createInstance } from "./create-instance";
|
||||
import { findClosestNonInlineParent } from "./find-closest-non-inline-parent";
|
||||
|
||||
describe("findClosestNonInlineParent", () => {
|
||||
const italic = createInstance({ component: "Italic" });
|
||||
|
||||
const boldWithItalic = createInstance({
|
||||
component: "Bold",
|
||||
children: [italic],
|
||||
});
|
||||
|
||||
test("when there's no non-inline parent, returns undefined", () => {
|
||||
expect(
|
||||
findClosestNonInlineParent(boldWithItalic, italic.id)
|
||||
).toBeUndefined();
|
||||
expect(findClosestNonInlineParent(italic, italic.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("when the given instance isn't in the tree, returns undefined", () => {
|
||||
const root = createInstance({ component: "Body" });
|
||||
expect(findClosestNonInlineParent(root, italic.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("retrunrns the first non-inline parent", () => {
|
||||
const root = createInstance({
|
||||
component: "Body",
|
||||
children: [
|
||||
createInstance({
|
||||
component: "Heading",
|
||||
id: "the-heading",
|
||||
children: [italic],
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(findClosestNonInlineParent(root, italic.id)?.id).toBe("the-heading");
|
||||
});
|
||||
|
||||
test("retrunrns the first non-inline parent (nested)", () => {
|
||||
const root = createInstance({
|
||||
component: "Body",
|
||||
children: [
|
||||
createInstance({
|
||||
component: "Heading",
|
||||
id: "the-heading",
|
||||
children: [boldWithItalic],
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(findClosestNonInlineParent(root, italic.id)?.id).toBe("the-heading");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Instance, components } from "@webstudio-is/react-sdk";
|
||||
import { getInstancePath } from "./get-instance-path";
|
||||
|
||||
export const findClosestNonInlineParent = (
|
||||
rootInstance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
): Instance | undefined => {
|
||||
const path = getInstancePath(rootInstance, instanceId);
|
||||
path.reverse();
|
||||
return path.find((item) => components[item.component].isInlineOnly === false);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { findClosestSiblingInstance } from "./find-closest-sibling-instance";
|
||||
|
||||
describe("Find closest sibling instance", () => {
|
||||
test("find", () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
"a",
|
||||
{
|
||||
component: "Box",
|
||||
id: "4",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
"b",
|
||||
{
|
||||
component: "Box",
|
||||
id: "5",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
findClosestSiblingInstance(rootInstance, "notFound")
|
||||
).toBeUndefined();
|
||||
expect(findClosestSiblingInstance(rootInstance, "1")).toBeUndefined();
|
||||
expect(findClosestSiblingInstance(rootInstance, "3")?.id).toBe("4");
|
||||
expect(findClosestSiblingInstance(rootInstance, "4")?.id).toBe("5");
|
||||
expect(findClosestSiblingInstance(rootInstance, "5")?.id).toBe("4");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
export const findClosestSiblingInstance = (
|
||||
instance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
): Instance | undefined => {
|
||||
if (instance.children.length === 0) return;
|
||||
const children = instance.children.filter(
|
||||
(instance) => typeof instance === "object"
|
||||
) as Array<Instance>;
|
||||
const index = children.findIndex((instance) => instance.id === instanceId);
|
||||
if (index === -1) return;
|
||||
const nextInstance = children[index + 1];
|
||||
if (nextInstance !== undefined) return nextInstance;
|
||||
const previousInstance = children[index - 1];
|
||||
return previousInstance;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
// @todo should be optimized because it will be a frequent operation on a potentially large tree.
|
||||
export const findInstanceById = (
|
||||
instance: Instance,
|
||||
id: Instance["id"]
|
||||
): Instance | undefined => {
|
||||
if (instance.id === id) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (instance.children !== undefined) {
|
||||
for (const child of instance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
const foundInstance = findInstanceById(child, id);
|
||||
if (foundInstance !== undefined) return foundInstance;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { findParentInstance } from "./find-parent-instance";
|
||||
|
||||
describe("Find parent instance", () => {
|
||||
test("find", () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(findParentInstance(rootInstance, "3")?.id).toBe("2");
|
||||
expect(findParentInstance(rootInstance, "2")?.id).toBe("1");
|
||||
expect(findParentInstance(rootInstance, "1")).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
export const findParentInstance = (
|
||||
instance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
): Instance | undefined => {
|
||||
const find = (
|
||||
childInstance: Instance,
|
||||
parentInstance?: Instance
|
||||
): Instance | undefined => {
|
||||
if (childInstance.id === instanceId) return parentInstance;
|
||||
for (const child of childInstance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
const foundInstance = find(child, childInstance);
|
||||
if (foundInstance !== undefined) return foundInstance;
|
||||
}
|
||||
};
|
||||
return find(instance);
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import {
|
||||
getInstancePath,
|
||||
getInstancePathWithPositions,
|
||||
} from "./get-instance-path";
|
||||
|
||||
const getIds = (array: Array<{ id: string }>) => array.map((item) => item.id);
|
||||
|
||||
describe("Get instance path", () => {
|
||||
test("single possible path", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const path = getInstancePath(instance, "3");
|
||||
expect(getIds(path)).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
test("two possible paths", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2a",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3a",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "2b",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3b",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const path1 = getInstancePath(instance, "3b");
|
||||
expect(getIds(path1)).toEqual(["1", "2b", "3b"]);
|
||||
const path2 = getInstancePath(instance, "3a");
|
||||
expect(getIds(path2)).toEqual(["1", "2a", "3a"]);
|
||||
});
|
||||
});
|
||||
|
||||
const getIdAndPosition = (
|
||||
array: Array<{ item: { id: string }; position: number }>
|
||||
) => array.map((item) => [item.item.id, item.position]);
|
||||
|
||||
describe("Get instance path with positions", () => {
|
||||
test("single possible path", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const path = getInstancePathWithPositions(instance, "3");
|
||||
expect(getIdAndPosition(path)).toEqual([
|
||||
["1", 0],
|
||||
["2", 0],
|
||||
["3", 0],
|
||||
]);
|
||||
});
|
||||
|
||||
test("two possible paths", () => {
|
||||
const instance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2a",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3a",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "2b",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "3b",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const path1 = getInstancePathWithPositions(instance, "3b");
|
||||
expect(getIdAndPosition(path1)).toEqual([
|
||||
["1", 0],
|
||||
["2b", 1],
|
||||
["3b", 0],
|
||||
]);
|
||||
|
||||
const path2 = getInstancePathWithPositions(instance, "3a");
|
||||
expect(getIdAndPosition(path2)).toEqual([
|
||||
["1", 0],
|
||||
["2a", 0],
|
||||
["3a", 0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
export const getInstancePath = (
|
||||
instance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
) => {
|
||||
const path = [];
|
||||
|
||||
const find = (instance: Instance) => {
|
||||
if (instance.id === instanceId) return true;
|
||||
for (const child of instance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
const found = find(child);
|
||||
if (found) {
|
||||
path.push(child);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (find(instance)) {
|
||||
path.push(instance);
|
||||
}
|
||||
|
||||
return path.reverse();
|
||||
};
|
||||
|
||||
export const getInstancePathWithPositions = (
|
||||
instance: Instance,
|
||||
instanceId: Instance["id"]
|
||||
) => {
|
||||
const path = [];
|
||||
|
||||
const find = (instance: Instance) => {
|
||||
if (instance.id === instanceId) return true;
|
||||
for (let index = 0; index < instance.children.length; index++) {
|
||||
const child = instance.children[index];
|
||||
if (typeof child === "string") continue;
|
||||
const found = find(child);
|
||||
if (found) {
|
||||
path.push({ item: child, position: index });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (find(instance)) {
|
||||
path.push({ item: instance, position: 0 });
|
||||
}
|
||||
|
||||
return path.reverse();
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from "./create-instance";
|
||||
export * from "./delete-instance";
|
||||
export * from "./insert-instance";
|
||||
export * from "./populate";
|
||||
export * from "./find-instance";
|
||||
export * from "./set-instance-style";
|
||||
export * from "./set-instance-children";
|
||||
export * from "./get-instance-path";
|
||||
export * from "./find-closest-sibling-instance";
|
||||
export * from "./find-parent-instance";
|
||||
export * from "./clone-instance";
|
||||
export * from "./reparent-instance";
|
||||
export * from "./find-closest-non-inline-parent";
|
||||
@@ -0,0 +1,98 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { insertInstanceMutable } from "./insert-instance";
|
||||
import { createInstance } from "./create-instance";
|
||||
|
||||
const instance = createInstance({ id: "3", component: "Box" });
|
||||
|
||||
describe("Insert instance", () => {
|
||||
test(`inside a nested div using "end": div 1 > div 2 > [div 3]`, () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasInserted = insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: "2",
|
||||
position: "end",
|
||||
});
|
||||
expect(hasInserted).toBeTruthy();
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("after a nested div using index: div 1 > div 2, [div 3]", () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasInserted = insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: "1",
|
||||
position: 1,
|
||||
});
|
||||
expect(hasInserted).toBeTruthy();
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("before a nested div using index: div 1 > [div 3], div 2", () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const hasInserted = insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: "1",
|
||||
position: 0,
|
||||
});
|
||||
expect(hasInserted).toBeTruthy();
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test(`after a nested div using "end": div 1 > div 2, [div 3]`, () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasInserted = insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: "1",
|
||||
position: "end",
|
||||
});
|
||||
expect(hasInserted).toBeTruthy();
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
|
||||
export type InstanceInsertionSpec = {
|
||||
parentId: Instance["id"];
|
||||
position: number | "end";
|
||||
};
|
||||
|
||||
export const insertInstanceMutable = (
|
||||
rootInstance: Instance,
|
||||
instance: Instance,
|
||||
spec: InstanceInsertionSpec
|
||||
): boolean => {
|
||||
if (spec.parentId !== rootInstance.id) {
|
||||
for (const child of rootInstance.children) {
|
||||
if (typeof child === "string") continue;
|
||||
const hasInserted = insertInstanceMutable(child, instance, spec);
|
||||
if (hasInserted === true) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inserting inside the selected/hovered instance.
|
||||
if (typeof spec.position === "number") {
|
||||
rootInstance.children.splice(spec.position, 0, instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inserting after all children
|
||||
if (spec.position === "end") {
|
||||
rootInstance.children.push(instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
type Instance,
|
||||
type Breakpoint,
|
||||
components,
|
||||
} from "@webstudio-is/react-sdk";
|
||||
|
||||
/**
|
||||
* Populate instance with:
|
||||
* - defaultStyles
|
||||
* @todo rewrite using immer
|
||||
*/
|
||||
export const populateInstance = (
|
||||
instance: Instance,
|
||||
breakpoint: Breakpoint["id"] = ""
|
||||
): Instance => {
|
||||
const populatedInstance = { ...instance };
|
||||
const componentMeta = components[instance.component];
|
||||
if (componentMeta !== undefined && "defaultStyle" in componentMeta) {
|
||||
const cssRule = {
|
||||
breakpoint,
|
||||
style: componentMeta.defaultStyle ?? {},
|
||||
};
|
||||
populatedInstance.cssRules.push(cssRule);
|
||||
}
|
||||
return populatedInstance;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { reparentInstanceMutable } from "./reparent-instance";
|
||||
|
||||
describe("Reparent instance", () => {
|
||||
test(`new parent`, () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "4",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
reparentInstanceMutable(rootInstance, "4", "2", 0);
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test(`same parent different position`, () => {
|
||||
const rootInstance: Instance = {
|
||||
component: "Box",
|
||||
id: "1",
|
||||
cssRules: [],
|
||||
children: [
|
||||
{
|
||||
component: "Box",
|
||||
id: "2",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "3",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: "Box",
|
||||
id: "4",
|
||||
cssRules: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
reparentInstanceMutable(rootInstance, "2", "1", 2);
|
||||
expect(rootInstance).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { findInstanceById } from "./find-instance";
|
||||
import { findParentInstance } from "./find-parent-instance";
|
||||
import { deleteInstanceMutable } from "./delete-instance";
|
||||
import { insertInstanceMutable } from "./insert-instance";
|
||||
|
||||
export const reparentInstanceMutable = (
|
||||
rootInstance: Instance,
|
||||
instanceId: Instance["id"],
|
||||
newParentId: Instance["id"],
|
||||
newPosition: number | "end"
|
||||
) => {
|
||||
const instance = findInstanceById(rootInstance, instanceId);
|
||||
if (instance === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPosition === "end") {
|
||||
deleteInstanceMutable(rootInstance, instance.id);
|
||||
insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: newParentId,
|
||||
position: "end",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let newPositionAdjusted = newPosition;
|
||||
|
||||
// If parent is the same, we need to adjust the position to account for the removal of the instance.
|
||||
const currentParent = findParentInstance(rootInstance, instance.id);
|
||||
if (currentParent !== undefined && currentParent.id === newParentId) {
|
||||
const currentPosition = currentParent.children.findIndex(
|
||||
(child) => typeof child !== "string" && child.id === instance.id
|
||||
);
|
||||
if (currentPosition < newPosition) {
|
||||
newPositionAdjusted--;
|
||||
}
|
||||
}
|
||||
|
||||
deleteInstanceMutable(rootInstance, instance.id);
|
||||
insertInstanceMutable(rootInstance, instance, {
|
||||
parentId: newParentId,
|
||||
position: newPositionAdjusted,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { type ChildrenUpdates } from "@webstudio-is/react-sdk";
|
||||
import { type Instance } from "@webstudio-is/react-sdk";
|
||||
import { createInstance } from "./create-instance";
|
||||
import { findInstanceById } from "./find-instance";
|
||||
|
||||
/**
|
||||
* Update children of an instance with new text.
|
||||
* When child is an instance - we update it's children with a single text entry.
|
||||
*/
|
||||
export const setInstanceChildrenMutable = (
|
||||
id: Instance["id"],
|
||||
// Not a consistent format now, maybe better:
|
||||
// [{set: 'string'}, {set: instance}, {update: {id,text}}]
|
||||
updates: ChildrenUpdates,
|
||||
rootInstance: Instance
|
||||
) => {
|
||||
const instance = findInstanceById(rootInstance, id);
|
||||
if (instance === undefined) return false;
|
||||
const children = [];
|
||||
|
||||
for (const update of updates) {
|
||||
// Set a string as a child
|
||||
if (typeof update === "string") {
|
||||
children.push(update);
|
||||
continue;
|
||||
}
|
||||
// We need to create an instance
|
||||
if ("createInstance" in update) {
|
||||
const childInstance = createInstance({
|
||||
id: update.id,
|
||||
component: update.component,
|
||||
children: [update.text],
|
||||
});
|
||||
children.push(childInstance);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set text as a single child of a child instance
|
||||
if ("text" in update) {
|
||||
const childInstance = findInstanceById(instance, update.id);
|
||||
// It should be impossible to have not found that instance
|
||||
if (childInstance === undefined) continue;
|
||||
children.push({ ...childInstance, children: [update.text] });
|
||||
continue;
|
||||
}
|
||||
|
||||
// It's a new instance.
|
||||
children.push(update);
|
||||
}
|
||||
|
||||
instance.children = children;
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Breakpoint, type Instance } from "@webstudio-is/react-sdk";
|
||||
import { StyleUpdates } from "../canvas-components";
|
||||
import { findInstanceById } from "./find-instance";
|
||||
|
||||
export const setInstanceStyleMutable = (
|
||||
rootInstance: Instance,
|
||||
id: Instance["id"],
|
||||
updates: StyleUpdates["updates"],
|
||||
breakpoint: Breakpoint
|
||||
) => {
|
||||
const instance = findInstanceById(rootInstance, id);
|
||||
if (instance === undefined) return false;
|
||||
let cssRule = instance.cssRules.find(
|
||||
(cssRule) => cssRule.breakpoint === breakpoint.id
|
||||
);
|
||||
if (cssRule === undefined) {
|
||||
cssRule = { style: {}, breakpoint: breakpoint.id };
|
||||
instance.cssRules.push(cssRule);
|
||||
}
|
||||
|
||||
for (const update of updates) {
|
||||
cssRule.style[update.property] = update.value;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Extends can't use node's resolution rn https://github.com/dividab/tsconfig-paths/issues/209#issuecomment-1177261091
|
||||
{
|
||||
"extends": "../../node_modules/@webstudio-is/tsconfig/base.json",
|
||||
"include": ["./src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -28,5 +28,5 @@ export const units = [
|
||||
"vmin",
|
||||
"vw",
|
||||
"x",
|
||||
"%"
|
||||
] as const;
|
||||
"%",
|
||||
] as const;
|
||||
@@ -13262,6 +13262,11 @@ nanoid@^3.2.0, nanoid@^3.3.1, nanoid@^3.3.4:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||
|
||||
nanoid@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.0.tgz#6e144dee117609232c3f415c34b0e550e64999a5"
|
||||
integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
@@ -18113,6 +18118,11 @@ zod@^3.17.3:
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba"
|
||||
integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg==
|
||||
|
||||
zod@^3.19.1:
|
||||
version "3.19.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
|
||||
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==
|
||||
|
||||
zwitch@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||
|
||||
Reference in new issue
Block a user