Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(issue-views): Fold navigate behavior into issueViews context #82540

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions static/app/views/issueList/customViewsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,12 @@ function CustomViewsIssueListHeaderTabsContent({
}
if (query) {
if (!tabListState?.selectionManager.isSelected(TEMPORARY_TAB_KEY)) {
dispatch({type: 'SET_TEMP_VIEW', query, sort});
navigate(
normalizeUrl({
...location,
query: {
...queryParamsWithPageFilters,
viewId: undefined,
},
}),
{replace: true}
);
dispatch({
type: 'SET_TEMP_VIEW',
query,
sort,
updateQueryParams: {newQueryParams: {viewId: undefined}, replace: true},
});
tabListState?.setSelectedKey(TEMPORARY_TAB_KEY);
return;
}
Expand Down
116 changes: 44 additions & 72 deletions static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import {useHotkeys} from 'sentry/utils/useHotkeys';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
import {DraggableTabMenuButton} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabMenuButton';
import EditableTabTitle from 'sentry/views/issueList/groupSearchViewTabs/editableTabTitle';
Expand All @@ -40,7 +39,6 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
const [editingTabKey, setEditingTabKey] = useState<string | null>(null);

const organization = useOrganization();
const navigate = useNavigate();
const location = useLocation();

const {cursor: _cursor, page: _page, ...queryParams} = router?.location?.query ?? {};
Expand All @@ -66,43 +64,6 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
[dispatch, tabListState?.selectedKey, tabs]
);

const handleDuplicateView = () => {
const newViewId = generateTempViewId();
const duplicatedTab = state.views.find(
view => view.key === tabListState?.selectedKey
);
if (!duplicatedTab) {
return;
}
dispatch({type: 'DUPLICATE_VIEW', newViewId, syncViews: true});
navigate({
...location,
query: {
...queryParams,
query: duplicatedTab.query,
sort: duplicatedTab.querySort,
viewId: newViewId,
},
});
};

const handleDiscardChanges = () => {
dispatch({type: 'DISCARD_CHANGES'});
const originalTab = state.views.find(view => view.key === tabListState?.selectedKey);
if (originalTab) {
// TODO(msun): Move navigate logic to IssueViewsContext
navigate({
...location,
query: {
...queryParams,
query: originalTab.query,
sort: originalTab.querySort,
viewId: originalTab.id,
},
});
}
};

const handleNewViewsSaved: NewTabContext['onNewViewsSaved'] = useCallback<
NewTabContext['onNewViewsSaved']
>(
Expand Down Expand Up @@ -145,37 +106,23 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
updatedTabs = [...updatedTabs, ...remainingNewViews];
}

dispatch({type: 'SET_VIEWS', views: updatedTabs, syncViews: true});
navigate(
{
...location,
query: {
...queryParams,
query,
sort: IssueSortOptions.DATE,
},
dispatch({
type: 'SET_VIEWS',
views: updatedTabs,
syncViews: true,
updateQueryParams: {
newQueryParams: {query, sort: IssueSortOptions.DATE},
replace: true,
},
{replace: true}
);
});
},

// eslint-disable-next-line react-hooks/exhaustive-deps
[location, navigate, setNewViewActive, tabs, viewId]
[location, setNewViewActive, tabs, viewId]
);

const handleCreateNewView = () => {
const tempId = generateTempViewId();
dispatch({type: 'CREATE_NEW_VIEW', tempId});
tabListState?.setSelectedKey(tempId);
navigate({
...location,
query: {
...queryParams,
query: '',
viewId: tempId,
},
});
};
useEffect(() => {
setOnNewViewsSaved(handleNewViewsSaved);
}, [setOnNewViewsSaved, handleNewViewsSaved]);

const handleDeleteView = (tab: IssueView) => {
dispatch({type: 'DELETE_VIEW', syncViews: true});
Expand All @@ -184,9 +131,28 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
tabListState?.setSelectedKey(tabs.filter(tb => tb.key !== tab.key)[0].key);
};

useEffect(() => {
setOnNewViewsSaved(handleNewViewsSaved);
}, [setOnNewViewsSaved, handleNewViewsSaved]);
const handleDuplicateView = (tab: IssueView) => {
const newViewId = generateTempViewId();
dispatch({
type: 'DUPLICATE_VIEW',
newViewId,
syncViews: true,
updateQueryParams: {
newQueryParams: {viewId: newViewId, query: tab.query, sort: tab.querySort},
},
});
};

const handleAddView = () => {
const tempId = generateTempViewId();
dispatch({
type: 'CREATE_NEW_VIEW',
tempId,
updateQueryParams: {
newQueryParams: {viewId: tempId, query: ''},
},
});
};

const makeMenuOptions = (tab: IssueView): MenuItemProps[] => {
if (tab.key === TEMPORARY_TAB_KEY) {
Expand All @@ -198,15 +164,21 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
if (tab.unsavedChanges) {
return makeUnsavedChangesMenuOptions({
onRename: () => setEditingTabKey(tab.key),
onDuplicate: handleDuplicateView,
onDuplicate: () => handleDuplicateView(tab),
onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined,
onSave: () => dispatch({type: 'SAVE_CHANGES', syncViews: true}),
onDiscard: handleDiscardChanges,
onDiscard: () =>
dispatch({
type: 'DISCARD_CHANGES',
updateQueryParams: {
newQueryParams: {viewId: tab.id, query: tab.query, sort: tab.querySort},
},
}),
});
}
return makeDefaultMenuOptions({
onRename: () => setEditingTabKey(tab.key),
onDuplicate: handleDuplicateView,
onDuplicate: () => handleDuplicateView(tab),
onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined,
});
};
Expand All @@ -223,7 +195,7 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) {
}
onReorderComplete={() => dispatch({type: 'SYNC_VIEWS_TO_BACKEND'})}
defaultSelectedKey={initialTabKey}
onAddView={handleCreateNewView}
onAddView={handleAddView}
orientation="horizontal"
editingTabKey={editingTabKey ?? undefined}
hideBorder
Expand Down
91 changes: 64 additions & 27 deletions static/app/views/issueList/groupSearchViewTabs/issueViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,20 @@ export interface IssueView {
unsavedChanges?: [string, IssueSortOptions];
}

export type IssueViewsQueryUpdateParams = {
// TODO(msun): Once project/env/datetime are added as view configs, add them here
newQueryParams: {
query?: string;
sort?: IssueSortOptions;
viewId?: string;
};
replace?: boolean;
};

type BaseIssueViewsAction = {
/** If true, the new views state created by the action will be synced to the backend */
syncViews?: boolean;
updateQueryParams?: IssueViewsQueryUpdateParams;
};

type ReorderTabsAction = {
Expand Down Expand Up @@ -112,7 +123,7 @@ type SetViewsAction = {
type SyncViewsToBackendAction = {
/** Syncs the current views state to the backend. Does not make any changes to the views state. */
type: 'SYNC_VIEWS_TO_BACKEND';
};
} & Exclude<BaseIssueViewsAction, 'syncViews'>;

export type IssueViewsActions =
| ReorderTabsAction
Expand Down Expand Up @@ -247,7 +258,11 @@ function deleteView(state: IssueViewsState, tabListState: TabListState<any>) {
return {...state, views: newViews};
}

function createNewView(state: IssueViewsState, action: CreateNewViewAction) {
function createNewView(
state: IssueViewsState,
action: CreateNewViewAction,
tabListState: TabListState<any>
) {
const newTabs: IssueView[] = [
...state.views,
{
Expand All @@ -259,6 +274,7 @@ function createNewView(state: IssueViewsState, action: CreateNewViewAction) {
isCommitted: false,
},
];
tabListState?.setSelectedKey(action.tempId);
return {...state, views: newTabs};
}

Expand Down Expand Up @@ -417,30 +433,6 @@ export function IssueViewsStateProvider({
onSuccess: replaceWithPersistantViewIds,
});

const debounceUpdateViews = useMemo(
() =>
debounce((newTabs: IssueView[]) => {
if (newTabs) {
updateViews({
orgSlug: organization.slug,
groupSearchViews: newTabs
.filter(tab => tab.isCommitted)
.map(tab => ({
// Do not send over an ID if it's a temporary or default tab so that
// the backend will save these and generate permanent Ids for them
...(tab.id[0] !== '_' && !tab.id.startsWith('default')
? {id: tab.id}
: {}),
name: tab.label,
query: tab.query,
querySort: tab.querySort,
})),
});
}
}, 500),
[organization.slug, updateViews]
);

const reducer: Reducer<IssueViewsState, IssueViewsActions> = useCallback(
(state, action): IssueViewsState => {
if (!tabListState) {
Expand All @@ -460,7 +452,7 @@ export function IssueViewsStateProvider({
case 'DELETE_VIEW':
return deleteView(state, tabListState);
case 'CREATE_NEW_VIEW':
return createNewView(state, action);
return createNewView(state, action, tabListState);
case 'SET_TEMP_VIEW':
return setTempView(state, action);
case 'DISCARD_TEMP_VIEW':
Expand Down Expand Up @@ -504,14 +496,59 @@ export function IssueViewsStateProvider({
tempView: initialTempView,
});

const debounceUpdateViews = useMemo(
() =>
debounce((newTabs: IssueView[]) => {
if (newTabs) {
updateViews({
orgSlug: organization.slug,
groupSearchViews: newTabs
.filter(tab => tab.isCommitted)
.map(tab => ({
// Do not send over an ID if it's a temporary or default tab so that
// the backend will save these and generate permanent Ids for them
...(tab.id[0] !== '_' && !tab.id.startsWith('default')
? {id: tab.id}
: {}),
name: tab.label,
query: tab.query,
querySort: tab.querySort,
})),
});
}
}, 500),
[organization.slug, updateViews]
);

const updateQueryParams = (params: IssueViewsQueryUpdateParams) => {
const {newQueryParams, replace = false} = params;
navigate(
normalizeUrl({
...location,
query: {
...queryParams,
...newQueryParams,
},
}),
{replace}
);
};

const dispatchWrapper = (action: IssueViewsActions) => {
const newState = reducer(state, action);
dispatch(action);

// These actions are outside of the dispatch to avoid introducing side effects in the reducer
if (action.type === 'SYNC_VIEWS_TO_BACKEND' || action.syncViews) {
debounceUpdateViews(newState.views);
}

if (action.updateQueryParams) {
queueMicrotask(() => {
updateQueryParams(action.updateQueryParams!);
});
}

const actionAnalyticsKey = ACTION_ANALYTICS_MAP[action.type];
if (actionAnalyticsKey) {
trackAnalytics(actionAnalyticsKey, {
Expand Down
Loading