1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 07:49:41 +02:00

refactor(app): move react components to react codebase [EE-3179] (#6971)

This commit is contained in:
Chaim Lev-Ari 2022-06-17 19:18:42 +03:00 committed by GitHub
parent 212400c283
commit 18252ab854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
346 changed files with 642 additions and 644 deletions

View file

@ -0,0 +1,34 @@
.items {
margin-top: 10px;
}
.items > * + * {
margin-top: 2px;
}
.label {
text-align: left;
font-size: 0.9em;
padding-top: 7px;
margin-bottom: 0;
display: inline-block;
max-width: 100%;
font-weight: 700;
}
.item-line {
display: flex;
}
.item-line.has-error {
margin-bottom: 20px;
}
.item-actions {
display: flex;
margin-left: 2px;
}
.default-item {
width: 100% !important;
}

View file

@ -0,0 +1,95 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { Input, Select } from '../Input';
import { DefaultType, InputList } from './InputList';
const meta: Meta = {
title: 'Components/Form/InputList',
component: InputList,
};
export default meta;
export { Defaults, ListWithInputAndSelect };
function Defaults() {
const [values, setValues] = useState<DefaultType[]>([{ value: '' }]);
return (
<InputList
label="default example"
value={values}
onChange={(value) => setValues(value)}
/>
);
}
interface ListWithSelectItem {
value: number;
select: string;
id: number;
}
interface ListWithInputAndSelectArgs {
label: string;
movable: boolean;
tooltip: string;
}
function ListWithInputAndSelect({
label,
movable,
tooltip,
}: ListWithInputAndSelectArgs) {
const [values, setValues] = useState<ListWithSelectItem[]>([
{ value: 0, select: '', id: 0 },
]);
return (
<InputList<ListWithSelectItem>
label={label}
onChange={setValues}
value={values}
item={SelectAndInputItem}
itemKeyGetter={(item) => item.id}
movable={movable}
itemBuilder={() => ({ value: 0, select: '', id: values.length })}
tooltip={tooltip}
/>
);
}
ListWithInputAndSelect.args = {
label: 'List with select and input',
movable: false,
tooltip: '',
};
function SelectAndInputItem({
item,
onChange,
}: {
item: ListWithSelectItem;
onChange: (value: ListWithSelectItem) => void;
}) {
return (
<div className="flex gap-2">
<Input
type="number"
value={item.value}
onChange={(e) =>
onChange({ ...item, value: parseInt(e.target.value, 10) })
}
/>
<Select
onChange={(e) => onChange({ ...item, select: e.target.value })}
options={[
{ label: 'option1', value: 'option1' },
{ label: 'option2', value: 'option2' },
]}
value={item.select}
/>
</div>
);
}

View file

@ -0,0 +1,232 @@
import { ComponentType } from 'react';
import clsx from 'clsx';
import { AddButton, Button } from '@@/buttons';
import { Tooltip } from '@@/Tip/Tooltip';
import { TextTip } from '@@/Tip/TextTip';
import { Input } from '../Input';
import { FormError } from '../FormError';
import styles from './InputList.module.css';
import { arrayMove } from './utils';
export type InputListError<T> = Record<keyof T, string>;
export interface ItemProps<T> {
item: T;
onChange(value: T): void;
error?: InputListError<T>;
}
type Key = string | number;
type ChangeType = 'delete' | 'create' | 'update';
export type DefaultType = { value: string };
type OnChangeEvent<T> =
| {
item: T;
type: ChangeType;
}
| {
type: 'move';
fromIndex: number;
to: number;
};
type RenderItemFunction<T> = (
item: T,
onChange: (value: T) => void,
error?: InputListError<T>
) => React.ReactNode;
interface Props<T> {
label: string;
value: T[];
onChange(value: T[], e: OnChangeEvent<T>): void;
itemBuilder?(): T;
renderItem?: RenderItemFunction<T>;
item?: ComponentType<ItemProps<T>>;
tooltip?: string;
addLabel?: string;
itemKeyGetter?(item: T, index: number): Key;
movable?: boolean;
errors?: InputListError<T>[] | string;
textTip?: string;
isAddButtonHidden?: boolean;
}
export function InputList<T = DefaultType>({
label,
value,
onChange,
itemBuilder = defaultItemBuilder as unknown as () => T,
renderItem = renderDefaultItem as unknown as RenderItemFunction<T>,
item: Item,
tooltip,
addLabel = 'Add item',
itemKeyGetter = (item: T, index: number) => index,
movable,
errors,
textTip,
isAddButtonHidden = false,
}: Props<T>) {
return (
<div className={clsx('form-group', styles.root)}>
<div className={clsx('col-sm-12', styles.header)}>
<div className={clsx('control-label text-left', styles.label)}>
{label}
{tooltip && <Tooltip message={tooltip} />}
</div>
{!isAddButtonHidden && (
<AddButton
label={addLabel}
className="space-left"
onClick={handleAdd}
/>
)}
</div>
{textTip && (
<div className="col-sm-12 my-5">
<TextTip color="blue">{textTip}</TextTip>
</div>
)}
<div className={clsx('col-sm-12', styles.items, 'space-y-4')}>
{value.map((item, index) => {
const key = itemKeyGetter(item, index);
const error = typeof errors === 'object' ? errors[index] : undefined;
return (
<div
key={key}
className={clsx(styles.itemLine, { [styles.hasError]: !!error })}
>
{Item ? (
<Item
item={item}
onChange={(value: T) => handleChangeItem(key, value)}
error={error}
/>
) : (
renderItem(
item,
(value: T) => handleChangeItem(key, value),
error
)
)}
<div className={clsx(styles.itemActions, 'items-start')}>
{movable && (
<>
<Button
size="small"
disabled={index === 0}
onClick={() => handleMoveUp(index)}
>
<i className="fa fa-arrow-up" aria-hidden="true" />
</Button>
<Button
size="small"
type="button"
disabled={index === value.length - 1}
onClick={() => handleMoveDown(index)}
>
<i className="fa fa-arrow-down" aria-hidden="true" />
</Button>
</>
)}
<Button
color="danger"
size="small"
onClick={() => handleRemoveItem(key, item)}
>
<i className="fa fa-trash" aria-hidden="true" />
</Button>
</div>
</div>
);
})}
</div>
</div>
);
function handleMoveUp(index: number) {
if (index <= 0) {
return;
}
handleMove(index, index - 1);
}
function handleMoveDown(index: number) {
if (index >= value.length - 1) {
return;
}
handleMove(index, index + 1);
}
function handleMove(from: number, to: number) {
if (!movable) {
return;
}
onChange(arrayMove(value, from, to), {
type: 'move',
fromIndex: from,
to,
});
}
function handleRemoveItem(key: Key, item: T) {
onChange(
value.filter((item, index) => {
const itemKey = itemKeyGetter(item, index);
return itemKey !== key;
}),
{ type: 'delete', item }
);
}
function handleAdd() {
const newItem = itemBuilder();
onChange([...value, newItem], { type: 'create', item: newItem });
}
function handleChangeItem(key: Key, newItemValue: T) {
const newItems = value.map((item, index) => {
const itemKey = itemKeyGetter(item, index);
if (itemKey !== key) {
return item;
}
return newItemValue;
});
onChange(newItems, {
type: 'update',
item: newItemValue,
});
}
}
function defaultItemBuilder(): DefaultType {
return { value: '' };
}
function DefaultItem({ item, onChange, error }: ItemProps<DefaultType>) {
return (
<>
<Input
value={item.value}
onChange={(e) => onChange({ value: e.target.value })}
className={styles.defaultItem}
/>
{error && <FormError>{error}</FormError>}
</>
);
}
function renderDefaultItem(
item: DefaultType,
onChange: (value: DefaultType) => void,
error?: InputListError<DefaultType>
) {
return <DefaultItem item={item} onChange={onChange} error={error} />;
}

View file

@ -0,0 +1 @@
export { InputList } from './InputList';

View file

@ -0,0 +1,23 @@
import { arrayMove } from './utils';
it('moves items in an array', () => {
expect(arrayMove(['a', 'b', 'c'], 2, 0)).toEqual(['c', 'a', 'b']);
expect(
arrayMove(
[
{ name: 'Fred' },
{ name: 'Barney' },
{ name: 'Wilma' },
{ name: 'Betty' },
],
2,
1
)
).toEqual([
{ name: 'Fred' },
{ name: 'Wilma' },
{ name: 'Barney' },
{ name: 'Betty' },
]);
expect(arrayMove([1, 2, 3], 2, 1)).toEqual([1, 3, 2]);
});

View file

@ -0,0 +1,37 @@
export function arrayMove<T>(array: Array<T>, from: number, to: number) {
if (!checkValidIndex(array, from) || !checkValidIndex(array, to)) {
throw new Error('index is out of bounds');
}
const item = array[from];
const { length } = array;
const diff = from - to;
if (diff > 0) {
// move left
return [
...array.slice(0, to),
item,
...array.slice(to, from),
...array.slice(from + 1, length),
];
}
if (diff < 0) {
// move right
const targetIndex = to + 1;
return [
...array.slice(0, from),
...array.slice(from + 1, targetIndex),
item,
...array.slice(targetIndex, length),
];
}
return [...array];
function checkValidIndex<T>(array: Array<T>, index: number) {
return index >= 0 && index <= array.length;
}
}