Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
oshi97 committed Sep 19, 2023
1 parent 76bf68a commit 0eb59ff
Show file tree
Hide file tree
Showing 23 changed files with 738 additions and 397 deletions.
731 changes: 379 additions & 352 deletions package-lock.json

Large diffs are not rendered by default.

68 changes: 60 additions & 8 deletions packages/studio-plugin/src/parsers/ComponentTreeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
ComponentState,
ComponentStateKind,
ErrorComponentState,
StandardComponentState,
RepeaterState,
StandardOrModuleComponentState,
} from "../types/ComponentState";
import { v4 } from "uuid";
import { FileMetadataKind, TypelessPropVal } from "../types";
Expand Down Expand Up @@ -71,13 +72,20 @@ export default class ComponentTreeParser {
};

if (component.isKind(SyntaxKind.JsxExpression)) {
throw new Error(
`Jsx nodes of kind "${component.getKindName()}" are not supported for direct use` +
" in page files."
const { selfClosingElement, listExpression } =
StaticParsingHelpers.parseJsxExpression(component);
const parsedRepeaterElement = this.parseRepeaterElement(
defaultImports,
selfClosingElement,
listExpression
);
return {
...commonComponentState,
...parsedRepeaterElement,
};
}

if (!TypeGuards.isNotFragmentElement(component)) {
if (!ComponentTreeParser.isNotFragmentElement(component)) {
return {
...commonComponentState,
kind: ComponentStateKind.Fragment,
Expand All @@ -98,12 +106,52 @@ export default class ComponentTreeParser {
};
}

private static isNotFragmentElement(
element: JsxElement | JsxSelfClosingElement | JsxFragment
): element is JsxElement | JsxSelfClosingElement {
if (element.isKind(SyntaxKind.JsxFragment)) {
return false;
}
if (element.isKind(SyntaxKind.JsxSelfClosingElement)) {
return true;
}
const name = StaticParsingHelpers.parseJsxElementName(element);
return !["Fragment", "React.Fragment"].includes(name);
}

private parseRepeaterElement(
defaultImports: Record<string, string>,
repeatedComponent: JsxSelfClosingElement,
listExpression: string
): Omit<RepeaterState, "uuid" | "parentUUID"> {
const componentName =
StaticParsingHelpers.parseJsxElementName(repeatedComponent);
const parsedRepeatedComponent = this.parseElement(
repeatedComponent,
componentName,
defaultImports
);
if (parsedRepeatedComponent.kind === ComponentStateKind.BuiltIn) {
throw new Error(
"Error parsing map expression: repetition of built-in components is not supported."
);
}
return {
kind: ComponentStateKind.Repeater,
listExpression,
repeatedComponent: {
...parsedRepeatedComponent,
componentName,
},
};
}

private parseElement(
component: JsxElement | JsxSelfClosingElement,
componentName: string,
defaultImports: Record<string, string>
):
| Pick<StandardComponentState, "kind" | "props" | "metadataUUID">
| Pick<StandardOrModuleComponentState, "kind" | "props" | "metadataUUID">
| Pick<BuiltInState, "kind" | "props">
| Omit<ErrorComponentState, "componentName"> {
const attributes: JsxAttributeLike[] = component.isKind(
Expand Down Expand Up @@ -149,8 +197,12 @@ export default class ComponentTreeParser {
props,
};
}
const { metadataUUID, propShape } = fileMetadata;
const { kind: fileMetadataKind, metadataUUID, propShape } = fileMetadata;

const componentStateKind =
fileMetadataKind === FileMetadataKind.Module
? ComponentStateKind.Module
: ComponentStateKind.Standard;
const props = StaticParsingHelpers.parseJsxAttributes(
attributes,
propShape
Expand All @@ -173,7 +225,7 @@ export default class ComponentTreeParser {
}

return {
kind: ComponentStateKind.Standard,
kind: componentStateKind,
metadataUUID,
props,
};
Expand Down
59 changes: 41 additions & 18 deletions packages/studio-plugin/src/utils/TypeGuards.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import {
JsxElement,
JsxFragment,
JsxSelfClosingElement,
SyntaxKind,
} from "ts-morph";
import {
ComponentState,
ComponentStateKind,
EditableComponentState,
FileMetadata,
FileMetadataKind,
ModuleMetadata,
ModuleState,
PropShape,
PropType,
PropVal,
PropValueKind,
PropValues,
PropValueType,
RepeaterState,
SiteSettingsShape,
SiteSettingsValues,
StandardOrModuleComponentState,
} from "../types";

import StaticParsingHelpers from "../parsers/helpers/StaticParsingHelpers";
import {
SiteSettingsExpression,
StreamsDataExpression,
Expand Down Expand Up @@ -159,17 +158,41 @@ export default class TypeGuards {
return typeof value === "string" && value.startsWith("siteSettings.");
}

static isNotFragmentElement(
element: JsxElement | JsxSelfClosingElement | JsxFragment
): element is JsxElement | JsxSelfClosingElement {
if (element.isKind(SyntaxKind.JsxFragment)) {
return false;
}
if (element.isKind(SyntaxKind.JsxSelfClosingElement)) {
return true;
}
const name = StaticParsingHelpers.parseJsxElementName(element);
return !["Fragment", "React.Fragment"].includes(name);
static isModuleMetadata(
metadata?: FileMetadata | null
): metadata is ModuleMetadata {
return metadata?.kind === FileMetadataKind.Module;
}

static isModuleState(
componentState: ComponentState
): componentState is ModuleState {
return componentState.kind === ComponentStateKind.Module;
}

static isStandardOrModuleComponentState(
componentState: ComponentState
): componentState is StandardOrModuleComponentState {
return (
componentState.kind === ComponentStateKind.Module ||
componentState.kind === ComponentStateKind.Standard
);
}

static isRepeaterState(
componentState: ComponentState
): componentState is RepeaterState {
return componentState.kind === ComponentStateKind.Repeater;
}

static isEditableComponentState(
componentState: ComponentState
): componentState is EditableComponentState {
return (
componentState.kind === ComponentStateKind.Module ||
componentState.kind === ComponentStateKind.Standard ||
componentState.kind === ComponentStateKind.Repeater
);
}

static isSiteSettingsValues(
Expand Down
1 change: 1 addition & 0 deletions packages/studio-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stats.html
6 changes: 5 additions & 1 deletion packages/studio-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"react-tooltip": "^5.18.0",
"tailwind-merge": "^1.8.1",
"tailwindcss": "^3.3.3",
"vite": "^4.4.7",
"vite-plugin-svgr": "^2.4.0",
"zundo": "2.0.0-beta.12",
"zustand": "^4.3.2"
},
Expand All @@ -64,7 +66,9 @@
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.3.1",
"resize-observer-polyfill": "^1.5.1",
"vite-plugin-svgr": "^3.2.0"
"rollup-plugin-visualizer": "^5.9.2",
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-dts": "^3.5.3"
},
"peerDependencies": {
"@yext/studio-plugin": "*"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, MouseEvent, CSSProperties } from "react";
import { ReactComponent as VectorIcon } from "../../icons/vector.svg";
import { startCase } from "lodash";
import startCase from "lodash/startCase";

const listStyles: CSSProperties = {
minWidth: "200px",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEqual } from "lodash";
import isEqual from "lodash/isEqual";
import { Component, PropsWithChildren, ReactInstance } from "react";
import DOMRectProperties from "../store/models/DOMRectProperties";
import useStudioStore from "../store/useStudioStore";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { StaticPageSettings } from "./StaticPageModal";
import { streamScopeFormData } from "../AddPageButton/StreamScopeCollector";
import PageDataValidator from "../../utils/PageDataValidator";
import { toast } from "react-toastify";
import { isEqual } from "lodash";
import isEqual from "lodash/isEqual";

type EntityPageSettings = StaticPageSettings & StreamScopeForm;

Expand Down
42 changes: 38 additions & 4 deletions packages/studio-ui/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
import { Dispatch, SetStateAction, useMemo } from "react";
import useStudioStore from "../store/useStudioStore";
import usePreviewProps from "../hooks/usePreviewProps";
import ComponentTreePreview from "./ComponentTreePreview";
import useRawSiteSettings from "../hooks/useRawSiteSettings";
import { ComponentStateHelpers, TypeGuards } from "@yext/studio-plugin";
import get from "lodash/get";
import { ITooltip } from "react-tooltip";

export default function PreviewPanel(props: {
setTooltipProps: Dispatch<SetStateAction<ITooltip>>;
}) {
const { setTooltipProps } = props;
const componentTree = useStudioStore((store) =>
store.actions.getComponentTree()
);
const [componentTree, moduleUUIDBeingEdited, getComponentState] =
useStudioStore((store) => [
store.actions.getComponentTree(),
store.pages.moduleUUIDBeingEdited,
store.actions.getComponentState,
]);

const pageExpressionSources = usePageExpressionSources();

const state = moduleUUIDBeingEdited
? getComponentState(componentTree, moduleUUIDBeingEdited)
: undefined;
const list =
state && TypeGuards.isRepeaterState(state)
? get(pageExpressionSources, state.listExpression)
: undefined;
const item = Array.isArray(list) ? list[0] : undefined;

const extractedState =
state && TypeGuards.isEditableComponentState(state)
? ComponentStateHelpers.extractRepeatedState(state)
: undefined;
const parentPreviewProps = usePreviewProps(
extractedState,
pageExpressionSources,
item
);

const expressionSources = useMemo(
() => ({
...pageExpressionSources,
...(moduleUUIDBeingEdited && { props: parentPreviewProps }),
}),
[pageExpressionSources, moduleUUIDBeingEdited, parentPreviewProps]
);

if (!componentTree) {
return null;
}

return (
<ComponentTreePreview
componentTree={componentTree}
expressionSources={pageExpressionSources}
expressionSources={expressionSources}
setTooltipProps={setTooltipProps}
/>
);
Expand Down
54 changes: 54 additions & 0 deletions packages/studio-ui/src/components/RepeaterPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RepeaterState } from "@yext/studio-plugin";
import get from "lodash/get";
import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
import { ExpressionSources } from "../utils/getPropsForPreview";
import ComponentPreview from "./ComponentPreview";
import { ITooltip } from "react-tooltip";

interface RepeaterPreviewProps {
repeaterState: RepeaterState;
expressionSources: ExpressionSources;
setTooltipProps: Dispatch<SetStateAction<ITooltip>>;
}

/**
* Renders the preview for a Repeater component.
*/
export default function RepeaterPreview({
repeaterState,
expressionSources,
setTooltipProps,
}: RepeaterPreviewProps): JSX.Element | null {
const { repeatedComponent, listExpression } = repeaterState;
const repeatedElementState = useMemo(
() => ({
...repeatedComponent,
uuid: repeaterState.uuid,
parentUUID: repeaterState.parentUUID,
}),
[repeatedComponent, repeaterState]
);

const renderRepeatedElement = useCallback(
(item: unknown, key: number | string) => (
<ComponentPreview
componentState={repeatedElementState}
expressionSources={expressionSources}
parentItem={item}
key={key}
setTooltipProps={setTooltipProps}
/>
),
[repeatedElementState, expressionSources, setTooltipProps]
);

const list = get(expressionSources, listExpression) as unknown;
if (!Array.isArray(list)) {
console.warn(
`Unable to render list repeater. Expected "${listExpression}" to reference an array in `,
expressionSources
);
return null;
}
return <>{list.map(renderRepeatedElement)}</>;
}
2 changes: 1 addition & 1 deletion packages/studio-ui/src/components/SiteSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
SiteSettingsVal,
} from "@yext/studio-plugin";
import React, { useCallback } from "react";
import { startCase } from "lodash";
import startCase from "lodash/startCase";
import useStudioStore from "../store/useStudioStore";
import PropInput from "./PropInput";

Expand Down
2 changes: 1 addition & 1 deletion packages/studio-ui/src/components/common/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeEvent, useEffect, useMemo, useState } from "react";
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import PropValueHelpers from "../../utils/PropValueHelpers";
import { PropValueKind, PropValueType } from "@yext/studio-plugin";

Expand Down
2 changes: 1 addition & 1 deletion packages/studio-ui/src/hooks/useFuncWithZundoBatching.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useMemo } from "react";
import useTemporalStore from "../store/useTemporalStore";
import { debounce } from "lodash";
import debounce from "lodash/debounce";

/**
* Updates a function so it doesn't trigger Zundo store updates until after a
Expand Down
2 changes: 1 addition & 1 deletion packages/studio-ui/src/hooks/useHasChanges.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import useStudioStore from "../store/useStudioStore";
import { isEqual } from "lodash";
import isEqual from "lodash/isEqual";

export default function useHasChanges() {
// TODO(SLAP-2556) Refactor pendingChanges to use PreviousSaveSlice
Expand Down
1 change: 1 addition & 0 deletions packages/studio-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as App } from "./App";
export { default as hotReloadStore } from "./store/hotReloadStore";
export { StudioHMRUpdateID } from "@yext/studio-plugin";
Loading

0 comments on commit 0eb59ff

Please sign in to comment.