Compare commits

...

1 Commits

Author SHA1 Message Date
Evan
e8c7d6656a fix(explore): allow chart owners to overwrite when owners are objects
The Save chart modal gates "Save (Overwrite)" on canOverwriteSlice(),
which checked slice.owners.includes(user.userId). The Slice type declares
owners as { id: number }[], and the Explore bootstrap can hydrate owners in
object form, so includes() against a numeric userId never matched. A chart
owner who wasn't also an admin was therefore forced into "Save as...",
which is the path that produces the duplicate/stale-chart behavior reported
in the issue.

Normalize owner identity to the numeric id before comparing so owners are
recognized whether they arrive as plain ids or as objects.

Fixes #38911

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:19:01 -07:00
2 changed files with 38 additions and 4 deletions

View File

@@ -334,6 +334,27 @@ test('enables overwrite option for admin non-owner', () => {
expect(getByRole('radio', { name: 'Save (Overwrite)' })).toBeEnabled();
});
test('enables overwrite option for owner when owners are objects', () => {
// The Slice type declares `owners: { id: number }[]`, and the explore
// bootstrap can deliver owners in object form. A chart owner must still be
// able to overwrite their own chart in that case.
const { getByRole } = setup(
{},
mockStore({
...initialState,
explore: {
...initialState.explore,
slice: {
...initialState.explore.slice,
owners: [{ id: 1 }],
},
},
user: { userId: 1 },
}),
);
expect(getByRole('radio', { name: 'Save (Overwrite)' })).toBeEnabled();
});
test('updates slice name and selected dashboard', async () => {
const dashboardId = mockEvent.value;
const saveDataset = jest.fn().mockResolvedValue(undefined);

View File

@@ -227,13 +227,26 @@ const SaveModal = ({
const history = useHistory();
const theme = useTheme();
const isCurrentUserOwner = useCallback((): boolean => {
const userId = user?.userId;
if (userId === undefined) {
return false;
}
// Owners can arrive either as plain ids (number) or as objects depending on
// where the slice was hydrated from (bootstrap vs. SLICE_UPDATED reducer),
// so normalize to the numeric id before comparing.
return Boolean(
slice?.owners?.some((owner: number | { id?: number }) =>
typeof owner === 'number' ? owner === userId : owner?.id === userId,
),
);
}, [slice, user]);
const canOverwriteSlice = useCallback(
(): boolean =>
(can_overwrite ||
isUserAdmin(user) ||
slice?.owners?.includes(user.userId)) &&
(can_overwrite || isUserAdmin(user) || isCurrentUserOwner()) &&
!slice?.is_managed_externally,
[can_overwrite, slice, user],
[can_overwrite, slice, user, isCurrentUserOwner],
);
const [newSliceName, setNewSliceName] = useState<string | undefined>(