Skip to content

Commit 7d5bb69

Browse files
authored
feat: add element constraints and lifecycle hooks to component manifests (#5020)
1 parent 30bedb0 commit 7d5bb69

File tree

84 files changed

+4147
-175
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+4147
-175
lines changed

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,7 @@ jobs:
171171
working-directory: ${{ github.event.inputs.branch }}
172172
- name: Version and publish "latest" tag to NPM
173173
working-directory: ${{ github.event.inputs.branch }}
174-
run: yarn release --type=latest --sourceTag=${{ github.event.inputs.tag }}
174+
run: >-
175+
yarn release --type=latest --sourceTag=${{ github.event.inputs.tag }}
176+
--createGithubRelease=true
175177
runs-on: ubuntu-latest

.github/workflows/wac/release.wac.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export const release = createWorkflow({
141141
{
142142
name: 'Version and publish "latest" tag to NPM',
143143
"working-directory": BRANCH_NAME,
144-
run: `yarn release --type=latest --sourceTag=${DIST_TAG}`
144+
run: `yarn release --type=latest --sourceTag=${DIST_TAG} --createGithubRelease=true`
145145
}
146146
],
147147
{ "working-directory": BRANCH_NAME }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
import { Admin } from "webiny/extensions";
3+
4+
export const FunnelBuilder = () => {
5+
return (
6+
<>
7+
<Admin.Extension src={import.meta.dirname + "/pageType/index.tsx"} />
8+
<Admin.Extension src={import.meta.dirname + "/pageEditor/index.tsx"} />
9+
</>
10+
);
11+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
import { Button, useDialogs } from "webiny/admin/ui";
3+
import { ElementInputs } from "webiny/admin/website-builder/page/editor";
4+
import { useElementInputs } from "webiny/admin/website-builder/page/editor";
5+
import { useComponent } from "webiny/admin/website-builder/page/editor";
6+
7+
export const ElementInputsDecorator = ElementInputs.createDecorator(Original => {
8+
return function FunnelElementSettings(props) {
9+
const { element } = props;
10+
const { inputs, updateInputs } = useElementInputs(element.id);
11+
const component = useComponent(element.component.name);
12+
const dialogs = useDialogs();
13+
14+
const handleClick = () => {
15+
dialogs.showDialog({
16+
formData: inputs.registry,
17+
title: `Edit ${component.label} Settings`,
18+
content: <pre>{JSON.stringify(inputs, null, 2)}</pre>,
19+
acceptLabel: "Save Field Settings",
20+
cancelLabel: "Cancel",
21+
onAccept: (data: any) => {
22+
console.log(data);
23+
updateInputs(inputs => {
24+
Object.assign(inputs.registry, data);
25+
});
26+
}
27+
});
28+
};
29+
30+
if (props.element.component.name.startsWith("FunnelBuilder/")) {
31+
return (
32+
<>
33+
<Button
34+
variant={"primary"}
35+
text={`Edit ${component.label} Settings`}
36+
className={"w-full"}
37+
onClick={handleClick}
38+
/>
39+
<pre>{JSON.stringify(element, null, 2)}</pre>
40+
</>
41+
);
42+
}
43+
return <Original {...props} />;
44+
};
45+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from "react";
2+
import { ElementInputsDecorator } from "./ElementInputs.js";
3+
4+
export const FubElementInputs = () => {
5+
return <ElementInputsDecorator />;
6+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react";
2+
import { StepsNavigator } from "./stepsNavigator/index.js";
3+
import { FubElementInputs } from "./elementInputs/index.js";
4+
5+
export default () => {
6+
return (
7+
<>
8+
<StepsNavigator />
9+
<FubElementInputs />
10+
</>
11+
);
12+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from "react";
2+
import { Button, Icon } from "webiny/admin/ui";
3+
import { ReactComponent as DeleteIcon } from "webiny/admin/icons/close.svg";
4+
import {
5+
useCreateElement,
6+
useDeleteElement,
7+
useUpdateElement
8+
} from "webiny/admin/website-builder/page/editor";
9+
import { type FunnelInputs, useFunnel } from "./useFunnel.js";
10+
11+
const iconClasses =
12+
"absolute z-10 rounded-full bg-neutral-dimmed border-solid border-sm border-neutral-muted cursor-pointer fill-neutral-strong";
13+
14+
const iconPosition = {
15+
top: -8,
16+
right: -8
17+
};
18+
19+
export const StepsNavigator = () => {
20+
const funnel = useFunnel();
21+
const { createElement } = useCreateElement();
22+
const { updateElement } = useUpdateElement();
23+
const { deleteElement } = useDeleteElement();
24+
25+
if (!funnel) {
26+
return null;
27+
}
28+
29+
const { activeStep, steps = [] } = funnel.inputs;
30+
31+
const activateStep = (index: number) => {
32+
funnel.updateInputs(inputs => {
33+
inputs.activeStep = index;
34+
});
35+
};
36+
37+
const deleteStep = (stepElementId: string) => {
38+
deleteElement(stepElementId);
39+
};
40+
41+
const addStep = () => {
42+
const steps = funnel.inputs.steps ?? [];
43+
const insertIndex = Math.max(steps.length - 1, 0);
44+
45+
createElement({
46+
componentName: "FunnelBuilder/Step",
47+
parentId: funnel.id,
48+
slot: "steps",
49+
index: insertIndex,
50+
bindings: {
51+
inputs: {
52+
label: `Step ${steps.length}`
53+
}
54+
}
55+
});
56+
57+
updateElement<FunnelInputs>(funnel.id, inputs => {
58+
inputs.activeStep = insertIndex;
59+
});
60+
};
61+
62+
return (
63+
<div
64+
className={"flex flex-row p-sm bg-neutral-light justify-between"}
65+
data-affects-preview={"height"}
66+
>
67+
<div className={"flex gap-md"}>
68+
{steps.map((step, index) => {
69+
const isFirstStep = index === 0;
70+
const isLastStep = index === steps.length - 1;
71+
const canDelete = !isFirstStep && !isLastStep;
72+
const activeVariant = activeStep === index ? "primary" : "secondary";
73+
74+
return (
75+
<div className={"relative"} key={index}>
76+
<Button
77+
variant={activeVariant}
78+
text={step.label}
79+
className={"border-solid border-sm border-neutral-muted"}
80+
onClick={() => activateStep(index)}
81+
/>
82+
{canDelete ? (
83+
<Icon
84+
icon={<DeleteIcon />}
85+
label={"Delete step"}
86+
style={iconPosition}
87+
onClick={() => deleteStep(step.elementId)}
88+
className={iconClasses}
89+
/>
90+
) : null}
91+
</div>
92+
);
93+
})}
94+
</div>
95+
<Button variant={"ghost"} text={"+ Add step"} onClick={addStep} />
96+
</div>
97+
);
98+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { PageEditorConfig } from "webiny/admin/website-builder/page/editor";
3+
import { StepsNavigator as Component } from "./StepsNavigator.js";
4+
5+
const { Ui } = PageEditorConfig;
6+
7+
export const StepsNavigator = () => {
8+
return (
9+
<PageEditorConfig>
10+
<Ui.Content.Element
11+
name={"stepsNavigator"}
12+
before={"iframe"}
13+
element={
14+
<Ui.IsNotReadOnly>
15+
<Component />
16+
</Ui.IsNotReadOnly>
17+
}
18+
/>
19+
</PageEditorConfig>
20+
);
21+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
useSelectFromDocument,
3+
$getFirstElementOfType,
4+
useElementInputs
5+
} from "webiny/admin/website-builder/page/editor";
6+
7+
export type FunnelInputs = {
8+
activeStep: number;
9+
registry: Record<string, any>;
10+
steps: Array<{ elementId: string; label: string; children: string[] }>;
11+
};
12+
13+
export const useFunnel = () => {
14+
const elementId = useSelectFromDocument(state => {
15+
const funnel = $getFirstElementOfType(state, "FunnelBuilder/Funnel");
16+
return funnel ? funnel.id : null;
17+
});
18+
19+
const { inputs, updateInputs } = useElementInputs<FunnelInputs>(elementId, 1);
20+
21+
if (!elementId) {
22+
return null;
23+
}
24+
25+
return { id: elementId, inputs, updateInputs };
26+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import { Grid, Input } from "webiny/admin/ui";
3+
import { pagePathFromTitle } from "webiny/admin/website-builder";
4+
import type { FormApi } from "webiny/admin/form";
5+
import { Bind, UnsetOnUnmount, useForm, validation } from "webiny/admin/form";
6+
7+
const generatePath = (form: FormApi) => () => {
8+
const title = form.getValue("properties.title");
9+
10+
const titlePath = pagePathFromTitle(title ?? "");
11+
12+
form.setValue("properties.path", `/funnel/${titlePath}`);
13+
};
14+
15+
export const FunnelPageForm = () => {
16+
const form = useForm();
17+
18+
return (
19+
<>
20+
<Grid.Column span={12}>
21+
<UnsetOnUnmount name={"properties.title"}>
22+
<Bind name={"properties.title"} validators={[validation.create("required")]}>
23+
<Input label={"Title"} onBlur={generatePath(form)} />
24+
</Bind>
25+
</UnsetOnUnmount>
26+
</Grid.Column>
27+
<Grid.Column span={12}>
28+
<UnsetOnUnmount name={"properties.path"}>
29+
<Bind name={"properties.path"} validators={[validation.create("required")]}>
30+
<Input label={"Path"} />
31+
</Bind>
32+
</UnsetOnUnmount>
33+
</Grid.Column>
34+
</>
35+
);
36+
};

0 commit comments

Comments
 (0)