mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
(ui) sso data mapping - by type
This commit is contained in:
parent
6ac3ad4373
commit
d542292dbf
@ -85,7 +85,8 @@ require('src/javascript/plugins.js.erb');
|
||||
|
||||
function importAll (r) { r.keys().forEach(r); }
|
||||
|
||||
importAll(require.context('src/javascript/components/', true, /.*/));
|
||||
// we do not include markdown files (*.md)
|
||||
importAll(require.context('src/javascript/components/', true, /^.+\.(?!md).+/));
|
||||
importAll(require.context('src/javascript/controllers/', true, /.*/));
|
||||
importAll(require.context('src/javascript/services/', true, /.*/));
|
||||
importAll(require.context('src/javascript/directives/', true, /.*/));
|
||||
|
16
app/frontend/src/javascript/components/README.md
Normal file
16
app/frontend/src/javascript/components/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# components
|
||||
|
||||
This directory is holding the components built with [React](https://reactjs.org/).
|
||||
|
||||
During the migration phase, these components may be included in [the legacy angularJS app](../../templates) using [react2angular](https://github.com/coatue-oss/react2angular).
|
||||
|
||||
These components must be written using the following conventions:
|
||||
- The component name must be in CamelCase.
|
||||
- The component must be exported as a named export (no `export default`).
|
||||
- A component `FooBar` must have a `className="foo-bar"` attribute on its top-level element.
|
||||
- The stylesheet associated with the component must be located in `app/frontend/src/stylesheets/modules/same-directory-structure/foo-bar.scss`.
|
||||
- All methods in the component must be commented with a comment block.
|
||||
- Other constants and variables must be commented with an inline block.
|
||||
- Depending on if we want to use the `<Suspense>` wrapper or not, we can export the component directly or wrap it in a `<Loader>` wrapper.
|
||||
- When a component is used in angularJS, the wrapped is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.
|
||||
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
|
||||
export interface BooleanMappingFormProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
fieldMappingId: number,
|
||||
}
|
||||
|
||||
export const BooleanMappingForm = <TFieldValues extends FieldValues>({ register, fieldMappingId }: BooleanMappingFormProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
return (
|
||||
<div className="boolean-mapping-form">
|
||||
<h4>{t('app.shared.authentication.mappings')}</h4>
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.true_value`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.true_value')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.false_value`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.false_value')} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import AuthProviderAPI from '../../api/auth-provider';
|
||||
import { MappingFields } from '../../models/authentication-provider';
|
||||
import { MappingFields, mappingType } from '../../models/authentication-provider';
|
||||
import { Control } from 'react-hook-form/dist/types/form';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { FormInput } from '../form/form-input';
|
||||
@ -66,7 +66,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
/**
|
||||
* Return the type of data expected for the given index, in the current data-mapping form
|
||||
*/
|
||||
const getDataType = (formData: Array<TFieldValues>, index: number): string => {
|
||||
const getDataType = (formData: Array<TFieldValues>, index: number): mappingType => {
|
||||
const model = getModel(formData, index);
|
||||
const field = getField(formData, index);
|
||||
if (model && field) {
|
||||
@ -89,7 +89,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="data-mapping-form">
|
||||
<div className="data-mapping-form array-mapping-form">
|
||||
<h4>{t('app.shared.oauth2.define_the_fields_mapping')}</h4>
|
||||
<FabButton
|
||||
icon={<i className="fa fa-plus"/>}
|
||||
@ -97,23 +97,46 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
{t('app.shared.oauth2.add_a_match')}
|
||||
</FabButton>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="data-mapping-item">
|
||||
<div key={item.id} className="mapping-item">
|
||||
<div className="inputs">
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
|
||||
<div className="local-data">
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_model`} control={control} rules={{ required: true }} options={buildModelOptions()} label={t('app.shared.oauth2.model')}/>
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_field`} options={buildFieldOptions(output, index)} control={control} rules={{ required: true }} label={t('app.shared.oauth2.field')} />
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_model`}
|
||||
control={control} rules={{ required: true }}
|
||||
options={buildModelOptions()} label={t('app.shared.oauth2.model')}/>
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_field`}
|
||||
options={buildFieldOptions(output, index)}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.oauth2.field')} />
|
||||
</div>
|
||||
<div className="remote-data">
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.api_endpoint`} register={register} rules={{ required: true }} placeholder="/api/resource..." label={t('app.shared.oauth2.api_endpoint_url')} />
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.api_data_type`} options={[{ label: 'JSON', value: 'json' }]} control={control} rules={{ required: true }} label={t('app.shared.oauth2.api_type')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`} register={register} rules={{ required: true }} placeholder="field_name..." label={t('app.shared.oauth2.api_fields')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.api_endpoint`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
placeholder="/api/resource..."
|
||||
label={t('app.shared.oauth2.api_endpoint_url')} />
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${index}.api_data_type`}
|
||||
options={[{ label: 'JSON', value: 'json' }]}
|
||||
control={control} rules={{ required: true }}
|
||||
label={t('app.shared.oauth2.api_type')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
placeholder="field_name..."
|
||||
label={t('app.shared.oauth2.api_fields')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<FabButton icon={<i className="fa fa-random" />} onClick={toggleTypeMappingModal} />
|
||||
<FabButton icon={<i className="fa fa-random" />} onClick={toggleTypeMappingModal} disabled={getField(output, index) === undefined} />
|
||||
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
|
||||
<TypeMappingModal model={getModel(output, index)} field={getField(output, index)} type={getDataType(output, index)} isOpen={isOpenTypeMappingModal} toggleModal={toggleTypeMappingModal} />
|
||||
<TypeMappingModal model={getModel(output, index)}
|
||||
field={getField(output, index)}
|
||||
type={getDataType(output, index)}
|
||||
isOpen={isOpenTypeMappingModal}
|
||||
toggleModal={toggleTypeMappingModal}
|
||||
control={control} register={register}
|
||||
fieldMappingId={index} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { Control } from 'react-hook-form/dist/types/form';
|
||||
|
||||
export interface DateMappingFormProps<TFieldValues, TContext extends object> {
|
||||
control: Control<TFieldValues, TContext>,
|
||||
fieldMappingId: number,
|
||||
}
|
||||
|
||||
export const DateMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ control, fieldMappingId }: DateMappingFormProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// available date formats
|
||||
const dateFormats = [
|
||||
{
|
||||
label: 'ISO 8601',
|
||||
value: 'iso8601'
|
||||
},
|
||||
{
|
||||
label: 'RFC 2822',
|
||||
value: 'rfc2822'
|
||||
},
|
||||
{
|
||||
label: 'RFC 3339',
|
||||
value: 'rfc3339'
|
||||
},
|
||||
{
|
||||
label: 'Timestamp (s)',
|
||||
value: 'timestamp-s'
|
||||
},
|
||||
{
|
||||
label: 'Timestamp (ms)',
|
||||
value: 'timestamp-ms'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="date-mapping-form">
|
||||
<h4>{t('app.shared.authentication.input_format')}</h4>
|
||||
<FormSelect id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.format`}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
options={dateFormats}
|
||||
label={t('app.shared.authentication.date_format')} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control } from 'react-hook-form/dist/types/form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FormInput } from '../form/form-input';
|
||||
|
||||
export interface IntegerMappingFormProps<TFieldValues, TContext extends object> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
control: Control<TFieldValues, TContext>,
|
||||
fieldMappingId: number,
|
||||
}
|
||||
|
||||
export const IntegerMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: IntegerMappingFormProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
|
||||
|
||||
return (
|
||||
<div className="integer-mapping-form array-mapping-form">
|
||||
<h4>{t('app.shared.authentication.mappings')}</h4>
|
||||
<FabButton
|
||||
icon={<i className="fa fa-plus" />}
|
||||
onClick={() => append({})} />
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="mapping-item">
|
||||
<div className="inputs">
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.mapping_from')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
|
||||
register={register}
|
||||
type="number"
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.mapping_to')} />
|
||||
</div>
|
||||
<div className="actions">
|
||||
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control } from 'react-hook-form/dist/types/form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FormInput } from '../form/form-input';
|
||||
|
||||
export interface StringMappingFormProps<TFieldValues, TContext extends object> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
control: Control<TFieldValues, TContext>,
|
||||
fieldMappingId: number,
|
||||
}
|
||||
|
||||
export const StringMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: StringMappingFormProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
|
||||
|
||||
return (
|
||||
<div className="string-mapping-form array-mapping-form">
|
||||
<h4>{t('app.shared.authentication.mappings')}</h4>
|
||||
<FabButton
|
||||
icon={<i className="fa fa-plus" />}
|
||||
onClick={() => append({})} />
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="mapping-item">
|
||||
<div className="inputs">
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.mapping_from')} />
|
||||
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
label={t('app.shared.authentication.mapping_to')} />
|
||||
</div>
|
||||
<div className="actions">
|
||||
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,20 +1,40 @@
|
||||
import React from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IntegerMappingForm } from './integer-mapping-form';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { Control } from 'react-hook-form/dist/types/form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { mappingType } from '../../models/authentication-provider';
|
||||
import { BooleanMappingForm } from './boolean-mapping-form';
|
||||
import { DateMappingForm } from './date-mapping-form';
|
||||
import { StringMappingForm } from './string-mapping-form';
|
||||
|
||||
interface TypeMappingModalProps {
|
||||
interface TypeMappingModalProps<TFieldValues, TContext extends object> {
|
||||
model: string,
|
||||
field: string,
|
||||
type: string,
|
||||
type: mappingType,
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
control: Control<TFieldValues, TContext>,
|
||||
fieldMappingId: number,
|
||||
}
|
||||
|
||||
export const TypeMappingModal: React.FC<TypeMappingModalProps> = ({ model, field, type, isOpen, toggleModal }) => {
|
||||
export const TypeMappingModal = <TFieldValues extends FieldValues, TContext extends object>({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId }:TypeMappingModalProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen} toggleModal={toggleModal}>
|
||||
<h1>{model}</h1>
|
||||
<h2>{field}</h2>
|
||||
<h3>{type}</h3>
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.shared.authentication.data_mapping')}
|
||||
confirmButton={<i className="fa fa-check" />}
|
||||
onConfirm={toggleModal}>
|
||||
<span>{model} > {field} ({t('app.shared.authentication.TYPE_expected', { TYPE: t(`app.shared.authentication.types.${type}`) })})</span>
|
||||
{type === 'integer' && <IntegerMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
|
||||
{type === 'boolean' && <BooleanMappingForm register={register} fieldMappingId={fieldMappingId} />}
|
||||
{type === 'date' && <DateMappingForm control={control} fieldMappingId={fieldMappingId} />}
|
||||
{type === 'string' && <StringMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
|
||||
</FabModal>
|
||||
);
|
||||
};
|
||||
|
8
app/frontend/src/javascript/components/form/README.md
Normal file
8
app/frontend/src/javascript/components/form/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# components/from
|
||||
|
||||
This directory is holding the inputs components for usage within forms controlled by [React-hook-form](https://react-hook-form.com/).
|
||||
|
||||
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherits from [FormComponent](../models/form-component.ts)
|
||||
or from [FormControlledComponent](../models/form-component.ts).
|
||||
|
||||
Please look at the existing components for examples.
|
@ -19,7 +19,7 @@ interface FormInputProps<TFieldValues> extends InputHTMLAttributes<HTMLInputElem
|
||||
export const FormInput = <TFieldValues extends FieldValues>({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, step }: FormInputProps<TFieldValues>) => {
|
||||
// Compose classnames from props
|
||||
const classNames = `
|
||||
form-item ${className || ''}
|
||||
form-input form-item ${className || ''}
|
||||
${type === 'hidden' ? 'is-hidden' : ''}
|
||||
${error && error[id] ? 'is-incorrect' : ''}
|
||||
${rules && rules.required ? 'is-required' : ''}
|
||||
|
@ -29,7 +29,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
|
||||
*/
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
const classNames = `
|
||||
form-item ${className || ''}
|
||||
form-multi-select form-item ${className || ''}
|
||||
${error && error[id] ? 'is-incorrect' : ''}
|
||||
${rules && rules.required ? 'is-required' : ''}
|
||||
${disabled ? 'is-disabled' : ''}`;
|
||||
|
@ -28,7 +28,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
|
||||
*/
|
||||
export const FormSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, className, control, placeholder, options, valueDefault, error, rules, disabled, onChange }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
const classNames = `
|
||||
form-item ${className || ''}
|
||||
form-select form-item ${className || ''}
|
||||
${error && error[id] ? 'is-incorrect' : ''}
|
||||
${rules && rules.required ? 'is-required' : ''}
|
||||
${disabled ? 'is-disabled' : ''}`;
|
||||
|
@ -24,7 +24,7 @@ export interface AuthenticationProviderMapping {
|
||||
false_value: string,
|
||||
mapping: {
|
||||
from: string,
|
||||
to: number
|
||||
to: number|string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
@import "modules/stripe";
|
||||
@import "modules/tour";
|
||||
@import "modules/authentication-provider/data-mapping-form";
|
||||
@import "modules/authentication-provider/array-mapping-form";
|
||||
@import "modules/base/fab-modal";
|
||||
@import "modules/base/fab-input";
|
||||
@import "modules/base/fab-button";
|
||||
|
@ -0,0 +1,23 @@
|
||||
.array-mapping-form {
|
||||
.mapping-item {
|
||||
margin: 1rem;
|
||||
border-left: 4px solid var(--gray-soft-dark);
|
||||
border-radius: 4px;
|
||||
padding-left: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.inputs {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 1em;
|
||||
.delete-button {
|
||||
background-color: #cb1117;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +1,4 @@
|
||||
.data-mapping-form {
|
||||
.data-mapping-item {
|
||||
margin: 1rem;
|
||||
border-left: 4px solid var(--gray-soft-dark);
|
||||
border-radius: 4px;
|
||||
padding-left: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.inputs {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 1em;
|
||||
.delete-button {
|
||||
background-color: #cb1117;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.local-data,
|
||||
.remote-data{
|
||||
display: flex;
|
||||
|
@ -257,8 +257,20 @@ en:
|
||||
authentication_type_is_required: "Authentication type is required."
|
||||
data_mapping: "Data mapping"
|
||||
expected_data_type: "Expected data type"
|
||||
TYPE_expected: "{TYPE} expected"
|
||||
input_format: "Input format"
|
||||
mappings: "Mappings"
|
||||
mapping_from: "From"
|
||||
mapping_to: "To"
|
||||
true_value: "True value"
|
||||
false_value: "False value"
|
||||
date_format: "Date format"
|
||||
types:
|
||||
integer: "integer"
|
||||
string: "string"
|
||||
text: "text"
|
||||
date: "date"
|
||||
boolean: "boolean"
|
||||
#edition/creation form of an OAuth2 authentication provider
|
||||
oauth2:
|
||||
common_url: "Server root URL"
|
||||
|
@ -33,7 +33,7 @@ const customConfig = {
|
||||
}),
|
||||
isDevelopment && new (require('@pmmmwh/react-refresh-webpack-plugin'))(),
|
||||
isDevelopment && new (require('eslint-webpack-plugin'))({
|
||||
extensions: ['js', 'ts', 'tsx'],
|
||||
extensions: ['js', 'ts', 'tsx']
|
||||
})
|
||||
].filter(Boolean),
|
||||
module: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user