|
@@ -1,126 +1,147 @@
|
|
|
import React from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
-import pickBy from 'lodash/pickBy';
|
|
|
-import PropTypes from 'prop-types';
|
|
|
|
|
|
import Button from 'app/components/button';
|
|
|
import Confirm from 'app/components/confirm';
|
|
|
import DropdownAutoComplete from 'app/components/dropdownAutoComplete';
|
|
|
+import {Item as ListItem} from 'app/components/dropdownAutoComplete/types';
|
|
|
import DropdownButton from 'app/components/dropdownButton';
|
|
|
import {IconAdd, IconDelete, IconSettings} from 'app/icons';
|
|
|
import {t} from 'app/locale';
|
|
|
import InputField from 'app/views/settings/components/forms/inputField';
|
|
|
|
|
|
-const RichListProps = {
|
|
|
+type ConfirmProps = Partial<React.ComponentProps<typeof Confirm>>;
|
|
|
+type DropdownProps = Omit<React.ComponentProps<typeof DropdownAutoComplete>, 'children'>;
|
|
|
+
|
|
|
+type UpdatedItem = ListItem | Record<string, string>;
|
|
|
+
|
|
|
+type DefaultProps = {
|
|
|
/**
|
|
|
* Text used for the add item button.
|
|
|
*/
|
|
|
- addButtonText: PropTypes.node,
|
|
|
+ addButtonText: string;
|
|
|
|
|
|
/**
|
|
|
- * Configuration for the add item dropdown.
|
|
|
+ * Callback invoked when an item is added via the dropdown menu.
|
|
|
+ *
|
|
|
+ * The callback is expected to call `addItem(item)`
|
|
|
*/
|
|
|
- addDropdown: PropTypes.object.isRequired,
|
|
|
+ onAddItem: RichListCallback;
|
|
|
|
|
|
+ /**
|
|
|
+ * Callback invoked when an item is removed.
|
|
|
+ *
|
|
|
+ * The callback is expected to call `removeItem(item)`
|
|
|
+ */
|
|
|
+ onRemoveItem: RichListCallback;
|
|
|
+};
|
|
|
+
|
|
|
+const defaultProps: DefaultProps = {
|
|
|
+ addButtonText: t('Add item'),
|
|
|
+ onAddItem: (item, addItem) => addItem(item),
|
|
|
+ onRemoveItem: (item, removeItem) => removeItem(item),
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * You can get better typing by specifying the item type
|
|
|
+ * when using this component.
|
|
|
+ *
|
|
|
+ * The callback parameter accepts a more general type than `ListItem` as the
|
|
|
+ * callback handler can perform arbitrary logic and extend the payload in
|
|
|
+ * ways that are hard to type.
|
|
|
+ */
|
|
|
+export type RichListCallback = (
|
|
|
+ item: ListItem,
|
|
|
+ callback: (item: UpdatedItem) => void
|
|
|
+) => void;
|
|
|
+
|
|
|
+export type RichListProps = {
|
|
|
/**
|
|
|
* Render function to render an item.
|
|
|
*/
|
|
|
- renderItem: PropTypes.func,
|
|
|
+ renderItem: (item: ListItem) => React.ReactNode;
|
|
|
|
|
|
/**
|
|
|
- * Callback invoked when an item is added via the dropdown menu.
|
|
|
+ * The list of items to render.
|
|
|
*/
|
|
|
- onAddItem: PropTypes.func,
|
|
|
+ value: ListItem[];
|
|
|
+
|
|
|
+ onBlur: InputField['props']['onBlur'];
|
|
|
+ onChange: InputField['props']['onChange'];
|
|
|
|
|
|
/**
|
|
|
- * Callback invoked when an item is interacted with.
|
|
|
+ * Configuration for the add item dropdown.
|
|
|
*/
|
|
|
- onEditItem: PropTypes.func,
|
|
|
+ addDropdown: DropdownProps;
|
|
|
|
|
|
/**
|
|
|
- * Callback invoked when an item is removed.
|
|
|
+ * Disables all controls in the rich list.
|
|
|
*/
|
|
|
- onRemoveItem: PropTypes.func,
|
|
|
+ disabled: boolean;
|
|
|
|
|
|
/**
|
|
|
* Properties for the confirm remove dialog. If missing, the item will be
|
|
|
* removed immediately.
|
|
|
*/
|
|
|
- removeConfirm: PropTypes.object,
|
|
|
-};
|
|
|
-
|
|
|
-function getDefinedProps(propTypes, props) {
|
|
|
- return pickBy(props, (_prop, key) => key in propTypes);
|
|
|
-}
|
|
|
+ removeConfirm?: ConfirmProps;
|
|
|
|
|
|
-class RichList extends React.PureComponent {
|
|
|
- static propTypes = {
|
|
|
- ...RichListProps,
|
|
|
-
|
|
|
- /**
|
|
|
- * Disables all controls in the rich list.
|
|
|
- */
|
|
|
- disabled: PropTypes.bool,
|
|
|
-
|
|
|
- /**
|
|
|
- * The list of items to render.
|
|
|
- */
|
|
|
- value: PropTypes.array.isRequired,
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * Callback invoked when an item is interacted with.
|
|
|
+ *
|
|
|
+ * The callback is expected to call `editItem(item)`
|
|
|
+ */
|
|
|
+ onEditItem?: RichListCallback;
|
|
|
+} & DefaultProps;
|
|
|
|
|
|
- static defaultProps = {
|
|
|
- addButtonText: t('Add Item'),
|
|
|
- renderItem: item => item,
|
|
|
- onAddItem: (item, addItem) => addItem(item),
|
|
|
- onRemoveItem: (item, removeItem) => removeItem(item),
|
|
|
- };
|
|
|
+class RichList extends React.PureComponent<RichListProps> {
|
|
|
+ static defaultProps = defaultProps;
|
|
|
|
|
|
- triggerChange = items => {
|
|
|
+ triggerChange = (items: UpdatedItem[]) => {
|
|
|
if (!this.props.disabled) {
|
|
|
- this.props.onChange(items, {});
|
|
|
- this.props.onBlur(items, {});
|
|
|
+ this.props.onChange?.(items, {});
|
|
|
+ this.props.onBlur?.(items, {});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- addItem = data => {
|
|
|
+ addItem = (data: UpdatedItem) => {
|
|
|
const items = [...this.props.value, data];
|
|
|
this.triggerChange(items);
|
|
|
};
|
|
|
|
|
|
- updateItem = (data, index) => {
|
|
|
- const items = [...this.props.value];
|
|
|
+ updateItem = (data: UpdatedItem, index: number) => {
|
|
|
+ const items = [...this.props.value] as UpdatedItem[];
|
|
|
items.splice(index, 1, data);
|
|
|
this.triggerChange(items);
|
|
|
};
|
|
|
|
|
|
- removeItem = index => {
|
|
|
+ removeItem = (index: number) => {
|
|
|
const items = [...this.props.value];
|
|
|
items.splice(index, 1);
|
|
|
this.triggerChange(items);
|
|
|
};
|
|
|
|
|
|
- onSelectDropdownItem = item => {
|
|
|
- if (!this.props.disabled) {
|
|
|
+ onSelectDropdownItem = (item: ListItem) => {
|
|
|
+ if (!this.props.disabled && this.props.onAddItem) {
|
|
|
this.props.onAddItem(item, this.addItem);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- onEditItem = (item, index) => {
|
|
|
- if (!this.props.disabled) {
|
|
|
+ onEditItem = (item: ListItem, index: number) => {
|
|
|
+ if (!this.props.disabled && this.props.onEditItem) {
|
|
|
this.props.onEditItem(item, data => this.updateItem(data, index));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- onRemoveItem = (item, index) => {
|
|
|
+ onRemoveItem = (item: ListItem, index: number) => {
|
|
|
if (!this.props.disabled) {
|
|
|
this.props.onRemoveItem(item, () => this.removeItem(index));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- renderItem = (item, index) => {
|
|
|
+ renderItem = (item: ListItem, index: number) => {
|
|
|
const {disabled} = this.props;
|
|
|
|
|
|
- const removeIcon = (onClick = null) => (
|
|
|
+ const removeIcon = (onClick?: () => void) => (
|
|
|
<ItemButton
|
|
|
onClick={onClick}
|
|
|
disabled={disabled}
|
|
@@ -194,31 +215,31 @@ class RichList extends React.PureComponent {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export default class RichListField extends React.PureComponent {
|
|
|
- static propTypes = {
|
|
|
- // TODO(ts)
|
|
|
- // ...InputField.propTypes,
|
|
|
- ...RichListProps,
|
|
|
- };
|
|
|
-
|
|
|
- renderRichList = fieldProps => {
|
|
|
- const richListProps = getDefinedProps(RichListProps, this.props);
|
|
|
- const {value, ...props} = fieldProps;
|
|
|
-
|
|
|
- // We must not render this field until `setValue` has been applied by the
|
|
|
- // model, which is done after the field is mounted for the first time. To
|
|
|
- // check this, we cannot use Array.isArray because the value passed in by
|
|
|
- // the model might actually be an ObservableArray.
|
|
|
- if (typeof value === 'string' || value.length === undefined) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return <RichList {...props} value={[...value]} {...richListProps} />;
|
|
|
- };
|
|
|
-
|
|
|
- render() {
|
|
|
- return <InputField {...this.props} field={this.renderRichList} />;
|
|
|
- }
|
|
|
+/**
|
|
|
+ * A 'rich' dropdown that provides action hooks for when item
|
|
|
+ * are selected/created/removed.
|
|
|
+ *
|
|
|
+ * An example usage is the debug image selector where each 'source' option
|
|
|
+ * requires additional configuration data.
|
|
|
+ */
|
|
|
+export default function RichListField(props: RichListProps & InputField['props']) {
|
|
|
+ return (
|
|
|
+ <InputField
|
|
|
+ {...props}
|
|
|
+ field={(fieldProps: RichListProps) => {
|
|
|
+ const {value, ...otherProps} = fieldProps;
|
|
|
+
|
|
|
+ // We must not render this field until `setValue` has been applied by the
|
|
|
+ // model, which is done after the field is mounted for the first time. To
|
|
|
+ // check this, we cannot use Array.isArray because the value passed in by
|
|
|
+ // the model might actually be an ObservableArray.
|
|
|
+ if (typeof value === 'string' || value?.length === undefined) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return <RichList {...otherProps} value={[...value]} />;
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
const ItemList = styled('ul')`
|
|
@@ -228,7 +249,7 @@ const ItemList = styled('ul')`
|
|
|
padding: 0;
|
|
|
`;
|
|
|
|
|
|
-const Item = styled('li')`
|
|
|
+const Item = styled('li')<{disabled?: boolean}>`
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
background-color: ${p => p.theme.button.default.background};
|