Compare commits

...

2 Commits
6.2 ... 6.1

Author SHA1 Message Date
Luiz Otavio
10d3a16e26 fix(chart): chart description not showing (#41453)
(cherry picked from commit 834ccf2613)
2026-07-02 10:11:00 -03:00
Luiz Otavio
569cfc9825 fix(chart-explore): allow Save (Overwrite) after editing chart properties (#41602) 2026-07-01 10:33:26 -03:00
6 changed files with 93 additions and 10 deletions

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

@@ -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: {