issueViews.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. import type {Dispatch, Reducer} from 'react';
  2. import {
  3. createContext,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useReducer,
  9. useState,
  10. } from 'react';
  11. import styled from '@emotion/styled';
  12. import type {TabListState} from '@react-stately/tabs';
  13. import type {Orientation} from '@react-types/shared';
  14. import debounce from 'lodash/debounce';
  15. import isEqual from 'lodash/isEqual';
  16. import type {TabContext, TabsProps} from 'sentry/components/tabs';
  17. import {tabsShouldForwardProp} from 'sentry/components/tabs/utils';
  18. import {t} from 'sentry/locale';
  19. import type {PageFilters} from 'sentry/types/core';
  20. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  21. import {defined} from 'sentry/utils';
  22. import {trackAnalytics} from 'sentry/utils/analytics';
  23. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  24. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  25. import {useNavigate} from 'sentry/utils/useNavigate';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import useProjects from 'sentry/utils/useProjects';
  28. import {useUpdateGroupSearchViews} from 'sentry/views/issueList/mutations/useUpdateGroupSearchViews';
  29. import type {
  30. GroupSearchView,
  31. UpdateGroupSearchViewPayload,
  32. } from 'sentry/views/issueList/types';
  33. import {IssueSortOptions} from 'sentry/views/issueList/utils';
  34. import {NewTabContext, type NewView} from 'sentry/views/issueList/utils/newTabContext';
  35. export const TEMPORARY_TAB_KEY = 'temporary-tab';
  36. export const DEFAULT_TIME_FILTERS: PageFilters['datetime'] = {
  37. start: null,
  38. end: null,
  39. period: '14d',
  40. utc: null,
  41. };
  42. export const DEFAULT_ENVIRONMENTS: string[] = [];
  43. export const generateTempViewId = () => `_${Math.random().toString().substring(2, 7)}`;
  44. /**
  45. * Savable properties of an IssueView, besides lable and position.
  46. * Changes to these properties are not automatically saved and can
  47. * trigger the unsaved changes indicator.
  48. */
  49. export interface IssueViewParams {
  50. environments: string[];
  51. projects: number[];
  52. query: string;
  53. querySort: IssueSortOptions;
  54. timeFilters: PageFilters['datetime'];
  55. }
  56. export interface IssueView extends IssueViewParams {
  57. id: string;
  58. /**
  59. * False for tabs that were added view the "Add View" button, but
  60. * have not been edited in any way. Only tabs with isCommitted=true
  61. * will be saved to the backend.
  62. */
  63. isCommitted: boolean;
  64. key: string;
  65. label: string;
  66. content?: React.ReactNode;
  67. unsavedChanges?: Partial<IssueViewParams>;
  68. }
  69. type BaseIssueViewsAction = {
  70. /** If true, the new views state created by the action will be synced to the backend */
  71. syncViews?: boolean;
  72. };
  73. type ReorderTabsAction = {
  74. newKeyOrder: string[];
  75. type: 'REORDER_TABS';
  76. } & BaseIssueViewsAction;
  77. type SaveChangesAction = {
  78. type: 'SAVE_CHANGES';
  79. } & BaseIssueViewsAction;
  80. type DiscardChangesAction = {
  81. type: 'DISCARD_CHANGES';
  82. } & BaseIssueViewsAction;
  83. type RenameTabAction = {
  84. newLabel: string;
  85. type: 'RENAME_TAB';
  86. } & BaseIssueViewsAction;
  87. type DuplicateViewAction = {
  88. newViewId: string;
  89. type: 'DUPLICATE_VIEW';
  90. } & BaseIssueViewsAction;
  91. type DeleteViewAction = {
  92. type: 'DELETE_VIEW';
  93. } & BaseIssueViewsAction;
  94. type CreateNewViewAction = {
  95. tempId: string;
  96. type: 'CREATE_NEW_VIEW';
  97. } & BaseIssueViewsAction;
  98. type SetTempViewAction = {
  99. query: string;
  100. sort: IssueSortOptions;
  101. type: 'SET_TEMP_VIEW';
  102. } & BaseIssueViewsAction;
  103. type DiscardTempViewAction = {
  104. type: 'DISCARD_TEMP_VIEW';
  105. } & BaseIssueViewsAction;
  106. type SaveTempViewAction = {
  107. newViewId: string;
  108. type: 'SAVE_TEMP_VIEW';
  109. } & BaseIssueViewsAction;
  110. type UpdateUnsavedChangesAction = {
  111. type: 'UPDATE_UNSAVED_CHANGES';
  112. // Explicitly typed as | undefined instead of optional to make it clear that `undefined` = no unsaved changes
  113. unsavedChanges: Partial<IssueViewParams> | undefined;
  114. isCommitted?: boolean;
  115. } & BaseIssueViewsAction;
  116. type UpdateViewIdsAction = {
  117. newViews: UpdateGroupSearchViewPayload[];
  118. type: 'UPDATE_VIEW_IDS';
  119. } & BaseIssueViewsAction;
  120. type SetViewsAction = {
  121. type: 'SET_VIEWS';
  122. views: IssueView[];
  123. } & BaseIssueViewsAction;
  124. type SyncViewsToBackendAction = {
  125. /** Syncs the current views state to the backend. Does not make any changes to the views state. */
  126. type: 'SYNC_VIEWS_TO_BACKEND';
  127. };
  128. export type IssueViewsActions =
  129. | ReorderTabsAction
  130. | SaveChangesAction
  131. | DiscardChangesAction
  132. | RenameTabAction
  133. | DuplicateViewAction
  134. | DeleteViewAction
  135. | CreateNewViewAction
  136. | SetTempViewAction
  137. | DiscardTempViewAction
  138. | SaveTempViewAction
  139. | UpdateUnsavedChangesAction
  140. | UpdateViewIdsAction
  141. | SetViewsAction
  142. | SyncViewsToBackendAction;
  143. const ACTION_ANALYTICS_MAP: Partial<Record<IssueViewsActions['type'], string>> = {
  144. REORDER_TABS: 'issue_views.reordered_views',
  145. SAVE_CHANGES: 'issue_views.saved_changes',
  146. DISCARD_CHANGES: 'issue_views.discarded_changes',
  147. RENAME_TAB: 'issue_views.renamed_view',
  148. DUPLICATE_VIEW: 'issue_views.duplicated_view',
  149. DELETE_VIEW: 'issue_views.deleted_view',
  150. SAVE_TEMP_VIEW: 'issue_views.temp_view_saved',
  151. DISCARD_TEMP_VIEW: 'issue_views.temp_view_discarded',
  152. CREATE_NEW_VIEW: 'issue_views.add_view.clicked',
  153. };
  154. export interface IssueViewsState {
  155. views: IssueView[];
  156. tempView?: IssueView;
  157. }
  158. export interface IssueViewsContextType extends TabContext {
  159. defaultProject: number[];
  160. dispatch: Dispatch<IssueViewsActions>;
  161. state: IssueViewsState;
  162. }
  163. export const IssueViewsContext = createContext<IssueViewsContextType>({
  164. rootProps: {orientation: 'horizontal'},
  165. setTabListState: () => {},
  166. // Issue Views specific state
  167. dispatch: () => {},
  168. state: {views: []},
  169. defaultProject: [],
  170. });
  171. function reorderTabs(state: IssueViewsState, action: ReorderTabsAction) {
  172. const newTabs: IssueView[] = action.newKeyOrder
  173. .map(key => {
  174. const foundTab = state.views.find(tab => tab.key === key);
  175. return foundTab?.key === key ? foundTab : null;
  176. })
  177. .filter(defined);
  178. return {...state, views: newTabs};
  179. }
  180. function saveChanges(state: IssueViewsState, tabListState: TabListState<any>) {
  181. const originalTab = state.views.find(tab => tab.key === tabListState?.selectedKey);
  182. if (originalTab) {
  183. const newViews = state.views.map(tab => {
  184. return tab.key === tabListState?.selectedKey && tab.unsavedChanges
  185. ? {
  186. ...tab,
  187. query: tab.unsavedChanges.query ?? tab.query,
  188. querySort: tab.unsavedChanges.querySort ?? tab.querySort,
  189. projects: tab.unsavedChanges.projects ?? tab.projects,
  190. environments: tab.unsavedChanges.environments ?? tab.environments,
  191. timeFilters: tab.unsavedChanges.timeFilters ?? tab.timeFilters,
  192. unsavedChanges: undefined,
  193. }
  194. : tab;
  195. });
  196. return {...state, views: newViews};
  197. }
  198. return state;
  199. }
  200. function discardChanges(state: IssueViewsState, tabListState: TabListState<any>) {
  201. const originalTab = state.views.find(tab => tab.key === tabListState?.selectedKey);
  202. if (originalTab) {
  203. const newViews = state.views.map(tab => {
  204. return tab.key === tabListState?.selectedKey
  205. ? {...tab, unsavedChanges: undefined}
  206. : tab;
  207. });
  208. return {...state, views: newViews};
  209. }
  210. return state;
  211. }
  212. function renameView(
  213. state: IssueViewsState,
  214. action: RenameTabAction,
  215. tabListState: TabListState<any>
  216. ) {
  217. const renamedTab = state.views.find(tab => tab.key === tabListState?.selectedKey);
  218. if (renamedTab && action.newLabel !== renamedTab.label) {
  219. const newViews = state.views.map(tab =>
  220. tab.key === renamedTab.key
  221. ? {...tab, label: action.newLabel, isCommitted: true}
  222. : tab
  223. );
  224. return {...state, views: newViews};
  225. }
  226. return state;
  227. }
  228. function duplicateView(
  229. state: IssueViewsState,
  230. action: DuplicateViewAction,
  231. tabListState: TabListState<any>
  232. ) {
  233. const idx = state.views.findIndex(tb => tb.key === tabListState?.selectedKey);
  234. if (idx !== -1) {
  235. const duplicatedTab = state.views[idx]!;
  236. const newTabs: IssueView[] = [
  237. ...state.views.slice(0, idx + 1),
  238. {
  239. ...duplicatedTab,
  240. id: action.newViewId,
  241. key: action.newViewId,
  242. label: `${duplicatedTab.label} (Copy)`,
  243. isCommitted: true,
  244. },
  245. ...state.views.slice(idx + 1),
  246. ];
  247. return {...state, views: newTabs};
  248. }
  249. return state;
  250. }
  251. function deleteView(state: IssueViewsState, tabListState: TabListState<any>) {
  252. const newViews = state.views.filter(tab => tab.key !== tabListState?.selectedKey);
  253. return {...state, views: newViews};
  254. }
  255. function createNewView(
  256. state: IssueViewsState,
  257. action: CreateNewViewAction,
  258. defaultProject: number[]
  259. ) {
  260. const newTabs: IssueView[] = [
  261. ...state.views,
  262. {
  263. id: action.tempId,
  264. key: action.tempId,
  265. label: 'New View',
  266. query: '',
  267. querySort: IssueSortOptions.DATE,
  268. isCommitted: false,
  269. environments: DEFAULT_ENVIRONMENTS,
  270. projects: defaultProject,
  271. timeFilters: DEFAULT_TIME_FILTERS,
  272. },
  273. ];
  274. return {...state, views: newTabs};
  275. }
  276. function setTempView(
  277. state: IssueViewsState,
  278. action: SetTempViewAction,
  279. defaultProject: number[]
  280. ) {
  281. const tempView: IssueView = {
  282. id: TEMPORARY_TAB_KEY,
  283. key: TEMPORARY_TAB_KEY,
  284. label: t('Unsaved'),
  285. query: action.query,
  286. querySort: action.sort ?? IssueSortOptions.DATE,
  287. isCommitted: true,
  288. environments: DEFAULT_ENVIRONMENTS,
  289. projects: defaultProject,
  290. timeFilters: DEFAULT_TIME_FILTERS,
  291. };
  292. return {...state, tempView};
  293. }
  294. function discardTempView(state: IssueViewsState, tabListState: TabListState<any>) {
  295. tabListState?.setSelectedKey(state.views[0]!.key);
  296. return {...state, tempView: undefined};
  297. }
  298. function saveTempView(
  299. state: IssueViewsState,
  300. action: SaveTempViewAction,
  301. tabListState: TabListState<any>
  302. ) {
  303. if (state.tempView) {
  304. const newTab: IssueView = {
  305. id: action.newViewId,
  306. key: action.newViewId,
  307. label: 'New View',
  308. query: state.tempView?.query,
  309. querySort: state.tempView?.querySort,
  310. isCommitted: true,
  311. environments: state.tempView?.environments,
  312. projects: state.tempView?.projects,
  313. timeFilters: state.tempView?.timeFilters,
  314. };
  315. tabListState?.setSelectedKey(action.newViewId);
  316. return {...state, views: [...state.views, newTab], tempView: undefined};
  317. }
  318. return state;
  319. }
  320. function updateUnsavedChanges(
  321. state: IssueViewsState,
  322. action: UpdateUnsavedChangesAction,
  323. tabListState: TabListState<any>
  324. ) {
  325. return {
  326. ...state,
  327. views: state.views.map(tab =>
  328. tab.key === tabListState?.selectedKey
  329. ? {
  330. ...tab,
  331. unsavedChanges: action.unsavedChanges,
  332. isCommitted: action.isCommitted ?? tab.isCommitted,
  333. }
  334. : tab
  335. ),
  336. };
  337. }
  338. function updateViewIds(state: IssueViewsState, action: UpdateViewIdsAction) {
  339. const assignedIds = new Set();
  340. const updatedViews = state.views.map(tab => {
  341. if (tab.id && tab.id[0] === '_') {
  342. const matchingView = action.newViews.find(
  343. view =>
  344. view.id &&
  345. !assignedIds.has(view.id) &&
  346. tab.query === view.query &&
  347. tab.querySort === view.querySort &&
  348. tab.label === view.name
  349. );
  350. if (matchingView?.id) {
  351. assignedIds.add(matchingView.id);
  352. return {...tab, id: matchingView.id};
  353. }
  354. }
  355. return tab;
  356. });
  357. return {...state, views: updatedViews};
  358. }
  359. function setViews(state: IssueViewsState, action: SetViewsAction) {
  360. return {...state, views: action.views};
  361. }
  362. interface IssueViewsStateProviderProps extends Omit<TabsProps<any>, 'children'> {
  363. children: React.ReactNode;
  364. initialViews: IssueView[];
  365. // TODO(msun): Replace router with useLocation() / useUrlParams() / useSearchParams() in the future
  366. router: InjectedRouter;
  367. }
  368. export function IssueViewsStateProvider({
  369. children,
  370. initialViews,
  371. router,
  372. ...props
  373. }: IssueViewsStateProviderProps) {
  374. const navigate = useNavigate();
  375. const organization = useOrganization();
  376. const {setNewViewActive, setOnNewViewsSaved} = useContext(NewTabContext);
  377. const [tabListState, setTabListState] = useState<TabListState<any>>();
  378. const {className: _className, ...restProps} = props;
  379. const allowMultipleProjects = organization.features.includes('global-views');
  380. const isSuperUser = isActiveSuperuser();
  381. const {projects: allProjects} = useProjects();
  382. const memberProjects = useMemo(
  383. () => allProjects.filter(project => project.isMember),
  384. [allProjects]
  385. );
  386. const defaultProject = useMemo(() => {
  387. // Should not be possible for member projects to be empty
  388. if (allowMultipleProjects) {
  389. return [];
  390. }
  391. if (isSuperUser) {
  392. return allowMultipleProjects ? [] : [parseInt(allProjects[0]!.id, 10)];
  393. }
  394. return [parseInt(memberProjects[0]!.id, 10)];
  395. }, [memberProjects, allowMultipleProjects, isSuperUser, allProjects]);
  396. const {query, sort, viewId} = router.location.query;
  397. // This function is fired upon receiving new views from the backend - it replaces any previously
  398. // generated temporary view ids with the permanent view ids from the backend
  399. const replaceWithPersistentViewIds = (views: GroupSearchView[]) => {
  400. const newlyCreatedViews = views.filter(
  401. view => !state.views.find(tab => tab.id === view.id)
  402. );
  403. if (newlyCreatedViews.length > 0) {
  404. dispatch({type: 'UPDATE_VIEW_IDS', newViews: newlyCreatedViews});
  405. const currentView = state.views.find(tab => tab.id === viewId);
  406. if (viewId?.startsWith('_') && currentView) {
  407. const matchingView = newlyCreatedViews.find(
  408. view =>
  409. view.id &&
  410. currentView.query === view.query &&
  411. currentView.querySort === view.querySort
  412. );
  413. if (matchingView?.id) {
  414. navigate(
  415. normalizeUrl({
  416. ...location,
  417. query: {
  418. ...router.location.query,
  419. viewId: matchingView.id,
  420. },
  421. }),
  422. {replace: true}
  423. );
  424. }
  425. }
  426. }
  427. return;
  428. };
  429. const {mutate: updateViews} = useUpdateGroupSearchViews({
  430. onSuccess: replaceWithPersistentViewIds,
  431. });
  432. const debounceUpdateViews = useMemo(
  433. () =>
  434. debounce((newTabs: IssueView[]) => {
  435. if (newTabs) {
  436. updateViews({
  437. orgSlug: organization.slug,
  438. groupSearchViews: newTabs
  439. .filter(tab => tab.isCommitted)
  440. .map(tab => ({
  441. // Do not send over an ID if it's a temporary or default tab so that
  442. // the backend will save these and generate permanent Ids for them
  443. ...(tab.id[0] !== '_' && !tab.id.startsWith('default')
  444. ? {id: tab.id}
  445. : {}),
  446. name: tab.label,
  447. query: tab.query,
  448. querySort: tab.querySort,
  449. projects: isEqual(tab.projects, [-1]) ? [] : tab.projects,
  450. isAllProjects: isEqual(tab.projects, [-1]),
  451. environments: tab.environments,
  452. timeFilters: tab.timeFilters,
  453. })),
  454. });
  455. }
  456. }, 500),
  457. [organization.slug, updateViews]
  458. );
  459. const reducer: Reducer<IssueViewsState, IssueViewsActions> = useCallback(
  460. (state, action): IssueViewsState => {
  461. if (!tabListState) {
  462. return state;
  463. }
  464. switch (action.type) {
  465. case 'REORDER_TABS':
  466. return reorderTabs(state, action);
  467. case 'SAVE_CHANGES':
  468. return saveChanges(state, tabListState);
  469. case 'DISCARD_CHANGES':
  470. return discardChanges(state, tabListState);
  471. case 'RENAME_TAB':
  472. return renameView(state, action, tabListState);
  473. case 'DUPLICATE_VIEW':
  474. return duplicateView(state, action, tabListState);
  475. case 'DELETE_VIEW':
  476. return deleteView(state, tabListState);
  477. case 'CREATE_NEW_VIEW':
  478. return createNewView(state, action, defaultProject);
  479. case 'SET_TEMP_VIEW':
  480. return setTempView(state, action, defaultProject);
  481. case 'DISCARD_TEMP_VIEW':
  482. return discardTempView(state, tabListState);
  483. case 'SAVE_TEMP_VIEW':
  484. return saveTempView(state, action, tabListState);
  485. case 'UPDATE_UNSAVED_CHANGES':
  486. return updateUnsavedChanges(state, action, tabListState);
  487. case 'UPDATE_VIEW_IDS':
  488. return updateViewIds(state, action);
  489. case 'SET_VIEWS':
  490. return setViews(state, action);
  491. case 'SYNC_VIEWS_TO_BACKEND':
  492. return state;
  493. default:
  494. return state;
  495. }
  496. },
  497. [tabListState, defaultProject]
  498. );
  499. const sortOption =
  500. sort && Object.values(IssueSortOptions).includes(sort.toString() as IssueSortOptions)
  501. ? (sort.toString() as IssueSortOptions)
  502. : IssueSortOptions.DATE;
  503. const initialTempView: IssueView | undefined =
  504. query && (!viewId || !initialViews.find(tab => tab.id === viewId))
  505. ? {
  506. id: TEMPORARY_TAB_KEY,
  507. key: TEMPORARY_TAB_KEY,
  508. label: t('Unsaved'),
  509. query: query.toString(),
  510. querySort: sortOption,
  511. isCommitted: true,
  512. environments: DEFAULT_ENVIRONMENTS,
  513. projects: defaultProject,
  514. timeFilters: DEFAULT_TIME_FILTERS,
  515. }
  516. : undefined;
  517. const [state, dispatch] = useReducer(reducer, {
  518. views: initialViews,
  519. tempView: initialTempView,
  520. });
  521. const dispatchWrapper = (action: IssueViewsActions) => {
  522. const newState = reducer(state, action);
  523. dispatch(action);
  524. if (action.type === 'SYNC_VIEWS_TO_BACKEND' || action.syncViews) {
  525. debounceUpdateViews(newState.views);
  526. }
  527. const actionAnalyticsKey = ACTION_ANALYTICS_MAP[action.type];
  528. if (actionAnalyticsKey) {
  529. trackAnalytics(actionAnalyticsKey, {
  530. organization,
  531. });
  532. }
  533. };
  534. const handleNewViewsSaved: NewTabContext['onNewViewsSaved'] = useCallback<
  535. NewTabContext['onNewViewsSaved']
  536. >(
  537. () => (newViews: NewView[]) => {
  538. if (newViews.length === 0) {
  539. return;
  540. }
  541. setNewViewActive(false);
  542. const {label, query: newQuery, saveQueryToView} = newViews[0]!;
  543. const remainingNewViews: IssueView[] = newViews.slice(1)?.map(view => {
  544. const newId = generateTempViewId();
  545. const viewToTab: IssueView = {
  546. id: newId,
  547. key: newId,
  548. label: view.label,
  549. query: view.query,
  550. querySort: IssueSortOptions.DATE,
  551. unsavedChanges: view.saveQueryToView
  552. ? undefined
  553. : {
  554. query: view.query,
  555. querySort: IssueSortOptions.DATE,
  556. environments: DEFAULT_ENVIRONMENTS,
  557. projects: defaultProject,
  558. timeFilters: DEFAULT_TIME_FILTERS,
  559. },
  560. environments: DEFAULT_ENVIRONMENTS,
  561. projects: defaultProject,
  562. timeFilters: DEFAULT_TIME_FILTERS,
  563. isCommitted: true,
  564. };
  565. return viewToTab;
  566. });
  567. let updatedTabs: IssueView[] = state.views.map(tab => {
  568. if (tab.key === viewId) {
  569. return {
  570. ...tab,
  571. label,
  572. query: saveQueryToView ? newQuery : '',
  573. querySort: IssueSortOptions.DATE,
  574. unsavedChanges: saveQueryToView
  575. ? undefined
  576. : {
  577. query,
  578. querySort: IssueSortOptions.DATE,
  579. environments: DEFAULT_ENVIRONMENTS,
  580. projects: defaultProject,
  581. timeFilters: DEFAULT_TIME_FILTERS,
  582. },
  583. isCommitted: true,
  584. environments: DEFAULT_ENVIRONMENTS,
  585. projects: defaultProject,
  586. timeFilters: DEFAULT_TIME_FILTERS,
  587. };
  588. }
  589. return tab;
  590. });
  591. if (remainingNewViews.length > 0) {
  592. updatedTabs = [...updatedTabs, ...remainingNewViews];
  593. }
  594. dispatch({type: 'SET_VIEWS', views: updatedTabs, syncViews: true});
  595. navigate(
  596. {
  597. ...location,
  598. query: {
  599. ...router.location.query,
  600. query: newQuery,
  601. sort: IssueSortOptions.DATE,
  602. },
  603. },
  604. {replace: true}
  605. );
  606. },
  607. // eslint-disable-next-line react-hooks/exhaustive-deps
  608. [location, navigate, setNewViewActive, state.views, viewId]
  609. );
  610. useEffect(() => {
  611. setOnNewViewsSaved(handleNewViewsSaved);
  612. }, [setOnNewViewsSaved, handleNewViewsSaved]);
  613. return (
  614. <IssueViewsContext.Provider
  615. value={{
  616. rootProps: {...restProps, orientation: 'horizontal'},
  617. tabListState,
  618. setTabListState,
  619. defaultProject,
  620. dispatch: dispatchWrapper,
  621. state,
  622. }}
  623. >
  624. {children}
  625. </IssueViewsContext.Provider>
  626. );
  627. }
  628. export function IssueViews<T extends string | number>({
  629. orientation = 'horizontal',
  630. className,
  631. children,
  632. initialViews,
  633. router,
  634. ...props
  635. }: TabsProps<T> & Omit<IssueViewsStateProviderProps, 'children'>) {
  636. return (
  637. <IssueViewsStateProvider initialViews={initialViews} router={router} {...props}>
  638. <TabsWrap orientation={orientation} className={className}>
  639. {children}
  640. </TabsWrap>
  641. </IssueViewsStateProvider>
  642. );
  643. }
  644. const TabsWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{
  645. orientation: Orientation;
  646. }>`
  647. display: flex;
  648. flex-direction: ${p => (p.orientation === 'horizontal' ? 'column' : 'row')};
  649. flex-grow: 1;
  650. ${p =>
  651. p.orientation === 'vertical' &&
  652. `
  653. height: 100%;
  654. align-items: stretch;
  655. `};
  656. `;