mirror of
https://github.com/apache/superset.git
synced 2026-07-09 00:05:36 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d3a16e26 | ||
|
|
569cfc9825 |
@@ -47,3 +47,11 @@ test('should pass removeToast to the Toast component', async () => {
|
||||
fireEvent.click(getAllByTestId('close-button')[0]);
|
||||
await waitFor(() => expect(removeToast).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test('presenter container does not capture pointer events over its empty area', () => {
|
||||
const { container } = setup();
|
||||
const presenter = container.querySelector('#toast-presenter');
|
||||
// The container spans a tall fixed region; its empty area must let clicks
|
||||
// pass through to underlying controls (e.g. the Explore Save button).
|
||||
expect(presenter).toHaveStyle('pointer-events: none');
|
||||
});
|
||||
|
||||
@@ -39,6 +39,11 @@ const StyledToastPresenter = styled.div<VisualProps>(
|
||||
|
||||
height: calc(100vh - 100px);
|
||||
|
||||
/* The container spans a tall region on the right edge of the screen.
|
||||
Let clicks pass through its empty area so it doesn't block underlying
|
||||
controls (e.g. the Explore Save button); toasts re-enable pointer events. */
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: ${position === 'bottom' ? 'column-reverse' : 'column'};
|
||||
align-items: stretch;
|
||||
@@ -107,6 +112,8 @@ const StyledToastPresenter = styled.div<VisualProps>(
|
||||
transition:
|
||||
transform ${theme.motionDurationMid},
|
||||
opacity ${theme.motionDurationMid};
|
||||
/* Off-screen/hidden toasts must not capture clicks; only visible ones do. */
|
||||
pointer-events: none;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -125,6 +132,7 @@ const StyledToastPresenter = styled.div<VisualProps>(
|
||||
.toast--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -178,7 +178,7 @@ export const hydrateDashboard =
|
||||
viz_type: slice.form_data.viz_type,
|
||||
datasource: slice.form_data.datasource,
|
||||
description: slice.description,
|
||||
description_markeddown: slice.description_markeddown,
|
||||
description_markdown: slice.description_markeddown,
|
||||
owners: slice.owners,
|
||||
modified: slice.modified,
|
||||
changed_on: new Date(slice.changed_on).getTime(),
|
||||
|
||||
@@ -265,6 +265,14 @@ test('disables overwrite option for non-owner', () => {
|
||||
expect(getByRole('radio', { name: 'Save (Overwrite)' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('enables and selects overwrite option for owner', () => {
|
||||
// Owners is a normalized array of user IDs, matching the current user
|
||||
const { getByRole } = setup();
|
||||
const overwriteRadio = getByRole('radio', { name: 'Save (Overwrite)' });
|
||||
expect(overwriteRadio).toBeEnabled();
|
||||
expect(overwriteRadio).toBeChecked();
|
||||
});
|
||||
|
||||
test('updates slice name and selected dashboard', async () => {
|
||||
const dashboardId = mockEvent.value;
|
||||
const saveDataset = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import exploreReducer, { ExploreState } from './exploreReducer';
|
||||
import { setStashFormData } from '../actions/exploreActions';
|
||||
import { setStashFormData, sliceUpdated } from '../actions/exploreActions';
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
|
||||
test('reset hiddenFormData on SET_STASH_FORM_DATA', () => {
|
||||
@@ -52,3 +52,49 @@ test('skips updates when the field is already updated on SET_STASH_FORM_DATA', (
|
||||
const newState = exploreReducer(initialState, restoreAction);
|
||||
expect(newState).toBe(initialState);
|
||||
});
|
||||
|
||||
test('normalizes API owner objects to id array on SLICE_UPDATED', () => {
|
||||
const initialState: ExploreState = {
|
||||
form_data: {} as unknown as QueryFormData,
|
||||
controls: {},
|
||||
slice: { slice_id: 1, owners: [1] },
|
||||
} as ExploreState;
|
||||
// The chart API returns owners as objects: {id, first_name, last_name}
|
||||
const action = sliceUpdated({
|
||||
slice_id: 1,
|
||||
slice_name: 'My chart',
|
||||
owners: [{ id: 5, first_name: 'Superset', last_name: 'Admin' }],
|
||||
} as never) as Parameters<typeof exploreReducer>[1];
|
||||
const newState = exploreReducer(initialState, action);
|
||||
expect(newState.slice?.owners).toEqual([5]);
|
||||
expect(newState.metadata?.owners).toEqual(['Superset Admin']);
|
||||
});
|
||||
|
||||
test('normalizes select control owners ({value, label}) on SLICE_UPDATED', () => {
|
||||
const initialState: ExploreState = {
|
||||
form_data: {} as unknown as QueryFormData,
|
||||
controls: {},
|
||||
slice: { slice_id: 1, owners: [1] },
|
||||
} as ExploreState;
|
||||
const action = sliceUpdated({
|
||||
slice_id: 1,
|
||||
owners: [{ value: 7, label: 'Jane Doe' }],
|
||||
} as never) as Parameters<typeof exploreReducer>[1];
|
||||
const newState = exploreReducer(initialState, action);
|
||||
expect(newState.slice?.owners).toEqual([7]);
|
||||
expect(newState.metadata?.owners).toEqual(['Jane Doe']);
|
||||
});
|
||||
|
||||
test('keeps numeric owner ids on SLICE_UPDATED', () => {
|
||||
const initialState: ExploreState = {
|
||||
form_data: {} as unknown as QueryFormData,
|
||||
controls: {},
|
||||
slice: { slice_id: 1, owners: [1] },
|
||||
} as ExploreState;
|
||||
const action = sliceUpdated({
|
||||
slice_id: 1,
|
||||
owners: [3, 4],
|
||||
} as never) as Parameters<typeof exploreReducer>[1];
|
||||
const newState = exploreReducer(initialState, action);
|
||||
expect(newState.slice?.owners).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
@@ -152,9 +152,13 @@ interface SetStashFormDataAction {
|
||||
isHidden: boolean;
|
||||
}
|
||||
|
||||
// Owner can be either a number (user ID) or an object with value/label
|
||||
// This handles both Slice format (number[]) and select control format ({value, label}[])
|
||||
type OwnerItem = number | { value: number; label: string };
|
||||
// Owner can be a number (user ID), a select control option ({value, label}),
|
||||
// or an API entity object ({id, first_name, last_name}). This handles the Slice
|
||||
// format (number[]), select control format, and the raw chart API response format.
|
||||
type OwnerItem =
|
||||
| number
|
||||
| { value: number; label: string }
|
||||
| { id: number; first_name?: string; last_name?: string };
|
||||
|
||||
interface SliceUpdatedAction {
|
||||
type: typeof actions.SLICE_UPDATED;
|
||||
@@ -601,11 +605,20 @@ export default function exploreReducer(
|
||||
},
|
||||
[actions.SLICE_UPDATED]() {
|
||||
const typedAction = action as SliceUpdatedAction;
|
||||
// Handle owners that can be either number[] or Array<{value, label}>
|
||||
const getOwnerId = (owner: OwnerItem): number =>
|
||||
typeof owner === 'number' ? owner : owner.value;
|
||||
const getOwnerLabel = (owner: OwnerItem): string | null =>
|
||||
typeof owner === 'number' ? null : owner.label;
|
||||
// Handle owners in number[], {value, label}[], or API {id, first_name, ...}[] form
|
||||
const getOwnerId = (owner: OwnerItem): number => {
|
||||
if (typeof owner === 'number') return owner;
|
||||
return 'value' in owner ? owner.value : owner.id;
|
||||
};
|
||||
const getOwnerLabel = (owner: OwnerItem): string | null => {
|
||||
if (typeof owner === 'number') return null;
|
||||
if ('value' in owner) return owner.label;
|
||||
const name = [owner.first_name, owner.last_name]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim();
|
||||
return name || null;
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
slice: {
|
||||
|
||||
Reference in New Issue
Block a user