Skip to content

Commit 37fb02e

Browse files
committed
feat: dynamic row wizard input
1 parent 84660e3 commit 37fb02e

File tree

11 files changed

+255
-16
lines changed

11 files changed

+255
-16
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171
{
7272
"command": "magento-toolbox.generateObserver",
7373
"title": "Magento Toolbox: Generate Observer"
74+
},
75+
{
76+
"command": "magento-toolbox.dynamicRowExample",
77+
"title": "Magento Toolbox: Dynamic Row Example"
7478
}
7579
],
7680
"menus": {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Command } from 'command/Command';
2+
import BlockClassGenerator from 'generator/block/BlockClassGenerator';
3+
import BlockWizard, { BlockWizardData } from 'wizard/BlockWizard';
4+
import FileGeneratorManager from 'generator/FileGeneratorManager';
5+
import { window } from 'vscode';
6+
import Common from 'util/Common';
7+
import WizzardClosedError from 'webview/error/WizzardClosedError';
8+
import DynamicRowWizard from 'wizard/DynamicRowWizard';
9+
10+
export default class DynamicRowExampleCommand extends Command {
11+
constructor() {
12+
super('magento-toolbox.dynamicRowExample');
13+
}
14+
15+
public async execute(...args: any[]): Promise<void> {
16+
const dynamicRowWizard = new DynamicRowWizard();
17+
18+
let data: void;
19+
20+
try {
21+
data = await dynamicRowWizard.show();
22+
} catch (error) {
23+
if (error instanceof WizzardClosedError) {
24+
return;
25+
}
26+
27+
throw error;
28+
}
29+
}
30+
}

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import XmlClasslikeHoverProvider from 'hover/XmlClasslikeHoverProvider';
1818
import ObserverCodelensProvider from 'codelens/ObserverCodelensProvider';
1919
import GenerateObserverCommand from 'command/GenerateObserverCommand';
2020
import GenerateBlockCommand from 'command/GenerateBlockCommand';
21+
import DynamicRowExampleCommand from 'command/DynamicRowExampleCommand';
2122

2223
// This method is called when your extension is activated
2324
// Your extension is activated the very first time the command is executed
@@ -32,6 +33,7 @@ export async function activate(context: vscode.ExtensionContext) {
3233
GenerateXmlCatalogCommand,
3334
GenerateObserverCommand,
3435
GenerateBlockCommand,
36+
DynamicRowExampleCommand,
3537
];
3638

3739
ExtensionState.init(context);

src/webview/WizardFieldBuilder.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export class WizardFieldBuilder {
1717
private id: string | undefined = undefined,
1818
private label: string | undefined = undefined,
1919
private description: string[] | undefined = undefined,
20-
private dependsOn: FieldDependency | undefined = undefined
20+
private dependsOn: FieldDependency | undefined = undefined,
21+
private fields: WizardField[] | undefined = undefined
2122
) {}
2223

2324
public static new(): WizardFieldBuilder {
@@ -40,6 +41,10 @@ export class WizardFieldBuilder {
4041
return new WizardFieldBuilder(WizardInput.Checkbox, id, label);
4142
}
4243

44+
public static dynamicRow(id?: string, label?: string): WizardFieldBuilder {
45+
return new WizardFieldBuilder(WizardInput.DynamicRow, id, label);
46+
}
47+
4348
public setId(id: string): WizardFieldBuilder {
4449
this.id = id;
4550
return this;
@@ -79,6 +84,11 @@ export class WizardFieldBuilder {
7984
return this;
8085
}
8186

87+
public addFields(fields: WizardField[]): WizardFieldBuilder {
88+
this.fields = fields;
89+
return this;
90+
}
91+
8292
public addOption(option: WizardSelectOption): WizardFieldBuilder {
8393
this.options.push(option);
8494
return this;
@@ -122,6 +132,15 @@ export class WizardFieldBuilder {
122132
initialValue: this.initialValue,
123133
type: this.type,
124134
};
135+
case WizardInput.DynamicRow:
136+
return {
137+
id: this.id,
138+
label: this.label,
139+
description: this.description,
140+
dependsOn: this.dependsOn,
141+
fields: this.fields ?? [],
142+
type: this.type,
143+
};
125144
case WizardInput.Checkbox:
126145
return {
127146
id: this.id,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { WizardDynamicRowField, WizardField } from 'webview/types';
3+
import { FieldRenderer } from './FieldRenderer';
4+
import { FieldArray, useFormikContext } from 'formik';
5+
6+
interface Props {
7+
field: WizardDynamicRowField;
8+
}
9+
10+
export const DynamicRowInput: React.FC<Props> = ({ field }) => {
11+
const { values } = useFormikContext<any>();
12+
const rows = values[field.id] ?? ([] as Record<string, any>[]);
13+
14+
return (
15+
<FieldArray
16+
name={field.id}
17+
render={arrayHelpers => {
18+
return (
19+
<>
20+
{/* @ts-ignore */}
21+
<vscode-table zebra>
22+
<vscode-table-header slot="header">
23+
{field.fields.map(field => (
24+
<vscode-table-header-cell key={`${field.id}-header`}>
25+
{field.label}
26+
</vscode-table-header-cell>
27+
))}
28+
<vscode-table-header-cell key="action-header">Action</vscode-table-header-cell>
29+
</vscode-table-header>
30+
31+
<vscode-table-body slot="body">
32+
{rows.map((row: any, index: number) => (
33+
<vscode-table-row key={`row-${index}`}>
34+
{field.fields.map(childField => (
35+
<vscode-table-cell
36+
className="dynamic-row-cell"
37+
key={`${row.id}-${childField.id}`}
38+
>
39+
<FieldRenderer prefix={`${field.id}.${index}`} field={childField} simple />
40+
</vscode-table-cell>
41+
))}
42+
<vscode-table-cell className="dynamic-row-cell" key="action-row">
43+
<vscode-button onClick={() => arrayHelpers.remove(index)}>
44+
Remove
45+
</vscode-button>
46+
</vscode-table-cell>
47+
</vscode-table-row>
48+
))}
49+
</vscode-table-body>
50+
</vscode-table>
51+
<vscode-button className="dynamic-row-add-row" onClick={() => arrayHelpers.push({})}>
52+
Add Row
53+
</vscode-button>
54+
</>
55+
);
56+
}}
57+
></FieldArray>
58+
);
59+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Field, FormikProps, getIn } from 'formik';
2+
3+
export const FieldErrorMessage: React.FC<{ name: string }> = ({ name }) => {
4+
return (
5+
<Field
6+
name={name}
7+
render={({ form }: { form: FormikProps<any> }) => {
8+
const error = form.errors[name];
9+
const touch = getIn(form.touched, name);
10+
11+
return touch && error ? error : null;
12+
}}
13+
/>
14+
);
15+
};

src/webview/components/Wizard/FieldRenderer.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,36 @@ import { Option } from '@vscode-elements/elements/dist/includes/vscode-select/ty
22
import { useField, useFormikContext } from 'formik';
33
import { useEffect, useMemo, useRef } from 'react';
44
import { WizardField, WizardInput, WizardSelectOption } from 'webview/types';
5+
import { DynamicRowInput } from './DynamicRowInput';
6+
import { FieldErrorMessage } from './FieldErrorMessage';
57

68
interface Props {
79
field: WizardField;
10+
prefix?: string;
11+
simple?: boolean;
812
}
913

10-
const getFieldProps = (field: WizardField) => {
14+
const getFieldId = (field: WizardField, prefix?: string) => {
15+
if (prefix) {
16+
return `${prefix}.${field.id}`;
17+
}
18+
19+
return field.id;
20+
};
21+
22+
const getFieldProps = (field: WizardField, prefix?: string) => {
1123
switch (field.type) {
1224
case WizardInput.Readonly:
1325
return { readonly: true };
1426
case WizardInput.Checkbox:
15-
return { name: field.id, checked: false };
27+
return { name: getFieldId(field, prefix), checked: false };
1628
case WizardInput.Select:
17-
return { name: field.id };
29+
return { name: getFieldId(field, prefix) };
30+
case WizardInput.DynamicRow:
31+
return { name: getFieldId(field, prefix) };
1832
default:
1933
return {
20-
name: field.id,
34+
name: getFieldId(field, prefix),
2135
placeholder: field.placeholder,
2236
};
2337
}
@@ -33,10 +47,10 @@ const mapOption = (option: WizardSelectOption): Option => {
3347
};
3448
};
3549

36-
export const FieldRenderer: React.FC<Props> = ({ field }) => {
50+
export const FieldRenderer: React.FC<Props> = ({ field, simple = false, prefix }) => {
3751
const { values } = useFormikContext<any>();
3852
const el = useRef<any>(null);
39-
const [fieldProps, meta] = useField(getFieldProps(field));
53+
const [fieldProps, meta] = useField(getFieldProps(field, prefix));
4054

4155
/**
4256
* vscode-elements do not support (yet) onChange prop
@@ -86,6 +100,9 @@ export const FieldRenderer: React.FC<Props> = ({ field }) => {
86100
/>
87101
);
88102
}
103+
case WizardInput.DynamicRow: {
104+
return <DynamicRowInput field={field} />;
105+
}
89106
case WizardInput.Checkbox: {
90107
return <vscode-checkbox {...fieldProps} ref={el} />;
91108
}
@@ -99,9 +116,25 @@ export const FieldRenderer: React.FC<Props> = ({ field }) => {
99116
}
100117

101118
if (fieldInner) {
119+
if (simple) {
120+
return (
121+
<>
122+
{fieldInner}
123+
<vscode-form-helper>
124+
<p className="error">
125+
<FieldErrorMessage name={fieldProps.name} />
126+
</p>
127+
</vscode-form-helper>
128+
</>
129+
);
130+
}
131+
102132
return (
103133
<vscode-form-group>
104-
<vscode-label>{field.label}</vscode-label>
134+
{field.type !== WizardInput.DynamicRow && <vscode-label>{field.label}</vscode-label>}
135+
{field.type === WizardInput.DynamicRow && (
136+
<div className="dynamic-row-title">{field.label}</div>
137+
)}
105138
{fieldInner}
106139
<vscode-form-helper>
107140
{meta.touched && meta.error && <p className="error">{meta.error}</p>}

src/webview/components/Wizard/Renderer.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ interface Props {
1111
}
1212

1313
export const Renderer: React.FC<Props> = ({ wizard, vscode }) => {
14+
const isSingleTab = wizard.tabs.length === 1;
1415
const initialValues: FormikValues = wizard.tabs.reduce((acc: FormikValues, tab) => {
1516
tab.fields.reduce((acc: FormikValues, field) => {
1617
if (field.type === WizardInput.Select && field.multiple) {
1718
acc[field.id] = field.initialValue ?? [];
1819
return acc;
1920
}
2021

22+
if (field.type === WizardInput.DynamicRow) {
23+
acc[field.id] = field.initialValue ?? [];
24+
return acc;
25+
}
26+
2127
acc[field.id] = field.initialValue ?? '';
2228
return acc;
2329
}, {});
@@ -50,21 +56,26 @@ export const Renderer: React.FC<Props> = ({ wizard, vscode }) => {
5056
<vscode-tabs>
5157
{wizard.tabs.map(tab => {
5258
return (
53-
<>
54-
<vscode-tab-header slot="header">{tab.title}</vscode-tab-header>
55-
<vscode-tab-panel>
59+
<div key={tab.id}>
60+
{!isSingleTab && (
61+
<vscode-tab-header slot="header">{tab.title}</vscode-tab-header>
62+
)}
63+
<vscode-tab-panel className="tab-panel">
5664
<p>{tab.description}</p>
5765
<br />
5866
{tab.fields.map(field => {
59-
return <FieldRenderer key={field.id} field={field} />;
67+
return <FieldRenderer key={`${field.id}-field}`} field={field} />;
6068
})}
6169
</vscode-tab-panel>
62-
</>
70+
</div>
6371
);
6472
})}
6573
</vscode-tabs>
6674
<br />
67-
<vscode-button disabled={!props.dirty || !props.isValid} type="submit">
75+
<vscode-button
76+
onClick={() => props.submitForm()}
77+
disabled={!props.dirty || !props.isValid}
78+
>
6879
Submit
6980
</vscode-button>
7081
</>

src/webview/components/app.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
11
.app {
22
padding: 16px;
33
}
4+
5+
.dynamic-row-cell {
6+
vertical-align: top;
7+
padding-top: 8px;
8+
padding-bottom: 6px;
9+
}
10+
11+
.dynamic-row-title {
12+
width: 100%;
13+
text-align: center;
14+
font-size: 14px;
15+
font-weight: 600;
16+
margin-bottom: 12px;
17+
}
18+
19+
.dynamic-row-add-row {
20+
margin-top: 12px;
21+
}
22+
23+
.tab-panel {
24+
padding: 16px;
25+
}

src/webview/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export type WizardField =
3838
| WizardNumberField
3939
| WizardSelectField
4040
| WizardReadonlyField
41-
| WizardCheckboxField;
41+
| WizardCheckboxField
42+
| WizardDynamicRowField;
4243

4344
export interface WizardTextField extends WizardGenericField {
4445
type: WizardInput.Text;
@@ -66,7 +67,12 @@ export interface WizardCheckboxField extends WizardGenericField {
6667
type: WizardInput.Checkbox;
6768
}
6869

69-
export type FieldValue = string | number | boolean;
70+
export interface WizardDynamicRowField extends WizardGenericField {
71+
type: WizardInput.DynamicRow;
72+
fields: WizardField[];
73+
}
74+
75+
export type FieldValue = string | number | boolean | Record<string, string | number | boolean>;
7076

7177
export interface FieldDependency {
7278
field: string;
@@ -87,6 +93,7 @@ export enum WizardInput {
8793
Select = 'select',
8894
Checkbox = 'checkbox',
8995
Readonly = 'readonly',
96+
DynamicRow = 'dynamic-row',
9097
}
9198

9299
export interface WizardSelectOption {

0 commit comments

Comments
 (0)