mirror of
https://github.com/grafana/grafana.git
synced 2024-12-30 10:47:30 -06:00
NestedFolderPicker: Add clearable
prop (#81114)
* add clearable prop to NestedFolderPicker * update types
This commit is contained in:
parent
7872a128a2
commit
3f1e97cb07
@ -113,6 +113,13 @@ describe('NestedFolderPicker', () => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title);
|
||||
});
|
||||
|
||||
it('can clear a selection if clearable is specified', async () => {
|
||||
render(<NestedFolderPicker clearable value={folderA.item.uid} onChange={mockOnChange} />);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Clear selection' }));
|
||||
expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined);
|
||||
});
|
||||
|
||||
it('can select a folder from the picker with the keyboard', async () => {
|
||||
render(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
|
@ -42,7 +42,10 @@ export interface NestedFolderPickerProps {
|
||||
excludeUIDs?: string[];
|
||||
|
||||
/* Callback for when the user selects a folder */
|
||||
onChange?: (folderUID: string, folderName: string) => void;
|
||||
onChange?: (folderUID: string | undefined, folderName: string | undefined) => void;
|
||||
|
||||
/* Whether the picker should be clearable */
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const];
|
||||
@ -64,6 +67,7 @@ export function NestedFolderPicker({
|
||||
value,
|
||||
invalid,
|
||||
showRootFolder = true,
|
||||
clearable = false,
|
||||
excludeUIDs,
|
||||
onChange,
|
||||
}: NestedFolderPickerProps) {
|
||||
@ -158,6 +162,17 @@ export function NestedFolderPicker({
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleClearSelection = useCallback(
|
||||
(event: React.MouseEvent<SVGElement> | React.KeyboardEvent<SVGElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onChange) {
|
||||
onChange(undefined, undefined);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleCloseOverlay = useCallback(() => setOverlayOpen(false), [setOverlayOpen]);
|
||||
|
||||
const baseHandleLoadMore = useLoadNextChildrenPage(EXCLUDED_KINDS);
|
||||
@ -259,6 +274,7 @@ export function NestedFolderPicker({
|
||||
return (
|
||||
<Trigger
|
||||
label={label}
|
||||
handleClearSelection={clearable && value !== undefined ? handleClearSelection : undefined}
|
||||
invalid={invalid}
|
||||
isLoading={selectedFolder.isLoading}
|
||||
autoFocus={autoFocusButton}
|
||||
|
@ -4,19 +4,29 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, getInputStyles, useTheme2, Text } from '@grafana/ui';
|
||||
import { focusCss } from '@grafana/ui/src/themes/mixins';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { focusCss, getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
interface TriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
isLoading: boolean;
|
||||
handleClearSelection?: (event: React.MouseEvent<SVGElement> | React.KeyboardEvent<SVGElement>) => void;
|
||||
invalid?: boolean;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
||||
function Trigger(
|
||||
{ handleClearSelection, isLoading, invalid, label, ...rest }: TriggerProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>
|
||||
) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, invalid);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<SVGElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleClearSelection?.(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.inputWrapper}>
|
||||
@ -41,6 +51,18 @@ function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: Reac
|
||||
<Trans i18nKey="browse-dashboards.folder-picker.button-label">Select folder</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isLoading && handleClearSelection && (
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('browse-dashboards.folder-picker.clear-selection', 'Clear selection')}
|
||||
className={styles.clearIcon}
|
||||
name="times"
|
||||
onClick={handleClearSelection}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className={styles.suffix}>
|
||||
@ -92,11 +114,26 @@ const getStyles = (theme: GrafanaTheme2, invalid = false) => {
|
||||
'&:focus-visible': css`
|
||||
${focusCss(theme)}
|
||||
`,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: 28,
|
||||
},
|
||||
]),
|
||||
|
||||
hasPrefix: css({
|
||||
paddingLeft: 28,
|
||||
}),
|
||||
|
||||
clearIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
},
|
||||
'&:focus:not(:focus-visible)': getMouseFocusStyles(theme),
|
||||
'&:focus-visible': getFocusStyles(theme),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -90,7 +90,7 @@ export class GeneralSettingsEditView
|
||||
this._dashboard.setState({ tags: value });
|
||||
};
|
||||
|
||||
public onFolderChange = (newUID: string, newTitle: string) => {
|
||||
public onFolderChange = (newUID: string | undefined, newTitle: string | undefined) => {
|
||||
const newMeta = {
|
||||
...this._dashboard.state.meta,
|
||||
folderUid: newUID || this._dashboard.state.meta.folderUid,
|
||||
|
@ -45,7 +45,7 @@ export function GeneralSettingsUnconnected({
|
||||
const [dashboardDescription, setDashboardDescription] = useState(dashboard.description);
|
||||
const pageNav = config.featureToggles.dockedMegaMenu ? sectionNav.node.parentItem : undefined;
|
||||
|
||||
const onFolderChange = (newUID: string, newTitle: string) => {
|
||||
const onFolderChange = (newUID: string | undefined, newTitle: string | undefined) => {
|
||||
dashboard.meta.folderUid = newUID;
|
||||
dashboard.meta.folderTitle = newTitle;
|
||||
dashboard.meta.hasUnsavedFolderChange = true;
|
||||
|
@ -167,7 +167,7 @@ export const SaveDashboardAsForm = ({
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<FolderPicker
|
||||
{...field}
|
||||
onChange={(uid: string, title: string) => field.onChange({ uid, title })}
|
||||
onChange={(uid: string | undefined, title: string | undefined) => field.onChange({ uid, title })}
|
||||
value={field.value?.uid}
|
||||
// Old folder picker fields
|
||||
initialTitle={dashboard.meta.folderTitle}
|
||||
|
@ -57,7 +57,7 @@ export const plugin = new PanelPlugin<Options>(DashList)
|
||||
id: 'folderUID',
|
||||
defaultValue: undefined,
|
||||
editor: function RenderFolderPicker({ value, onChange }) {
|
||||
return <FolderPicker value={value} onChange={(folderUID) => onChange(folderUID)} />;
|
||||
return <FolderPicker clearable value={value} onChange={(folderUID) => onChange(folderUID)} />;
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
|
@ -76,6 +76,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "Ordner auswählen: {{ label }} aktuell ausgewählt",
|
||||
"button-label": "Ordner auswählen",
|
||||
"clear-selection": "",
|
||||
"empty-message": "Keine Ordner gefunden",
|
||||
"error-title": "Fehler beim Laden der Ordner",
|
||||
"search-placeholder": "Ordner suchen",
|
||||
|
@ -76,6 +76,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "Select folder: {{ label }} currently selected",
|
||||
"button-label": "Select folder",
|
||||
"clear-selection": "Clear selection",
|
||||
"empty-message": "No folders found",
|
||||
"error-title": "Error loading folders",
|
||||
"search-placeholder": "Search folders",
|
||||
|
@ -81,6 +81,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "Seleccionar carpeta: {{ label }} seleccionada actualmente",
|
||||
"button-label": "Seleccionar carpeta",
|
||||
"clear-selection": "",
|
||||
"empty-message": "No se ha encontrado ninguna carpeta",
|
||||
"error-title": "Error al cargar las carpetas",
|
||||
"search-placeholder": "Buscar carpetas",
|
||||
|
@ -81,6 +81,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "Sélectionnez un dossier : {{ label }} est sélectionné actuellement",
|
||||
"button-label": "Sélectionner un dossier",
|
||||
"clear-selection": "",
|
||||
"empty-message": "Aucun dossier trouvé",
|
||||
"error-title": "Impossible de charger les dossiers",
|
||||
"search-placeholder": "Rechercher dans les dossiers",
|
||||
|
@ -76,6 +76,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "Ŝęľęčŧ ƒőľđęř: {{ label }} čūřřęʼnŧľy şęľęčŧęđ",
|
||||
"button-label": "Ŝęľęčŧ ƒőľđęř",
|
||||
"clear-selection": "Cľęäř şęľęčŧįőʼn",
|
||||
"empty-message": "Ńő ƒőľđęřş ƒőūʼnđ",
|
||||
"error-title": "Ēřřőř ľőäđįʼnģ ƒőľđęřş",
|
||||
"search-placeholder": "Ŝęäřčĥ ƒőľđęřş",
|
||||
|
@ -71,6 +71,7 @@
|
||||
"folder-picker": {
|
||||
"accessible-label": "选择文件夹:{{ label }} 当前已选择",
|
||||
"button-label": "选择文件夹",
|
||||
"clear-selection": "",
|
||||
"empty-message": "未找到文件夹",
|
||||
"error-title": "加载文件夹时出错",
|
||||
"search-placeholder": "搜索文件夹",
|
||||
|
Loading…
Reference in New Issue
Block a user