Folders: Migrate bulk move action to new API (#110150)
This commit is contained in:
@@ -5,31 +5,51 @@ import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { useDeleteFoldersMutation as useDeleteFoldersMutationLegacy } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import {
|
||||
useDeleteFoldersMutation as useDeleteFoldersMutationLegacy,
|
||||
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
|
||||
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
|
||||
import { useGetFolderQueryFacade, useDeleteMultipleFoldersMutationFacade } from './hooks';
|
||||
import { AnnoKeyFolder } from '../../../../features/apiserver/types';
|
||||
|
||||
import {
|
||||
useGetFolderQueryFacade,
|
||||
useDeleteMultipleFoldersMutationFacade,
|
||||
useMoveMultipleFoldersMutationFacade,
|
||||
} from './hooks';
|
||||
import { setupCreateFolder } from './test-utils';
|
||||
|
||||
import { useDeleteFolderMutation } from './index';
|
||||
import { useDeleteFolderMutation, useUpdateFolderMutation } from './index';
|
||||
|
||||
// Mocks for the hooks used inside useGetFolderQueryFacade
|
||||
jest.mock('./index', () => ({
|
||||
...jest.requireActual('./index'),
|
||||
useDeleteFolderMutation: jest.fn(),
|
||||
useUpdateFolderMutation: jest.fn(),
|
||||
}));
|
||||
|
||||
const publishMockFn = jest.fn();
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getAppEvents: jest.fn(() => ({
|
||||
publish: jest.fn(),
|
||||
publish: publishMockFn,
|
||||
})),
|
||||
}));
|
||||
const mockGetAppEvents = jest.mocked(require('@grafana/runtime').getAppEvents);
|
||||
|
||||
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
|
||||
...jest.requireActual('app/features/browse-dashboards/api/browseDashboardsAPI'),
|
||||
useDeleteFoldersMutation: jest.fn(),
|
||||
useMoveFoldersMutation: jest.fn(),
|
||||
}));
|
||||
|
||||
const dispatchMockFn = jest.fn();
|
||||
jest.mock('../../../../types/store', () => {
|
||||
return {
|
||||
...jest.requireActual('../../../../types/store'),
|
||||
useDispatch: () => dispatchMockFn,
|
||||
};
|
||||
});
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
@@ -54,9 +74,14 @@ const renderFolderHook = async () => {
|
||||
};
|
||||
|
||||
const originalToggles = { ...config.featureToggles };
|
||||
const originalAppSubUrl = String(config.appSubUrl);
|
||||
afterAll(() => {
|
||||
// Restore the original feature toggle value changed during tests
|
||||
config.featureToggles = originalToggles;
|
||||
});
|
||||
|
||||
describe('useGetFolderQueryFacade', () => {
|
||||
const originalAppSubUrl = String(config.appSubUrl);
|
||||
|
||||
beforeEach(() => {
|
||||
config.appSubUrl = '/grafana';
|
||||
});
|
||||
@@ -130,31 +155,16 @@ describe('useGetFolderQueryFacade', () => {
|
||||
});
|
||||
|
||||
describe('useDeleteMultipleFoldersMutationFacade', () => {
|
||||
const dispatchMock = jest.fn();
|
||||
const mockDeleteFolder = jest.fn(() => ({ error: undefined }));
|
||||
const mockDeleteFolderLegacy = jest.fn(() => ({ error: undefined }));
|
||||
const publishMock = jest.fn();
|
||||
|
||||
const oldToggleValue = config.featureToggles.foldersAppPlatformAPI;
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.foldersAppPlatformAPI = oldToggleValue;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeleteFolder.mockClear();
|
||||
mockDeleteFolderLegacy.mockClear();
|
||||
jest.clearAllMocks();
|
||||
(useDeleteFolderMutation as jest.Mock).mockReturnValue([mockDeleteFolder]);
|
||||
(useDeleteFoldersMutationLegacy as jest.Mock).mockReturnValue([mockDeleteFolderLegacy]);
|
||||
|
||||
// Mock useDispatch
|
||||
jest.spyOn(require('../../../../types/store'), 'useDispatch').mockReturnValue(dispatchMock);
|
||||
});
|
||||
|
||||
it('deletes multiple folders and publishes success alert', async () => {
|
||||
mockGetAppEvents.mockReturnValue({
|
||||
publish: publishMock,
|
||||
});
|
||||
config.featureToggles.foldersAppPlatformAPI = true;
|
||||
const folderUIDs = ['uid1', 'uid2'];
|
||||
const deleteFolders = useDeleteMultipleFoldersMutationFacade();
|
||||
@@ -166,13 +176,13 @@ describe('useDeleteMultipleFoldersMutationFacade', () => {
|
||||
expect(mockDeleteFolder).toHaveBeenCalledWith({ name: 'uid2' });
|
||||
|
||||
// Should publish success alert
|
||||
expect(publishMock).toHaveBeenCalledWith({
|
||||
expect(publishMockFn).toHaveBeenCalledWith({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Folder deleted'],
|
||||
});
|
||||
|
||||
// Should dispatch refreshParents
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
expect(dispatchMockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses legacy call when flag is false', async () => {
|
||||
@@ -187,6 +197,55 @@ describe('useDeleteMultipleFoldersMutationFacade', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMoveMultipleFoldersMutationFacade', () => {
|
||||
const mockUpdateFolder = jest.fn(() => ({ error: undefined }));
|
||||
const mockMoveFolders = jest.fn(() => ({ error: undefined }));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUpdateFolderMutation as jest.Mock).mockReturnValue([mockUpdateFolder]);
|
||||
(useMoveFoldersMutationLegacy as jest.Mock).mockReturnValue([mockMoveFolders]);
|
||||
});
|
||||
|
||||
it('moves multiple folders and publishes success alert', async () => {
|
||||
config.featureToggles.foldersAppPlatformAPI = true;
|
||||
const folderUIDs = ['uid1', 'uid2'];
|
||||
const [moveFolders] = useMoveMultipleFoldersMutationFacade();
|
||||
await moveFolders({ folderUIDs, destinationUID: 'uid3' });
|
||||
|
||||
// Should call deleteFolder for each UID
|
||||
expect(mockUpdateFolder).toHaveBeenCalledTimes(folderUIDs.length);
|
||||
expect(mockUpdateFolder).toHaveBeenCalledWith({
|
||||
name: 'uid1',
|
||||
patch: { metadata: { annotations: { [AnnoKeyFolder]: 'uid3' } } },
|
||||
});
|
||||
expect(mockUpdateFolder).toHaveBeenCalledWith({
|
||||
name: 'uid2',
|
||||
patch: { metadata: { annotations: { [AnnoKeyFolder]: 'uid3' } } },
|
||||
});
|
||||
|
||||
// Should publish a success alert
|
||||
expect(publishMockFn).toHaveBeenCalledWith({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Folder moved'],
|
||||
});
|
||||
|
||||
// Should dispatch refreshParents
|
||||
expect(dispatchMockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses legacy call when flag is false', async () => {
|
||||
config.featureToggles.foldersAppPlatformAPI = false;
|
||||
const folderUIDs = ['uid1', 'uid2'];
|
||||
const [moveFolders] = useMoveMultipleFoldersMutationFacade();
|
||||
await moveFolders({ folderUIDs, destinationUID: 'uid3' });
|
||||
|
||||
// Should call deleteFolder for each UID
|
||||
expect(mockMoveFolders).toHaveBeenCalledTimes(1);
|
||||
expect(mockMoveFolders).toHaveBeenCalledWith({ folderUIDs, destinationUID: 'uid3' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateFolder', () => {
|
||||
describe.each([
|
||||
// app platform
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
useGetFolderQuery as useGetFolderQueryLegacy,
|
||||
useDeleteFoldersMutation as useDeleteFoldersMutationLegacy,
|
||||
useNewFolderMutation as useLegacyNewFolderMutation,
|
||||
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
|
||||
MoveFoldersArgs,
|
||||
DeleteFoldersArgs,
|
||||
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { FolderDTO, NewFolder } from 'app/types/folders';
|
||||
@@ -38,6 +41,7 @@ import {
|
||||
useGetFolderParentsQuery,
|
||||
useDeleteFolderMutation,
|
||||
useCreateFolderMutation,
|
||||
useUpdateFolderMutation,
|
||||
Folder,
|
||||
CreateFolderApiArg,
|
||||
} from './index';
|
||||
@@ -202,29 +206,84 @@ export function useDeleteMultipleFoldersMutationFacade() {
|
||||
return deleteFolders;
|
||||
}
|
||||
|
||||
return async function deleteFolders({ folderUIDs }: { folderUIDs: string[] }) {
|
||||
return async function deleteFolders({ folderUIDs }: DeleteFoldersArgs) {
|
||||
const successMessage = t('folders.api.folder-deleted-success', 'Folder deleted');
|
||||
|
||||
// Delete all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of folderUIDs) {
|
||||
// This also shows warning alert
|
||||
if (await isProvisionedFolderCheck(dispatch, folderUID)) {
|
||||
continue;
|
||||
}
|
||||
const result = await deleteFolder({ name: folderUID });
|
||||
if (!result.error) {
|
||||
// Before this was done in backend srv automatically because the old API sent a message wiht 200 request. see
|
||||
// public/app/core/services/backend_srv.ts#L341-L361. New API does not do that so we do it here.
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('folders.api.folder-deleted-success', 'Folder deleted')],
|
||||
});
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
const isProvisioned = await isProvisionedFolderCheck(dispatch, folderUID);
|
||||
|
||||
if (!isProvisioned) {
|
||||
const result = await deleteFolder({ name: folderUID });
|
||||
if (!result.error) {
|
||||
// Before this was done in backend srv automatically because the old API sent a message wiht 200 request. see
|
||||
// public/app/core/services/backend_srv.ts#L341-L361. New API does not do that so we do it here.
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [successMessage],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
return { data: undefined };
|
||||
};
|
||||
}
|
||||
|
||||
export function useMoveMultipleFoldersMutationFacade() {
|
||||
const moveFoldersLegacyResult = useMoveFoldersMutationLegacy();
|
||||
const [updateFolder, updateFolderData] = useUpdateFolderMutation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!config.featureToggles.foldersAppPlatformAPI) {
|
||||
return moveFoldersLegacyResult;
|
||||
}
|
||||
|
||||
async function moveFolders({ folderUIDs, destinationUID }: MoveFoldersArgs) {
|
||||
const provisionedWarning = t(
|
||||
'folders.api.folder-move-error-provisioned',
|
||||
'Cannot move provisioned folder. To move it, move it in the repository and synchronise to apply the changes.'
|
||||
);
|
||||
const successMessage = t('folders.api.folder-moved-success', 'Folder moved');
|
||||
|
||||
// Move all the folders sequentially one by one
|
||||
for (const folderUID of folderUIDs) {
|
||||
// isProvisionedFolderCheck also shows a warning alert
|
||||
const isFolderProvisioned = await isProvisionedFolderCheck(dispatch, folderUID, { warning: provisionedWarning });
|
||||
|
||||
// If provisioned, we just skip this folder
|
||||
if (!isFolderProvisioned) {
|
||||
const result = await updateFolder({
|
||||
name: folderUID,
|
||||
patch: { metadata: { annotations: { [AnnoKeyFolder]: destinationUID } } },
|
||||
});
|
||||
if (!result.error) {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [successMessage],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the state of the parent folders to update the UI after folders are moved
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: destinationUID,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
|
||||
return { data: undefined };
|
||||
}
|
||||
|
||||
return [moveFolders, updateFolderData] as const;
|
||||
}
|
||||
|
||||
export function useCreateFolder() {
|
||||
const [createFolder, result] = useCreateFolderMutation();
|
||||
const legacyHook = useLegacyNewFolderMutation();
|
||||
|
||||
@@ -18,11 +18,34 @@ export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({
|
||||
// We don't want delete to invalidate getFolder tags, as that would lead to unnecessary 404s
|
||||
invalidatesTags: (result, error) => (error ? [] : [{ type: 'Folder', id: 'LIST' }]),
|
||||
},
|
||||
updateFolder: {
|
||||
query: (queryArg) => ({
|
||||
url: `/folders/${queryArg.name}`,
|
||||
method: 'PATCH',
|
||||
// We need to stringify the body and set the correct header for the call to work with k8s api.
|
||||
body: JSON.stringify(queryArg.patch),
|
||||
headers: {
|
||||
'Content-Type': 'application/strategic-merge-patch+json',
|
||||
},
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { useGetFolderQuery, useGetFolderParentsQuery, useDeleteFolderMutation, useCreateFolderMutation } =
|
||||
folderAPIv1beta1;
|
||||
export const {
|
||||
useGetFolderQuery,
|
||||
useGetFolderParentsQuery,
|
||||
useDeleteFolderMutation,
|
||||
useCreateFolderMutation,
|
||||
useUpdateFolderMutation,
|
||||
} = folderAPIv1beta1;
|
||||
|
||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
||||
export { type Folder, type FolderList, type CreateFolderApiArg } from './endpoints.gen';
|
||||
|
||||
@@ -8,7 +8,11 @@ import { useDispatch } from '../../../../types/store';
|
||||
|
||||
import { folderAPIv1beta1 as folderAPI } from './index';
|
||||
|
||||
export async function isProvisionedFolderCheck(dispatch: ReturnType<typeof useDispatch>, folderUID: string) {
|
||||
export async function isProvisionedFolderCheck(
|
||||
dispatch: ReturnType<typeof useDispatch>,
|
||||
folderUID: string,
|
||||
options?: { warning?: string }
|
||||
) {
|
||||
if (config.featureToggles.provisioning) {
|
||||
const folder = await dispatch(folderAPI.endpoints.getFolder.initiate({ name: folderUID }));
|
||||
// TODO: taken from browseDashboardAPI as it is, but this error handling should be moved up to UI code.
|
||||
@@ -16,10 +20,11 @@ export async function isProvisionedFolderCheck(dispatch: ReturnType<typeof useDi
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: [
|
||||
t(
|
||||
'folders.api.folder-delete-error-provisioned',
|
||||
'Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.'
|
||||
),
|
||||
options?.warning ||
|
||||
t(
|
||||
'folders.api.folder-delete-error-provisioned',
|
||||
'Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.'
|
||||
),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -36,6 +36,10 @@ export interface ObjectMeta {
|
||||
export const AnnoKeyCreatedBy = 'grafana.app/createdBy';
|
||||
export const AnnoKeyUpdatedTimestamp = 'grafana.app/updatedTimestamp';
|
||||
export const AnnoKeyUpdatedBy = 'grafana.app/updatedBy';
|
||||
/**
|
||||
* A name (or uid in old Grafana) of a folder the resource is contained in. Updating this will move the resource to the
|
||||
* new folder.
|
||||
*/
|
||||
export const AnnoKeyFolder = 'grafana.app/folder';
|
||||
export const AnnoKeyMessage = 'grafana.app/message';
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { t } from '@grafana/i18n';
|
||||
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { folderAPIv1beta1 as folderAPI } from 'app/api/clients/folder/v1beta1';
|
||||
import { isProvisionedFolderCheck } from 'app/api/clients/folder/v1beta1/utils';
|
||||
import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -24,10 +23,10 @@ import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/D
|
||||
import { refetchChildren, refreshParents } from '../state/actions';
|
||||
import { DashboardTreeSelection } from '../types';
|
||||
|
||||
import { isProvisionedDashboard, isProvisionedFolder } from './isProvisioned';
|
||||
import { isProvisionedDashboard } from './isProvisioned';
|
||||
import { PAGE_SIZE } from './services';
|
||||
|
||||
interface DeleteFoldersArgs {
|
||||
export interface DeleteFoldersArgs {
|
||||
folderUIDs: string[];
|
||||
}
|
||||
|
||||
@@ -35,9 +34,14 @@ interface DeleteDashboardsArgs {
|
||||
dashboardUIDs: string[];
|
||||
}
|
||||
|
||||
interface MoveItemsArgs {
|
||||
interface MoveDashboardsArgs {
|
||||
destinationUID: string;
|
||||
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
|
||||
dashboardUIDs: string[];
|
||||
}
|
||||
|
||||
export interface MoveFoldersArgs {
|
||||
destinationUID: string;
|
||||
folderUIDs: string[];
|
||||
}
|
||||
|
||||
export interface ImportInputs {
|
||||
@@ -217,37 +221,13 @@ export const browseDashboardsAPI = createApi({
|
||||
},
|
||||
}),
|
||||
|
||||
// move *multiple* items (folders and dashboards). used in the move modal.
|
||||
moveItems: builder.mutation<void, MoveItemsArgs>({
|
||||
// move *multiple* dashboards. used in the move modal.
|
||||
moveDashboards: builder.mutation<void, MoveDashboardsArgs>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
queryFn: async ({ selectedItems, destinationUID }, _api, _extraOptions, baseQuery) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
// Move all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
if (config.featureToggles.provisioning) {
|
||||
const folder = await dispatch(folderAPI.endpoints.getFolder.initiate({ name: folderUID }));
|
||||
if (isProvisionedFolder(folder.data)) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: ['Cannot move provisioned folder'],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await baseQuery({
|
||||
url: `/folders/${folderUID}/move`,
|
||||
method: 'POST',
|
||||
body: { parentUID: destinationUID },
|
||||
});
|
||||
}
|
||||
|
||||
queryFn: async ({ dashboardUIDs, destinationUID }, _api, _extraOptions, baseQuery) => {
|
||||
// Move all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
for (const dashboardUID of dashboardUIDs) {
|
||||
const fullDash = await getDashboardAPI().getDashboardDTO(dashboardUID);
|
||||
const dashboard = isDashboardV2Resource(fullDash) ? fullDash.spec : fullDash.dashboard;
|
||||
const k8s = isDashboardV2Resource(fullDash) ? fullDash.metadata : undefined;
|
||||
@@ -271,9 +251,7 @@ export const browseDashboardsAPI = createApi({
|
||||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ destinationUID, selectedItems }, { queryFulfilled, dispatch }) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
onQueryStarted: ({ destinationUID, dashboardUIDs }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
@@ -281,7 +259,47 @@ export const browseDashboardsAPI = createApi({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
dispatch(refreshParents([...selectedFolders, ...selectedDashboards]));
|
||||
dispatch(refreshParents(dashboardUIDs));
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// move *multiple* folders. used in the move modal.
|
||||
moveFolders: builder.mutation<void, MoveFoldersArgs>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
queryFn: async ({ folderUIDs, destinationUID }, _api, _extraOptions, baseQuery) => {
|
||||
// Move all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of folderUIDs) {
|
||||
if (
|
||||
await isProvisionedFolderCheck(dispatch, folderUID, {
|
||||
warning: t(
|
||||
'folders.api.folder-move-error-provisioned',
|
||||
'Cannot move provisioned folder. To move it, move it in the repository and synchronise to apply the changes.'
|
||||
),
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await baseQuery({
|
||||
url: `/folders/${folderUID}/move`,
|
||||
method: 'POST',
|
||||
body: { parentUID: destinationUID },
|
||||
});
|
||||
}
|
||||
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ destinationUID, folderUIDs }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: destinationUID,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -499,7 +517,8 @@ export const {
|
||||
useGetFolderQuery,
|
||||
useLazyGetFolderQuery,
|
||||
useMoveFolderMutation,
|
||||
useMoveItemsMutation,
|
||||
useMoveDashboardsMutation,
|
||||
useMoveFoldersMutation,
|
||||
useNewFolderMutation,
|
||||
useSaveDashboardMutation,
|
||||
useSaveFolderMutation,
|
||||
|
||||
@@ -13,8 +13,11 @@ import { ShowModalReactEvent } from 'app/types/events';
|
||||
import { FolderDTO } from 'app/types/folders';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { useDeleteMultipleFoldersMutationFacade } from '../../../../api/clients/folder/v1beta1/hooks';
|
||||
import { useDeleteDashboardsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI';
|
||||
import {
|
||||
useDeleteMultipleFoldersMutationFacade,
|
||||
useMoveMultipleFoldersMutationFacade,
|
||||
} from '../../../../api/clients/folder/v1beta1/hooks';
|
||||
import { useDeleteDashboardsMutation, useMoveDashboardsMutation } from '../../api/browseDashboardsAPI';
|
||||
import { useActionSelectionState } from '../../state/hooks';
|
||||
import { setAllSelection } from '../../state/slice';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
@@ -35,7 +38,8 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
const selectedItems = useActionSelectionState();
|
||||
const [deleteDashboards] = useDeleteDashboardsMutation();
|
||||
const deleteFolders = useDeleteMultipleFoldersMutationFacade();
|
||||
const [moveItems] = useMoveItemsMutation();
|
||||
const [moveFolders] = useMoveMultipleFoldersMutationFacade();
|
||||
const [moveDashboards] = useMoveDashboardsMutation();
|
||||
const [, stateManager] = useSearchStateManager();
|
||||
const provisioningEnabled = config.featureToggles.provisioning;
|
||||
|
||||
@@ -65,7 +69,11 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
};
|
||||
|
||||
const onMove = async (destinationUID: string) => {
|
||||
await moveItems({ selectedItems, destinationUID });
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
await moveFolders({ folderUIDs: selectedFolders, destinationUID });
|
||||
await moveDashboards({ dashboardUIDs: selectedDashboards, destinationUID });
|
||||
trackAction('move', selectedItems);
|
||||
onActionComplete();
|
||||
};
|
||||
|
||||
@@ -7566,7 +7566,9 @@
|
||||
"folders": {
|
||||
"api": {
|
||||
"folder-delete-error-provisioned": "Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.",
|
||||
"folder-deleted-success": "Folder deleted"
|
||||
"folder-deleted-success": "Folder deleted",
|
||||
"folder-move-error-provisioned": "Cannot move provisioned folder. To move it, move it in the repository and synchronise to apply the changes.",
|
||||
"folder-moved-success": "Folder moved"
|
||||
},
|
||||
"get-loading-nav": {
|
||||
"main": {
|
||||
|
||||
Reference in New Issue
Block a user