1
0
Fork 0
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:
Ali 2024-01-03 08:17:54 +13:00 committed by GitHub
parent 6228314e3c
commit 488393007f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 209 deletions

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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;
}