Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import styled, { css } from "styled-components";
import { Alert } from "@lifesg/react-design-system/alert";
import { Button } from "@lifesg/react-design-system/button";
import { Textarea } from "@lifesg/react-design-system/input-textarea";
import { FrontendEngine } from "../../../common";
import { css } from "@linaria/core";
import { Border, Colour, Radius, Spacing } from "@lifesg/react-design-system/theme";

// =============================================================================
// STYLE INTERFACE
// =============================================================================
interface IContentWrapperProps {
$hidden?: boolean | undefined;
$flexbox?: boolean | undefined;
}

export interface IModeButtonProps {
$active: boolean | undefined;
}

// =============================================================================
// STYLING
// =============================================================================
export const Wrapper = styled.div`
export const wrapper = css`
position: absolute;
z-index: 1;
top: 0;
Expand All @@ -31,29 +15,27 @@ export const Wrapper = styled.div`
flex-direction: column;
`;

export const ContentWrapper = styled.div<IContentWrapperProps>`
display: ${({ $hidden, $flexbox }) => {
if ($hidden) return "none";
if ($flexbox) return "flex";
return "block";
}};

${({ $flexbox }) =>
$flexbox &&
css`
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: ${Spacing["spacing-32"]};
padding: ${Spacing["spacing-32"]};
`};

export const contentWrapper = css`
display: block;
width: 100%;
flex: 1;
overflow-y: auto;
`;

export const Toolbar = styled.div`
export const contentWrapperHidden = css`
display: none;
`;

export const contentWrapperFlexbox = css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: ${Spacing["spacing-32"]};
padding: ${Spacing["spacing-32"]};
`;

export const toolbar = css`
position: relative;
display: flex;
width: 100%;
Expand All @@ -66,8 +48,8 @@ export const Toolbar = styled.div`
box-sizing: border-box;
`;

export const ModeButton = styled.button<IModeButtonProps>`
background: ${({ $active }) => ($active ? Colour["bg-primary-subtlest-selected"] : "transparent")};
export const modeButton = css`
background: transparent;
color: ${Colour["icon-primary"]};
display: grid;
cursor: pointer;
Expand All @@ -90,12 +72,16 @@ export const ModeButton = styled.button<IModeButtonProps>`
}
`;

export const FrontendEnginePreview = styled(FrontendEngine)`
export const modeButtonActive = css`
background: ${Colour["bg-primary-subtlest-selected"]};
`;

export const frontendEnginePreview = css`
width: 100%;
margin-bottom: ${Spacing["spacing-32"]};
`;

export const SchemaEditorWrapper = styled.div`
export const schemaEditorWrapper = css`
display: flex;
flex-direction: column;
width: 100%;
Expand All @@ -106,7 +92,7 @@ export const SchemaEditorWrapper = styled.div`
}
`;

export const SchemaEditor = styled(Textarea)`
export const schemaEditor = css`
flex: 1;
width: 100%;
overflow: auto;
Expand All @@ -115,22 +101,22 @@ export const SchemaEditor = styled(Textarea)`
border-radius: ${Radius.sm};
`;

export const SaveButton = styled(Button)`
export const saveButton = css`
width: 10rem;
margin-left: auto;
`;

export const ActionWrapper = styled.div`
export const actionWrapper = css`
display: flex;
width: 100%;
gap: 2rem;
`;

export const AlertWrapper = styled(Alert)`
export const alertWrapper = css`
flex-grow: 1;
`;

export const RefreshButton = styled(Button)`
export const refreshButton = css`
display: inline;
padding: 0;
`;
81 changes: 47 additions & 34 deletions src/stories/1-intro/form-builder/components/form-builder-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { TabletIcon } from "@lifesg/react-icons/tablet";
import { FormBuilder, IFormBuilderMethods } from "@lifesg/web-form-builder";
import { ISchemaProps } from "@lifesg/web-form-builder/translator";
import { Unstyled } from "@storybook/addon-docs/blocks";
import clsx from "clsx";
import { useRef, useState } from "react";
import { ContentWrapper, FrontendEnginePreview, ModeButton, Toolbar, Wrapper } from "./form-builder-tool.styles";
import * as styles from "./form-builder-tool.styles";
import { SchemaView } from "./schema-view";
import { IFrontendEngineData } from "../../../../components";
import { FrontendEngine } from "../../../common";
import { ThemeProvider } from "@lifesg/react-design-system/theme";

export type TFormBuilderMode = "form-builder" | "preview" | "schema";

Expand Down Expand Up @@ -48,72 +51,82 @@ export const FormBuilderTool = () => {
// =========================================================================
// RENDER FUNCTIONS
// =========================================================================
const getContentWrapperClassName = (hidden?: boolean, flexbox?: boolean) =>
clsx(styles.contentWrapper, hidden && styles.contentWrapperHidden, flexbox && styles.contentWrapperFlexbox);

const renderToolbar = () => (
<Toolbar>
<ModeButton
$active={formBuilderMode === "form-builder"}
<div className={styles.toolbar}>
<button
className={clsx(styles.modeButton, formBuilderMode === "form-builder" && styles.modeButtonActive)}
onClick={() => handleClickToolbarButton("form-builder")}
type="button"
>
<PencilIcon />
</ModeButton>
<ModeButton
$active={formBuilderMode === "preview"}
</button>
<button
className={clsx(styles.modeButton, formBuilderMode === "preview" && styles.modeButtonActive)}
onClick={() => handleClickToolbarButton("preview")}
type="button"
>
<EyeIcon />
</ModeButton>
<ModeButton
$active={formBuilderMode === "schema"}
</button>
<button
className={clsx(styles.modeButton, formBuilderMode === "schema" && styles.modeButtonActive)}
onClick={() => handleClickToolbarButton("schema")}
type="button"
>
<TabletIcon />
</ModeButton>
</Toolbar>
</button>
</div>
);

const renderPreview = () => {
if (formBuilderMode !== "preview") return;
return (
<ContentWrapper $flexbox={true}>
<div className={getContentWrapperClassName(false, true)}>
<Typography.HeadingMD weight="bold">Generate Form</Typography.HeadingMD>
{formBuilderOutput && <FrontendEnginePreview data={formBuilderOutput.schema as IFrontendEngineData} />}
</ContentWrapper>
{formBuilderOutput && (
<FrontendEngine
className={styles.frontendEnginePreview}
data={formBuilderOutput.schema as IFrontendEngineData}
/>
)}
</div>
);
};

const renderSchemaPreview = () => {
if (formBuilderMode !== "schema") return;
return (
<ContentWrapper $flexbox={true}>
<div className={getContentWrapperClassName(false, true)}>
<SchemaView
schema={formBuilderOutput.schema as IFrontendEngineData}
onChange={setFormBuilderOutput}
formBuilderRef={formBuilderRef}
/>
</ContentWrapper>
</div>
);
};

return (
<Unstyled>
<Wrapper>
{renderToolbar()}
<ContentWrapper $hidden={formBuilderMode !== "form-builder"}>
<FormBuilder
ref={formBuilderRef}
offset={5.1}
config={{
attributes: { prefill: { shouldShow: false } },
panels: { pages: { shouldShow: false } },
}}
/>
</ContentWrapper>
{renderPreview()}
{renderSchemaPreview()}
</Wrapper>
</Unstyled>
<ThemeProvider theme="lifesg" mode="light">
<Unstyled>
<div className={styles.wrapper}>
{renderToolbar()}
<div className={getContentWrapperClassName(formBuilderMode !== "form-builder")}>
<FormBuilder
ref={formBuilderRef}
offset={5.1}
config={{
attributes: { prefill: { shouldShow: false } },
panels: { pages: { shouldShow: false } },
}}
/>
</div>
{renderPreview()}
{renderSchemaPreview()}
</div>
</Unstyled>
</ThemeProvider>
);
};
45 changes: 25 additions & 20 deletions src/stories/1-intro/form-builder/components/schema-view.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { Typography } from "@lifesg/react-design-system/typography";
import { Alert } from "@lifesg/react-design-system/alert";
import { Button } from "@lifesg/react-design-system/button";
import { Textarea } from "@lifesg/react-design-system/input-textarea";
import clsx from "clsx";
import { IFormBuilderMethods } from "@lifesg/web-form-builder";
import { ISchemaProps } from "@lifesg/web-form-builder/translator";
import { useEffect, useState } from "react";
import { IFrontendEngineData } from "../../../../components";
import {
ActionWrapper,
AlertWrapper,
RefreshButton,
SaveButton,
SchemaEditor,
SchemaEditorWrapper,
} from "./form-builder-tool.styles";
import * as styles from "./form-builder-tool.styles";

interface IProps {
schema?: IFrontendEngineData | undefined;
Expand Down Expand Up @@ -74,34 +71,42 @@ export const SchemaView = ({ schema, onChange, formBuilderRef }: IProps) => {
// =========================================================================

const renderActionPanel = () => (
<ActionWrapper>
<div className={styles.actionWrapper}>
{isDirty && (
<>
{hasError ? (
<AlertWrapper type="error" showIcon>
<Alert className={styles.alertWrapper} type="error" showIcon>
Unable to save changes because there’s a syntax error. Amend the error or{" "}
<RefreshButton type="button" sizeType="small" styleType="link" onClick={handleReset}>
<Button
className={styles.refreshButton}
type="button"
sizeType="small"
styleType="link"
onClick={handleReset}
>
refresh to sync with the form builder.
</RefreshButton>
</AlertWrapper>
</Button>
</Alert>
) : (
<AlertWrapper type="warning" showIcon>
<Alert className={styles.alertWrapper} type="warning" showIcon>
To reflect changes on preview, save changes first.
</AlertWrapper>
</Alert>
)}
</>
)}
<SaveButton onClick={onSubmit}>{isDirty ? "Save Changes" : "Saved"}</SaveButton>
</ActionWrapper>
<Button className={clsx(styles.saveButton)} onClick={onSubmit}>
{isDirty ? "Save Changes" : "Saved"}
</Button>
</div>
);

return (
<>
<Typography.HeadingMD weight="bold">Generate Schema</Typography.HeadingMD>
{renderActionPanel()}
<SchemaEditorWrapper>
<SchemaEditor value={stringifiedSchema} onChange={handleSchemaChange} />
</SchemaEditorWrapper>
<div className={styles.schemaEditorWrapper}>
<Textarea className={styles.schemaEditor} value={stringifiedSchema} onChange={handleSchemaChange} />
</div>
</>
);
};
2 changes: 1 addition & 1 deletion src/stories/1-intro/form-builder/docs.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Meta, Unstyled } from "@storybook/addon-docs/blocks";
import { Meta } from "@storybook/addon-docs/blocks";
import { Wrapper } from "./components/form-builder-tool";
import { FormBuilderTool } from "./components/form-builder-tool";

Expand Down
Loading
Loading