mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 08:19:40 +02:00
refactor(app): migrate env var form section [EE-6232] (#10499)
* refactor(app): migrate env var form section [EE-6232] * allow undoing delete in inputlist
This commit is contained in:
parent
6228314e3c
commit
488393007f
16 changed files with 274 additions and 209 deletions
|
@ -12,7 +12,7 @@ const meta: Meta = {
|
|||
|
||||
export default meta;
|
||||
|
||||
export { Defaults, ListWithInputAndSelect };
|
||||
export { Defaults, ListWithInputAndSelect, ListWithUndoDeletion };
|
||||
|
||||
function Defaults() {
|
||||
const [values, setValues] = useState<DefaultType[]>([{ value: '' }]);
|
||||
|
@ -26,6 +26,21 @@ function Defaults() {
|
|||
);
|
||||
}
|
||||
|
||||
function ListWithUndoDeletion() {
|
||||
const [values, setValues] = useState<DefaultType[]>([
|
||||
{ value: 'Existing item', needsDeletion: false },
|
||||
]);
|
||||
|
||||
return (
|
||||
<InputList
|
||||
label="List with undo deletion"
|
||||
value={values}
|
||||
onChange={(value) => setValues(value)}
|
||||
canUndoDelete
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListWithSelectItem {
|
||||
value: number;
|
||||
select: string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ComponentType } from 'react';
|
||||
import { ComponentType, useRef } from 'react';
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, Plus, RotateCw, Trash2 } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
@ -9,7 +10,7 @@ import { TextTip } from '@@/Tip/TextTip';
|
|||
import { Input } from '../Input';
|
||||
import { FormError } from '../FormError';
|
||||
|
||||
import { arrayMove } from './utils';
|
||||
import { arrayMove, hasKey } from './utils';
|
||||
|
||||
type ArrElement<ArrType> = ArrType extends readonly (infer ElementType)[]
|
||||
? ElementType
|
||||
|
@ -30,10 +31,12 @@ export interface ItemProps<T> {
|
|||
readOnly?: boolean;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
index: number;
|
||||
needsDeletion?: boolean;
|
||||
}
|
||||
type Key = string | number;
|
||||
type ChangeType = 'delete' | 'create' | 'update';
|
||||
export type DefaultType = { value: string };
|
||||
export type DefaultType = { value: string; needsDeletion?: boolean };
|
||||
type CanUndoDeleteItem<T> = T & { needsDeletion: boolean };
|
||||
|
||||
type OnChangeEvent<T> =
|
||||
| {
|
||||
|
@ -64,6 +67,7 @@ interface Props<T> {
|
|||
addLabel?: string;
|
||||
itemKeyGetter?(item: T, index: number): Key;
|
||||
movable?: boolean;
|
||||
canUndoDelete?: boolean;
|
||||
errors?: ArrayError<T[]>;
|
||||
textTip?: string;
|
||||
isAddButtonHidden?: boolean;
|
||||
|
@ -83,6 +87,7 @@ export function InputList<T = DefaultType>({
|
|||
addLabel = 'Add item',
|
||||
itemKeyGetter = (item: T, index: number) => index,
|
||||
movable,
|
||||
canUndoDelete = false,
|
||||
errors,
|
||||
textTip,
|
||||
isAddButtonHidden = false,
|
||||
|
@ -90,6 +95,7 @@ export function InputList<T = DefaultType>({
|
|||
readOnly,
|
||||
'aria-label': ariaLabel,
|
||||
}: Props<T>) {
|
||||
const initialItemsCount = useRef(value.length);
|
||||
const isAddButtonVisible = !(isAddButtonHidden || readOnly);
|
||||
return (
|
||||
<div className="form-group" aria-label={ariaLabel || label}>
|
||||
|
@ -154,7 +160,7 @@ export function InputList<T = DefaultType>({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{!readOnly && (
|
||||
{!readOnly && !canUndoDelete && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
|
@ -163,6 +169,17 @@ export function InputList<T = DefaultType>({
|
|||
icon={Trash2}
|
||||
/>
|
||||
)}
|
||||
{!readOnly &&
|
||||
canUndoDelete &&
|
||||
hasKey(item, 'needsDeletion') && (
|
||||
<CanUndoDeleteButton
|
||||
item={{ ...item, needsDeletion: !!item.needsDeletion }}
|
||||
itemIndex={index}
|
||||
initialItemsCount={initialItemsCount.current}
|
||||
handleRemoveItem={handleRemoveItem}
|
||||
handleToggleNeedsDeletion={handleToggleNeedsDeletion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -224,6 +241,10 @@ export function InputList<T = DefaultType>({
|
|||
);
|
||||
}
|
||||
|
||||
function handleToggleNeedsDeletion(key: Key, item: CanUndoDeleteItem<T>) {
|
||||
handleChangeItem(key, { ...item, needsDeletion: !item.needsDeletion });
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
const newItem = itemBuilder();
|
||||
onChange([...value, newItem], { type: 'create', item: newItem });
|
||||
|
@ -260,8 +281,8 @@ function DefaultItem({
|
|||
<Input
|
||||
value={item.value}
|
||||
onChange={(e) => onChange({ value: e.target.value })}
|
||||
className="!w-full"
|
||||
disabled={disabled}
|
||||
className={clsx('!w-full', item.needsDeletion && 'striked')}
|
||||
disabled={disabled || item.needsDeletion}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{error && <FormError>{error}</FormError>}
|
||||
|
@ -279,3 +300,51 @@ function renderDefaultItem(
|
|||
<DefaultItem item={item} onChange={onChange} error={error} index={index} />
|
||||
);
|
||||
}
|
||||
|
||||
type CanUndoDeleteButtonProps<T> = {
|
||||
item: CanUndoDeleteItem<T>;
|
||||
itemIndex: number;
|
||||
initialItemsCount: number;
|
||||
handleRemoveItem(key: Key, item: T): void;
|
||||
handleToggleNeedsDeletion(key: Key, item: T): void;
|
||||
};
|
||||
|
||||
function CanUndoDeleteButton<T>({
|
||||
item,
|
||||
itemIndex,
|
||||
initialItemsCount,
|
||||
handleRemoveItem,
|
||||
handleToggleNeedsDeletion,
|
||||
}: CanUndoDeleteButtonProps<T>) {
|
||||
return (
|
||||
<div className="items-start">
|
||||
{!item.needsDeletion && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={handleDeleteClick}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
/>
|
||||
)}
|
||||
{item.needsDeletion && (
|
||||
<Button
|
||||
color="default"
|
||||
size="medium"
|
||||
onClick={handleDeleteClick}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={RotateCw}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// if the item is new, we can just remove it, otherwise we need to toggle the needsDeletion flag
|
||||
function handleDeleteClick() {
|
||||
if (itemIndex < initialItemsCount) {
|
||||
handleToggleNeedsDeletion(itemIndex, item);
|
||||
} else {
|
||||
handleRemoveItem(itemIndex, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,3 +35,14 @@ export function arrayMove<T>(array: Array<T>, from: number, to: number) {
|
|||
return index >= 0 && index <= array.length;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasKey(
|
||||
value: unknown,
|
||||
key: string | number | symbol
|
||||
): value is { needsDeletion: boolean } {
|
||||
return isObject(value) && key in value;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is object {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue