diff --git a/src/actions.ts b/src/actions.ts index 9c9a6c9b0..42fc125d2 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -33,5 +33,6 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_FROM_PERSISTENCE: "SET_FROM_PERSISTENCE", OPEN_DASHBOARD: "OPEN_DASHBOARD", OPEN_INSTALLATION_WIZARD: "OPEN_INSTALLATION_WIZARD", - SET_SCOPE: "SET_SCOPE" + SET_SCOPE: "SET_SCOPE", + SET_STATE: "SET_STATE" }); diff --git a/src/components/Navigation/ScopeNavigation/ScopeNavigation.stories.tsx b/src/components/Navigation/ScopeNavigation/ScopeNavigation.stories.tsx new file mode 100644 index 000000000..eeab49964 --- /dev/null +++ b/src/components/Navigation/ScopeNavigation/ScopeNavigation.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { ScopeNavigation } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Navigation/ScopeNavigation", + component: ScopeNavigation, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = { + args: {} +}; + +export const Disabled: Story = { + args: {} +}; diff --git a/src/components/Navigation/ScopeNavigation/index.tsx b/src/components/Navigation/ScopeNavigation/index.tsx new file mode 100644 index 000000000..7b7bd160d --- /dev/null +++ b/src/components/Navigation/ScopeNavigation/index.tsx @@ -0,0 +1,109 @@ +import { useContext, useEffect, useState } from "react"; +import { actions } from "../../../actions"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { HistoryManager, HistoryStep } from "../../../utils/historyManager"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { Scope } from "../../common/App/types"; +import { HistoryNavigationPanel } from "../HistoryNavigationPanel"; +import { ChangeEnvironmentPayload, ChangeViewPayload } from "../types"; +import { actions as globalActions } from "./../actions"; +import { ScopeNavigationProps } from "./types"; + +const sendMessage = (historyStep: HistoryStep) => { + if (historyStep.scope) { + window.sendMessageToDigma({ + action: globalActions.CHANGE_SCOPE, + payload: { + ...historyStep.scope + } + }); + } +}; + +export const ScopeNavigation = (props: ScopeNavigationProps) => { + const [historyManager, setHistoryManager] = useState( + new HistoryManager() + ); + const { environment } = useContext(ConfigContext); + const previousTabId = usePrevious(props.currentTabId); + const previousSate = usePrevious(historyManager.getCurrent()); + + useEffect(() => { + const currentStep = historyManager.getCurrent(); + if ( + previousSate?.scope.span?.spanCodeObjectId === + currentStep?.scope.span?.spanCodeObjectId + ) { + if (previousTabId !== props.currentTabId) { + historyManager.updateCurrent({ tabId: props.currentTabId }); + } + } + }, [previousTabId, props.currentTabId, previousSate]); + + useEffect(() => { + const handleSetScope = (data: unknown) => { + const newScope = data as Scope; + const currentScope = historyManager.getCurrent()?.scope; + if ( + !currentScope || + currentScope.span?.spanCodeObjectId !== newScope.span?.spanCodeObjectId + ) { + historyManager.push({ + environment: environment || null, + scope: newScope, + tabId: null + }); + } else { + const historyStep = historyManager.getCurrent(); + + if (historyStep && historyStep.tabId) { + window.sendMessageToDigma({ + action: globalActions.CHANGE_VIEW, + payload: { + view: historyStep.tabId + } + }); + } + + if (historyStep && historyStep.environment) { + window.sendMessageToDigma({ + action: globalActions.CHANGE_ENVIRONMENT, + payload: { + environment: historyStep.environment + } + }); + } + } + }; + + dispatcher.addActionListener(actions.SET_SCOPE, handleSetScope); + + return () => { + dispatcher.removeActionListener(actions.SET_SCOPE, handleSetScope); + }; + }, [environment, props.currentTabId, historyManager]); + + const handleBackClick = () => { + const currentStep = historyManager.back(); + if (currentStep) { + sendMessage(currentStep); + } + }; + + const handleNexClick = () => { + const currentStep = historyManager.forward(); + if (currentStep) { + sendMessage(currentStep); + } + }; + + return ( + + ); +}; diff --git a/src/components/Navigation/ScopeNavigation/styles.ts b/src/components/Navigation/ScopeNavigation/styles.ts new file mode 100644 index 000000000..549940deb --- /dev/null +++ b/src/components/Navigation/ScopeNavigation/styles.ts @@ -0,0 +1,14 @@ +import styled from "styled-components"; + +export const NavigationButton = styled.button` + height: 28px; + width: 28px; + + &:not([disabled]) { + border: none; + } + + &:disabled { + color: ${({ theme }) => theme.colors.v3.icon.disabled}; + } +`; diff --git a/src/components/Navigation/ScopeNavigation/types.ts b/src/components/Navigation/ScopeNavigation/types.ts new file mode 100644 index 000000000..6bc3e6522 --- /dev/null +++ b/src/components/Navigation/ScopeNavigation/types.ts @@ -0,0 +1,3 @@ +export interface ScopeNavigationProps { + currentTabId: string; +} diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx index 997a386ca..25c0eaf63 100644 --- a/src/components/Navigation/index.tsx +++ b/src/components/Navigation/index.tsx @@ -16,8 +16,8 @@ import { FourSquaresIcon } from "../common/icons/FourSquaresIcon"; import { ThreeDotsIcon } from "../common/icons/ThreeDotsIcon"; import { CodeButton } from "./CodeButton"; import { CodeButtonMenu } from "./CodeButtonMenu"; -import { HistoryNavigationPanel } from "./HistoryNavigationPanel"; import { IconButton } from "./IconButton"; +import { ScopeNavigation } from "./ScopeNavigation"; import { Tabs } from "./Tabs"; import { TargetButtonMenu } from "./TargetButtonMenu"; import { actions } from "./actions"; @@ -113,6 +113,7 @@ export const Navigation = () => { const [isAutoFixing, setIsAutoFixing] = useState(false); const [isAnnotationAdding, setIsAnnotationAdding] = useState(false); const previousCodeContext = usePrevious(codeContext); + const [currentTab, setCurrentTab] = useState(); const environments = config.environments || []; @@ -124,6 +125,9 @@ export const Navigation = () => { const handleViewData = (data: unknown) => { const payload = data as SetViewsPayload; setTabs(payload.views); + + const selected = payload.views.find((x) => x.isSelected); + selected && setCurrentTab(selected.id); }; const handleCodeContextData = (data: unknown) => { @@ -319,7 +323,8 @@ export const Navigation = () => { openURLInDefaultBrowser(SLACK_WORKSPACE_URL); }; - const handleTabClick = (tabId: string) => { + const changeTab = (tabId: string) => { + setCurrentTab(tabId); window.sendMessageToDigma({ action: actions.CHANGE_VIEW, payload: { @@ -400,12 +405,7 @@ export const Navigation = () => { return ( - + { )} - + ); diff --git a/src/utils/historyManager.ts b/src/utils/historyManager.ts new file mode 100644 index 000000000..2d7bcf784 --- /dev/null +++ b/src/utils/historyManager.ts @@ -0,0 +1,158 @@ +import { Environment, Scope } from "../components/common/App/types"; + +const MAX_STEPS = 15; + +export interface HistoryStep { + scope: Scope; + environment?: Environment | null; + tabId: string | null; +} + +export interface UpdateStepParams { + scope?: Scope; + environment?: Environment | null; + tabId?: string; +} + +export interface HistoryData { + steps: HistoryStep[]; + currentStepIndex: number; +} + +interface Node { + next: Node | null; + previous: Node | null; + value: T; +} + +export class HistoryManager { + private head: Node | null = null; + private tail: Node | null = null; + + private current: Node | null = null; + private itemsCount = 0; + private currentIndex = -1; + + constructor(data?: HistoryData) { + if (data) { + this.init(data.steps, data.currentStepIndex); + } + } + + push(step: HistoryStep) { + const newNode: Node = { + value: step, + previous: null, + next: null + }; + + if (this.current != this.tail) { + this.tail = this.current; + this.itemsCount = this.itemsCount - (this.itemsCount - this.currentIndex); + } + + if (this.tail) { + this.tail.next = newNode; + newNode.previous = this.tail; + } + + if (!this.head) { + this.head = newNode; + } + + this.tail = newNode; + this.current = newNode; + this.currentIndex++; + + if (this.itemsCount === MAX_STEPS && this.head?.next) { + this.head = this.head.next; + } else { + this.itemsCount++; + } + } + + canMoveBack() { + if (!this.current?.previous) { + return false; + } + return true; + } + + back() { + if (!this.current?.previous) { + return null; + } + + this.current = this.current.previous; + this.currentIndex--; + return this.getCurrent(); + } + + canMoveForward() { + if (!this.current?.next) { + return false; + } + return true; + } + + forward() { + if (!this.current?.next) { + return null; + } + + this.current = this.current.next; + this.currentIndex++; + return this.getCurrent(); + } + + getLast() { + return this.tail?.value; + } + + getCurrent() { + return this.current?.value; + } + + updateCurrent(newValue: UpdateStepParams) { + if (this.current) { + this.current.value = { ...this.current.value, ...newValue }; + } + } + + getHistoryData() { + const steps = []; + let step = this.head; + while (step) { + steps.push(step.value); + step = step.next; + } + + return { + steps, + currentStepIndex: this.currentIndex + }; + } + + private init(steps: HistoryStep[], selectedStep: number) { + if (!steps.length) { + return; + } + + let currentIndex = 0; + + let localCurrent = null; + do { + this.push(steps[currentIndex]); + + if (selectedStep === currentIndex) { + localCurrent = this.current; + } + + currentIndex++; + } while (currentIndex < steps.length); + + this.itemsCount = steps.length; + this.currentIndex = selectedStep; + this.current = localCurrent; + } +}