history#Location TypeScript Examples
The following examples show how to use
history#Location.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts From backstage with Apache License 2.0 | 7 votes |
export function isLocationMatch(currentLocation: Location, toLocation: Path) {
const toDecodedSearch = new URLSearchParams(toLocation.search).toString();
const toQueryParameters = qs.parse(toDecodedSearch);
const currentDecodedSearch = new URLSearchParams(
currentLocation.search,
).toString();
const currentQueryParameters = qs.parse(currentDecodedSearch);
const matching =
isEqual(toLocation.pathname, currentLocation.pathname) &&
isMatch(currentQueryParameters, toQueryParameters);
return matching;
}
Example #2
Source File: LocationContext.ts From next-core with GNU General Public License v3.0 | 6 votes |
handleBeforePageLeave(detail: {
location?: Location<PluginHistoryState>;
action?: Action;
}): void {
this.dispatchLifeCycleEvent(
new CustomEvent("page.beforeLeave", {
detail,
}),
this.beforePageLeaveHandlers
);
}
Example #3
Source File: useLocation.ts From rocon with MIT License | 6 votes |
useLocation = (): Location => {
const location = useContext(LocationContext);
if (location === undefined) {
throw new Error(
"No location found. Please make sure you have placed RoconRoot above."
);
}
return location;
}
Example #4
Source File: RoconRoot.tsx From rocon with MIT License | 6 votes |
RoconRoot: React.FC<RoconRootProps> = memo((props) => {
const history = useMemo(
() => props.history || createBrowserHistory(),
[props.history]
);
const historyContextValue = useMemo(() => {
return {
history,
};
}, [history]);
const [location, setLocation] = useState<Location>(history.location);
useEffect(() => {
// this `setLocation` handles the case where location is changed after above `history.location` is calculated
setLocation(history.location);
return history.listen((state) => {
setLocation(state.location);
});
}, [history]);
return (
<HistoryContext.Provider value={historyContextValue}>
<LocationContext.Provider value={location}>
{props.children}
</LocationContext.Provider>
</HistoryContext.Provider>
);
})
Example #5
Source File: index.tsx From subscan-multisig-react with Apache License 2.0 | 6 votes |
// redirect on invalid tabs
// eslint-disable-next-line complexity
function redirect(basePath: string, location: Location, items: TabItem[], hidden?: string[] | null | false): void {
if (location.pathname !== basePath) {
// Has the form /staking/query/<something>
const [, , section] = location.pathname.split('/');
// eslint-disable-next-line @typescript-eslint/no-shadow
const alias = items.find(({ alias }) => alias === section);
if (alias) {
window.location.hash = alias.isRoot ? basePath : `${basePath}/${alias.name}`;
} else if (hidden && (hidden.includes(section) || !items.some(({ isRoot, name }) => !isRoot && name === section))) {
window.location.hash = basePath;
}
}
}
Example #6
Source File: useLocation.ts From next-core with GNU General Public License v3.0 | 6 votes |
/** @internal */
export function useLocation(): Location<PluginHistoryState> {
const history = getHistory();
const [location, setLocation] = React.useState(history.location);
React.useEffect(() => {
const unlisten = history.listen((location) => setLocation(location));
return unlisten;
}, [history]);
return location;
}
Example #7
Source File: Router.ts From next-core with GNU General Public License v3.0 | 6 votes |
private getBlockMessageBeforePageLave(detail: {
location?: Location<PluginHistoryState>;
action?: Action;
}): string {
const history = getHistory();
const previousMessage = history.getBlockMessage();
this.locationContext?.handleBeforePageLeave(detail);
const message = history.getBlockMessage();
if (!previousMessage && message) {
// Auto unblock only if new block was introduced by `onBeforePageLeave`.
history.unblock();
}
return message;
}
Example #8
Source File: Router.spec.ts From next-core with GNU General Public License v3.0 | 6 votes |
spyOnHistory = {
location: {
pathname: "/yo",
} as unknown as Location,
listen: jest.fn((fn: LocationListener) => {
historyListeners.push(fn);
return () => {
const index = historyListeners.indexOf(fn);
if (index >= 0) {
historyListeners.splice(index, 1);
}
};
}),
replace: jest.fn(),
block: jest.fn((fn) => {
blockFn = fn;
}),
createHref: () => "/oops",
setBlockMessage,
getBlockMessage,
unblock,
}
Example #9
Source File: Tracker.ts From nosgestesclimat-site with MIT License | 6 votes |
track(loc: Location) {
const currentPath = loc.pathname + loc.search
if (this.previousPath === currentPath) {
return
}
if (this.previousPath) {
this.push(['setReferrerUrl', this.previousPath])
}
this.push(['setCustomUrl', currentPath])
// TODO: We should also call 'setDocumentTitle' but at this point the
// document.title isn't updated yet.
this.push(['trackPageView'])
this.previousPath = currentPath
}
Example #10
Source File: tour.tsx From clearflask with Apache License 2.0 | 6 votes |
setStep = async (
dispatch: ThunkDispatch<ReduxStateTour, undefined, AllTourActions>,
history: History,
location: Location,
guideId: string,
guide: TourDefinitionGuide,
onGuideCompleted: TourData['onGuideCompleted'],
stepId?: string,
step?: TourDefinitionGuideStep,
) => {
dispatch({
type: 'tourSetStep', payload: {
activeStep: undefined,
}
});
if (!!stepId && !!step) {
if (!!step.openPath && location.pathname !== step.openPath) {
await new Promise(resolve => setTimeout(resolve, 250));
history.push(step.openPath);
}
await new Promise(resolve => setTimeout(resolve, step.showDelay !== undefined ? step.showDelay : 250));
dispatch({
type: 'tourSetStep', payload: {
activeStep: { guideId, stepId },
}
});
} else {
if (guide.onComplete?.openPath) {
await new Promise(resolve => setTimeout(resolve, 250));
history.push(guide.onComplete.openPath);
}
onGuideCompleted?.(guideId, guide);
}
}
Example #11
Source File: router.ts From che-dashboard-next with Eclipse Public License 2.0 | 6 votes |
getMockRouterProps = <Params extends { [K in keyof Params]: string } = {}>(path: string, params: Params): {
history: MemoryHistory<LocationState>;
location: Location<LocationState>;
match: routerMatch<Params>
} => {
const isExact = false;
const url = generateUrl(path, params);
const match: routerMatch<Params> = { isExact, path, url, params };
const history = createMemoryHistory();
const location = createLocation(match.url);
return { history, location, match };
}
Example #12
Source File: Items.tsx From backstage with Apache License 2.0 | 6 votes |
useLocationMatch = (
submenu: React.ReactElement<SidebarSubmenuProps>,
location: Location,
): boolean =>
useElementFilter(
submenu.props.children,
elements => {
let active = false;
elements
.getElements()
.forEach(
({
props: { to, dropdownItems },
}: {
props: Partial<SidebarSubmenuItemProps>;
}) => {
if (!active) {
if (dropdownItems?.length) {
dropdownItems.forEach(
({ to: _to }) =>
(active =
active || isLocationMatch(location, resolvePath(_to))),
);
return;
}
if (to) {
active = isLocationMatch(location, resolvePath(to));
}
}
},
);
return active;
},
[location.pathname],
)
Example #13
Source File: utils.test.ts From backstage with Apache License 2.0 | 6 votes |
describe('isLocationMatching', () => {
let currentLocation: Location;
let toLocation: Path;
it('return false when pathname in target and current location differ', async () => {
currentLocation = {
pathname: '/catalog',
search: '?kind=component',
state: null,
hash: '',
key: '',
};
toLocation = {
pathname: '/catalog-a',
search: '?kind=component',
hash: '',
};
expect(isLocationMatch(currentLocation, toLocation)).toBe(false);
});
it('return true when exact match between current and target location parameters', async () => {
currentLocation = {
pathname: '/catalog',
search: '?kind=component',
state: null,
hash: '',
key: '',
};
toLocation = { pathname: '/catalog', search: '?kind=component', hash: '' };
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
});
it('return true when target query parameters are subset of current location query parameters', async () => {
currentLocation = {
pathname: '/catalog',
search: '?x=foo&y=bar',
state: null,
hash: '',
key: '',
};
toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' };
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
});
it('return false when no matching query parameters between target and current location', async () => {
currentLocation = {
pathname: '/catalog',
search: '?y=bar',
state: null,
hash: '',
key: '',
};
toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' };
expect(isLocationMatch(currentLocation, toLocation)).toBe(false);
});
it('return true when query parameters match in different order', async () => {
currentLocation = {
pathname: '/catalog',
search: '?y=bar&x=foo',
state: null,
hash: '',
key: '',
};
toLocation = { pathname: '/catalog', search: '?x=foo&y=bar', hash: '' };
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
});
it('return true when there is a matching query parameter alongside extra parameters', async () => {
currentLocation = {
pathname: '/catalog',
search: '?y=bar&x=foo',
state: null,
hash: '',
key: '',
};
toLocation = { pathname: '/catalog', search: '', hash: '' };
expect(isLocationMatch(currentLocation, toLocation)).toBe(true);
});
});
Example #14
Source File: filters.tsx From ke with MIT License | 6 votes |
setFilterValue = (
location: Location,
filterName: string,
filterValue: Accessor<string>,
history: History,
setPage?: (page: number) => void
): void => {
const filters = FilterManager.getFilters(location.search)
filters.push({ filterName, filterOperation: undefined, value: filterValue })
FilterManager.resetPagination(setPage)
FilterManager.setFilters(location, filters, history)
}
Example #15
Source File: hooks.ts From atlas with GNU General Public License v3.0 | 5 votes |
useVideoWorkspaceRouting = (): Location => {
const navigate = useNavigate()
const location = useLocation()
const locationState = location.state as RoutingState
const [cachedLocation, setCachedLocation] = useState<Location>()
const videoWorkspaceMatch = useMatch(WORKSPACE_MATCH)
const { isWorkspaceOpen, setIsWorkspaceOpen } = useVideoWorkspace()
const [cachedIsWorkspaceOpen, setCachedIsWorkspaceOpen] = useState(false)
useEffect(() => {
if (location === cachedLocation) {
return
}
setCachedLocation(location)
if (videoWorkspaceMatch && !isWorkspaceOpen) {
// route changed to video edit
const state: RoutingState = {
overlaidLocation: cachedLocation ?? defaultLocation,
}
navigate(location, { replace: true, state })
setIsWorkspaceOpen(true)
}
}, [location, cachedLocation, videoWorkspaceMatch, navigate, isWorkspaceOpen, setIsWorkspaceOpen])
useEffect(() => {
if (isWorkspaceOpen === cachedIsWorkspaceOpen) {
return
}
setCachedIsWorkspaceOpen(isWorkspaceOpen)
if (!isWorkspaceOpen) {
// restore the old location when videoWorkspace was closed
const oldLocation = locationState?.overlaidLocation ?? absoluteRoutes.studio.index()
navigate(oldLocation)
}
if (isWorkspaceOpen && !videoWorkspaceMatch) {
// isWorkspaceOpen changed without the route change, change URL and save current location
const state: RoutingState = {
overlaidLocation: location,
}
navigate(absoluteRoutes.studio.videoWorkspace(), { state: state })
}
}, [cachedIsWorkspaceOpen, isWorkspaceOpen, location, locationState, navigate, videoWorkspaceMatch])
if (videoWorkspaceMatch) {
return locationState?.overlaidLocation ?? cachedLocation ?? defaultLocation
}
return location
}
Example #16
Source File: LocationContext.ts From rocon with MIT License | 5 votes |
LocationContext = createContext<Location | undefined>(undefined)
Example #17
Source File: hooks.ts From atlas with GNU General Public License v3.0 | 5 votes |
defaultLocation: Location = {
pathname: absoluteRoutes.studio.index(),
key: '',
search: '',
hash: '',
state: null,
}
Example #18
Source File: useModalRoute.ts From querybook with Apache License 2.0 | 5 votes |
export function useModalRoute(location: Location<{ isModal: boolean }>) {
const disableModal = useSelector(
(state: IStoreState) =>
state.user.computedSettings.show_full_view === 'enabled'
);
return Boolean(!disableModal && location?.state?.isModal);
}
Example #19
Source File: App.tsx From minesweeper with MIT License | 5 votes |
Navigation: FC = () => {
const [searchParams] = useSearchParams();
const level = searchParams.get('level') || '';
const getLocationObjWithSearchParams = (
pathname: string
): Partial<Location> => ({
pathname,
search: `${
level &&
`?${new URLSearchParams({
level,
}).toString()}`
}`,
});
return (
<nav>
<ul>
<li>
<Link to={getLocationObjWithSearchParams('/')}>Home</Link>
</li>
<li>
<Link to={getLocationObjWithSearchParams('/minesweeper/hooks')}>
Game With Hooks
</Link>
</li>
<li>
<Link to={getLocationObjWithSearchParams('/minesweeper/usereducer')}>
Game With useReducer
</Link>
</li>
<li>
<Link to={getLocationObjWithSearchParams('/minesweeper/reactredux')}>
Game With ReactRedux
</Link>
</li>
</ul>
</nav>
);
}
Example #20
Source File: routerStore.ts From lightning-terminal with MIT License | 5 votes |
/** the current location from the browser's history */
location: Location;
Example #21
Source File: Router.spec.ts From next-core with GNU General Public License v3.0 | 5 votes |
mockHistoryPop = (location: Partial<Location>): void => {
historyListeners.forEach((fn) => {
fn(location as Location, "POP");
});
}
Example #22
Source File: Router.spec.ts From next-core with GNU General Public License v3.0 | 5 votes |
mockHistoryPush = (location: Partial<Location>): void => {
historyListeners.forEach((fn) => {
fn(location as Location, "PUSH");
});
}
Example #23
Source File: AppContainer.tsx From anthem with Apache License 2.0 | 5 votes |
routeChangeListener = (location: Location, action: Action) => {
// Dispatch relevant actions for routing events.
Analytics.page();
this.props.onRouteChange({ ...location, action });
};
Example #24
Source File: WorkbenchActionList.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function WorkbenchActionList(
props: WorkbenchActionListProps
): React.ReactElement {
const { menu, appId } = props;
const history = getHistory();
const [activeIndex, setActiveIndex] = useState<number>();
const [location, setLocation] = useState<Location>(history.location);
useEffect(() => {
const unlisten: UnregisterCallback = history.listen((location) => {
setLocation(location);
});
return unlisten;
}, []);
useEffect(() => {
if (!currentAppId) currentAppId = appId;
if (currentAppId !== appId) {
historyMap.clear();
currentAppId = appId;
}
}, [appId]);
useEffect(() => {
const { pathname, search } = location;
const { selectedKeys } = initMenuItemAndMatchCurrentPathKeys(
menu?.menuItems ?? [],
pathname,
search,
""
);
setActiveIndex(Number(selectedKeys[0]));
}, [menu, location]);
const handleLinkClick = (item: SidebarMenuSimpleItem): void => {
if (item.href) return;
historyMap.set(activeIndex, `${location.pathname}${location.search}`);
};
return (
<div className={styles.workBenchActionList}>
{menu?.menuItems
?.map((item, index) => {
if (item.type === "default") {
let url = item.to;
if (activeIndex !== index && historyMap.has(index)) {
url = historyMap.get(index);
}
return (
<WorkbenchAction
key={index}
icon={item.icon}
tooltip={item.text}
to={url as string}
href={item.href}
target={item.target}
active={activeIndex === index}
linkClick={() => handleLinkClick(item)}
/>
);
}
})
.filter(Boolean)}
</div>
);
}
Example #25
Source File: filterManager.ts From ke with MIT License | 5 votes |
static setFilters(location: Location, filters: Filter[], history: History): void {
const query = new URLSearchParams(location.search)
FilterManager.setQueryFilters(query, filters)
history.replace({ ...history.location, search: query.toString() })
}
Example #26
Source File: Router.spec.ts From next-core with GNU General Public License v3.0 | 4 votes |
describe("Router", () => {
let router: Router;
const kernel = {
mountPoints: {
main: document.createElement("div"),
bg: document.createElement("div"),
portal: document.createElement("div"),
},
bootstrapData: {
storyboards: [],
},
unsetBars: jest.fn(),
menuBar: {
element: document.createElement("div"),
},
appBar: {
element: document.createElement("div"),
},
getFeatureFlags: mockFeature,
toggleBars: jest.fn(),
firstRendered: jest.fn(),
toggleLegacyIframe: jest.fn(),
updateWorkspaceStack: jest.fn(),
getPreviousWorkspace: jest.fn(),
getRecentApps: jest.fn(),
loadDepsOfStoryboard: jest.fn(),
registerCustomTemplatesInStoryboard: jest.fn(),
fulfilStoryboard: jest.fn(),
loadMicroAppApiOrchestrationAsync: jest.fn(),
prefetchDepsOfStoryboard: jest.fn(),
loadDynamicBricks: jest.fn(),
layoutBootstrap: jest.fn(),
presetBricks: {
pageNotFound: "basic-bricks.page-not-found",
pageError: "basic-bricks.page-error",
},
currentLayout: "console",
} as unknown as Kernel;
beforeEach(() => {
router = new Router(kernel);
apiAnalyzer.create({ api: "fake-api" });
});
afterEach(() => {
historyListeners = [];
__setMatchedStoryboard(undefined);
__setMountRoutesResults(undefined);
jest.clearAllMocks();
});
it("should render matched storyboard", async () => {
const analyticsData = {
prop1: "value",
};
__setMatchedStoryboard({
routes: [],
app: {
id: "hello",
},
});
__setMountRoutesResults(
{
route: {
alias: "route alias",
},
main: [
{
type: "p",
},
],
menuBar: {
title: "menu",
},
appBar: {
title: "app",
},
analyticsData,
},
null
);
expect(router.getState()).toBe("initial");
await router.bootstrap();
expect(router.getState()).toBe("mounted");
expect(spyOnHistory.listen).toBeCalled();
const dispatchedEvent = spyOnDispatchEvent.mock.calls[0][0] as CustomEvent;
expect(dispatchedEvent.type).toBe("app.change");
expect(spyOnMountTree.mock.calls[0][0]).toEqual([{ type: "p" }]);
expect(spyOnMountStaticNode.mock.calls[0][0]).toBe(kernel.menuBar.element);
expect(spyOnMountStaticNode.mock.calls[0][1]).toEqual({
title: "menu",
subMenu: null,
});
expect(spyOnMountStaticNode.mock.calls[1][0]).toBe(kernel.appBar.element);
expect(spyOnMountStaticNode.mock.calls[1][1]).toEqual({ title: "app" });
expect(kernel.toggleBars).not.toBeCalled();
expect(kernel.firstRendered).toBeCalled();
expect(kernel.loadDepsOfStoryboard).toBeCalled();
expect(kernel.registerCustomTemplatesInStoryboard).toBeCalled();
expect(kernel.fulfilStoryboard).toBeCalled();
expect(kernel.loadMicroAppApiOrchestrationAsync).toBeCalled();
expect(kernel.prefetchDepsOfStoryboard).toBeCalled();
expect(preCheckPermissions).toBeCalled();
expect(spyOnMediaEventTargetAddEventListener).toBeCalled();
expect(mockUserAnalyticsEvent).toBeCalledWith("page_view", {
micro_app_id: "hello",
route_alias: "route alias",
...analyticsData,
});
});
it("should redirect to login page if not logged in.", async () => {
__setMatchedStoryboard({
app: {
id: "hello",
},
dependsAll: true,
routes: [],
});
__setMountRoutesResults(
null,
new HttpResponseError(new Response("", { status: 401 }), {
code: 100003,
})
);
await router.bootstrap();
expect(spyOnHistory.replace.mock.calls[0]).toEqual([
"/auth/login",
{
from: {
pathname: "/yo",
},
},
]);
expect(mockConsoleError).toBeCalled();
});
it("should show page error.", async () => {
__setMatchedStoryboard({
app: {
id: "hello",
},
routes: [],
});
__setMountRoutesResults(null, new Error("oops"));
await router.bootstrap();
expect(spyOnHistory.replace).not.toBeCalled();
expect(kernel.layoutBootstrap).toBeCalledWith("console");
expect(kernel.loadDynamicBricks).toBeCalledWith([
"basic-bricks.page-error",
]);
expect(spyOnMountTree).toBeCalledTimes(1);
expect(spyOnMountTree.mock.calls[0][0]).toMatchObject([
{
type: "basic-bricks.page-error",
properties: {
error: "Error: oops",
code: null,
},
},
]);
__setMountRoutesResults(
null,
new HttpResponseError(
new Response("", { statusText: "oops", status: 403 })
)
);
await router.bootstrap();
expect(spyOnMountTree).toBeCalledTimes(2);
expect(spyOnMountTree.mock.calls[1][0]).toMatchObject([
{
type: "basic-bricks.page-error",
properties: {
error: "HttpResponseError: oops",
code: 403,
},
},
]);
});
it("should redirect to login page if redirect exists.", async () => {
__setMatchedStoryboard({
app: {
id: "hello",
},
dependsAll: true,
routes: [],
});
__setMountRoutesResults(
{
flags: {
unauthenticated: true,
redirect: {
path: "/auth/login",
state: {
from: "/private",
},
},
},
},
null
);
await router.bootstrap();
expect(spyOnHistory.replace.mock.calls[0]).toEqual([
"/auth/login",
{
from: {
pathname: "/yo",
},
},
]);
});
it("should render matched storyboard with dependsAll and redirect", async () => {
__setMatchedStoryboard({
app: {
id: "hello",
},
dependsAll: true,
routes: [],
});
__setMountRoutesResults(
{
flags: {
redirect: {
path: "/auth/login",
state: {
from: "/private",
},
},
},
},
null
);
await router.bootstrap();
expect(spyOnHistory.replace.mock.calls[0]).toEqual([
"/auth/login",
{
from: "/private",
},
]);
expect(spyOnMountStaticNode).not.toBeCalled();
expect(spyOnMountTree).not.toBeCalled();
});
it("should render matched storyboard with bars hidden and empty main", async () => {
__setMatchedStoryboard({
app: {
id: "hello",
},
routes: [],
});
__setMountRoutesResults(
{
flags: {
barsHidden: true,
},
main: [],
},
null
);
await router.bootstrap();
expect(kernel.loadDynamicBricks).toBeCalledWith([
"basic-bricks.page-not-found",
]);
expect(kernel.toggleBars).toBeCalledWith(false);
expect(spyOnMountStaticNode).not.toBeCalled();
expect(spyOnMountTree).toBeCalledTimes(1);
expect(spyOnMountTree.mock.calls[0][0]).toMatchObject([
{
type: "basic-bricks.page-not-found",
properties: {
url: "/oops",
},
},
]);
});
it("should handle when page not found", async () => {
await router.bootstrap();
expect(kernel.loadDynamicBricks).toBeCalledWith([
"basic-bricks.page-not-found",
]);
expect(spyOnMountTree).toBeCalledTimes(1);
expect(spyOnMountTree.mock.calls[0][0]).toMatchObject([
{
type: "basic-bricks.page-not-found",
properties: {
url: "/oops",
},
},
]);
});
it("should redirect to login when page not found and not logged in", async () => {
spyOnIsLoggedIn.mockReturnValueOnce(false);
await router.bootstrap();
expect(kernel.loadMicroAppApiOrchestrationAsync).not.toBeCalled();
expect(spyOnMountTree).toBeCalledTimes(0);
expect(spyOnHistory.replace).toBeCalledWith("/auth/login", {
from: {
pathname: "/yo",
},
});
});
it("should ignore rendering if notify is false", async () => {
await router.bootstrap();
jest.clearAllMocks();
mockHistoryPush({
pathname: "/first",
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(1);
mockHistoryPush({
pathname: "/second",
state: {
notify: false,
},
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(1);
});
it("should ignore rendering if location not changed except hash, state and key", async () => {
await router.bootstrap();
jest.clearAllMocks();
mockHistoryPush({
pathname: "/first",
search: "?ok=1",
key: "123",
state: {
from: "earth",
},
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(1);
mockHistoryPush({
pathname: "/first",
search: "?ok=1",
hash: "#good",
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(1);
});
it("should ignore rendering if in situation of goBack after pushAnchor", async () => {
await router.bootstrap();
jest.clearAllMocks();
mockHistoryPush({
pathname: "/first",
search: "?ok=1",
key: "123",
hash: "#yes",
state: {
notify: false,
},
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(0);
mockHistoryPop({
pathname: "/first",
search: "?ok=1",
hash: null,
key: "456",
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(0);
});
it("should render in queue", async () => {
await router.bootstrap();
jest.clearAllMocks();
mockHistoryPush({
pathname: "/first",
});
// `/second` should be ignored and replaced by `/third`.
mockHistoryPush({
pathname: "/second",
});
mockHistoryPush({
pathname: "/third",
});
await (global as any).flushPromises();
expect(spyOnMountTree).toBeCalledTimes(2);
});
it("should notify when location changed", async () => {
const mockImage = jest.spyOn(window, "Image");
mockFeature.mockReturnValue({ "log-location-change": true });
router = new Router(kernel);
await router.bootstrap();
jest.clearAllMocks();
mockHistoryPush({
pathname: "/first",
});
expect(mockImage).toBeCalled();
});
it("should block", async () => {
router = new Router(kernel);
jest.clearAllMocks();
const fireBeforeunload = (): CustomEvent => {
const beforeunloadEvent = new CustomEvent("beforeunload");
jest.spyOn(beforeunloadEvent, "preventDefault");
window.dispatchEvent(beforeunloadEvent);
return beforeunloadEvent;
};
const eventTriggeringNothing = fireBeforeunload();
expect(eventTriggeringNothing.preventDefault).not.toBeCalled();
jest.clearAllMocks();
spyOnHistory.setBlockMessage("Are you sure to leave?");
const eventBlocking = fireBeforeunload();
expect(eventBlocking.preventDefault).toBeCalled();
expect(spyOnHistory.unblock).not.toBeCalled();
spyOnHistory.unblock();
jest.clearAllMocks();
const messageUnblocked = blockFn(
{
pathname: "/home",
} as Location,
"PUSH"
);
expect(messageUnblocked).toBe(undefined);
jest.clearAllMocks();
spyOnHistory.setBlockMessage("Are you sure to leave?");
const messageBlocked = blockFn(
{
pathname: "/home",
} as Location,
"PUSH"
);
expect(messageBlocked).toBe("Are you sure to leave?");
});
});
Example #27
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
ApiDesign = () => {
const [
{
contentKey,
dataTypeFormData,
filterKey,
apiResourceList,
apiDataTypeList,
quotePathMap,
treeModalVisible,
apiModalVisible,
curTreeNodeData,
curApiName,
curDataType,
newTreeNode,
popVisible,
apiDetail,
},
updater,
update,
] = useUpdate({
contentKey: 'SUMMARY',
dataTypeFormData: {},
filterKey: '',
apiResourceList: [] as string[],
apiDataTypeList: [] as string[],
quotePathMap: {} as Obj,
treeModalVisible: false,
apiModalVisible: false,
curTreeNodeData: {},
curApiName: '',
curDataType: '',
newTreeNode: {} as API_SETTING.IFileTree,
popVisible: false,
apiDetail: {},
});
const { inode: inodeQuery, pinode: pinodeQuery } = routeInfoStore.useStore((s) => s.query);
React.useEffect(() => {
const [key] = contentKey.split('&DICE&');
if (key === 'RESOURCE') {
curApiName && updater.contentKey(`${key}&DICE&${curApiName}`);
} else {
curDataType && updater.contentKey(`${key}&DICE&${curDataType}`);
}
}, [curApiName, contentKey, updater, curDataType]);
const { isExternalRepo, repoConfig } = appStore.useStore((s) => s.detail);
const [openApiDoc, apiWs, apiLockState, isDocChanged, wsQuery, formErrorNum, isApiReadOnly, lockUser, docValidData] =
apiDesignStore.useStore((s) => [
s.openApiDoc,
s.apiWs,
s.apiLockState,
s.isDocChanged,
s.wsQuery,
s.formErrorNum,
s.isApiReadOnly,
s.lockUser,
s.docValidData,
]);
const {
updateOpenApiDoc,
createTreeNode,
commitSaveApi,
getApiDetail,
publishApi,
updateFormErrorNum,
resetDocValidData,
} = apiDesignStore;
const [getApiDocDetailLoading, commitSaveApiLoading, getTreeListLoading] = useLoading(apiDesignStore, [
'getApiDetail',
'commitSaveApi',
'getTreeList',
]);
useMount(() => {
window.addEventListener('beforeunload', beforeunload);
});
useUnmount(() => {
updateOpenApiDoc({});
apiWs && apiWs.close();
window.removeEventListener('beforeunload', beforeunload);
});
const changeRef = React.useRef(null as any);
React.useEffect(() => {
changeRef.current = isDocChanged;
}, [isDocChanged]);
const beforeunload = React.useCallback((e) => {
const msg = `${i18n.t('dop:not saved yet, confirm to leave')}?`;
if (changeRef.current) {
// eslint-disable-next-line no-param-reassign
(e || window.event).returnValue = msg;
}
return msg;
}, []);
const apiResourceMap = React.useMemo(() => {
const tempMap = openApiDoc?.paths || {};
const fullKeys = keys(tempMap);
let tempList = [];
if (filterKey) {
tempList = filter(keys(tempMap), (name) => name.indexOf(filterKey) > -1);
} else {
tempList = fullKeys;
}
updater.apiResourceList(tempList);
return tempMap;
}, [filterKey, openApiDoc, updater]);
const apiDataTypeMap = React.useMemo(() => {
const tempMap = openApiDoc?.components?.schemas || {};
const fullKeys = keys(tempMap);
let tempList = [];
if (filterKey) {
tempList = filter(fullKeys, (name) => name.indexOf(filterKey) > -1);
} else {
tempList = fullKeys;
}
updater.apiDataTypeList(tempList);
return tempMap;
}, [filterKey, openApiDoc, updater]);
const onCreateDoc = (values: { name: string; pinode: string }) => {
createTreeNode(values).then((res) => {
updater.newTreeNode(res);
});
updater.treeModalVisible(false);
};
const onContentChange = React.useCallback(
(contentName: string) => {
const nextHandle = () => {
updateFormErrorNum(0);
const [, name] = contentName.split('&DICE&');
updater.contentKey(contentName);
if (contentName.startsWith('RESOURCE') && name) {
updater.curApiName(name);
const tempApiDetail = get(openApiDoc, ['paths', name]) || {};
updater.apiDetail(tempApiDetail);
}
if (contentName.startsWith('DATATYPE')) {
const _fromData = apiDataTypeMap[name] || { type: 'string', example: 'Example', 'x-dice-name': name };
updater.dataTypeFormData({ ..._fromData, name });
updater.curDataType(name);
}
};
if (formErrorNum > 0) {
confirm({
title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
onOk() {
nextHandle();
},
});
} else {
nextHandle();
}
},
[apiDataTypeMap, formErrorNum, openApiDoc, updateFormErrorNum, updater],
);
const dataTypeNameMap = React.useMemo(() => {
return keys(get(openApiDoc, ['components', 'schemas']));
}, [openApiDoc]);
const apiNameMap = React.useMemo(() => {
return keys(openApiDoc.paths || {});
}, [openApiDoc]);
const onAddHandle = (addKey: IListKey) => {
let newData = {};
let newName = `/api/new${apiResourceList.length}`;
while (apiNameMap.includes(newName)) {
newName += '1';
}
let dataPath = ['paths', newName];
if (addKey === 'DATATYPE') {
newName = `NewDataType${apiDataTypeList.length}`;
newData = { type: 'string', example: 'Example', 'x-dice-name': newName };
dataPath = ['components', 'schemas', newName];
}
const tempDocDetail = produce(openApiDoc, (draft) => set(draft, dataPath, newData));
updateOpenApiDoc(tempDocDetail);
onContentChange(`${addKey}&DICE&${newName}`);
};
const onDeleteHandle = (itemKey: string) => {
const [key, name] = itemKey.split('&DICE&');
if (key === 'DATATYPE') {
const newQuoteMap = getQuoteMap(openApiDoc);
if (newQuoteMap[name]?.length) {
message.warning(i18n.t('dop:this type is referenced and cannot be deleted'));
return;
}
} else if (key === 'RESOURCE') {
const paths = keys(openApiDoc.paths);
if (paths.length === 1) {
message.warning(i18n.t('dop:at least one API needs to be kept'));
return;
}
}
const dataPath = key === 'RESOURCE' ? ['paths', name] : ['components', 'schemas', name];
const tempDocDetail = produce(openApiDoc, (draft) => {
unset(draft, dataPath);
});
updateOpenApiDoc(tempDocDetail);
onContentChange('SUMMARY');
};
// 左侧列表头部渲染
const renderPanelHead = (titleKey: IListKey) => (
<div className="list-panel-head inline-flex justify-between items-center">
<span className="font-bold">{LIST_TITLE_MAP[titleKey]}</span>
{!apiLockState && (
<ErdaIcon
type="plus"
className="mr-0 cursor-pointer"
size="16px"
onClick={(e) => {
e.stopPropagation();
onAddHandle(titleKey);
}}
/>
)}
</div>
);
// 左侧列表渲染
const renderListItem = (listKey: IListKey, name: string) => {
const apiData = apiResourceMap[name] || {};
const key = `${listKey}&DICE&${name}`;
return (
<LazyRender key={name} minHeight={listKey === 'RESOURCE' ? '58px' : '37px'}>
<div
className={`list-title ${contentKey === key ? 'list-title-active' : ''}`}
onClick={() => onContentChange(key)}
>
<div className="flex justify-between items-center">
<Ellipsis title={name}>
<div className="list-title-name w-full nowrap mr-1">{name}</div>
</Ellipsis>
<Popconfirm
title={`${i18n.t('common:confirm to delete')}?`}
onConfirm={(e: any) => {
e.stopPropagation();
onDeleteHandle(key);
}}
onCancel={(e: any) => e.stopPropagation()}
>
{!apiLockState && (
<CustomIcon
type="shanchu"
className="list-title-btn cursor-pointer"
onClick={(e) => e?.stopPropagation()}
/>
)}
</Popconfirm>
</div>
{listKey === 'RESOURCE' && (
<div className="method-list">
{map(API_METHODS, (methodKey: API_SETTING.ApiMethod) => {
const methodIconClass = !isEmpty(apiData[methodKey]) ? `method-icon-${methodKey}` : '';
return (
<Tooltip title={methodKey} key={methodKey}>
<div className={`method-icon mr-2 ${methodIconClass}`} />
</Tooltip>
);
})}
</div>
)}
</div>
</LazyRender>
);
};
// 获取所有引用的pathMap
const getQuoteMap = React.useCallback(
(data: Obj) => {
const getQuotePath = (innerData: Obj, prefixPath: Array<number | string>, pathMap: Obj) => {
const refTypePath = get(innerData, [QUOTE_PREFIX, 0, '$ref']) || innerData[QUOTE_PREFIX_NO_EXTENDED];
if (refTypePath) {
const _type = refTypePath.split('/').slice(-1)[0];
// eslint-disable-next-line no-param-reassign
!pathMap[_type] && (pathMap[_type] = []);
if (!pathMap[_type].includes(prefixPath)) {
pathMap[_type].push(prefixPath);
}
}
if (innerData?.properties) {
forEach(keys(innerData.properties), (item) => {
getQuotePath(innerData.properties[item], [...prefixPath, 'properties', item], pathMap);
});
}
if (innerData?.items) {
getQuotePath(innerData.items, [...prefixPath, 'items'], pathMap);
}
};
const tempMap = {};
const pathMap = data.paths;
forEach(keys(pathMap), (path) => {
const pathData = pathMap[path];
forEach(keys(pathData), (method) => {
const methodData = pathData[method];
const _path = ['paths', path, method];
forEach(API_MEDIA_TYPE, (mediaType) => {
// responses
const responsePath = ['responses', '200', 'content', mediaType, 'schema'];
const responseData = get(methodData, responsePath) || {};
getQuotePath(responseData, [..._path, ...responsePath], tempMap);
// requestBody;
const requestBodyPath = ['requestBody', 'content', mediaType, 'schema'];
const requestBody = get(methodData, requestBodyPath) || {};
getQuotePath(requestBody, [..._path, ...requestBodyPath], tempMap);
});
// parameters
const parametersData = methodData.parameters || [];
forEach(parametersData, (pData, index) => {
getQuotePath(pData, [..._path, 'parameters', index], tempMap);
});
});
});
// datatype中的引用
const dataTypeData = data?.components?.schemas || {};
forEach(keys(dataTypeData), (dataTypeName) => {
getQuotePath(dataTypeData[dataTypeName], ['components', 'schemas', dataTypeName], tempMap);
});
updater.quotePathMap(tempMap);
return tempMap;
},
[updater],
);
const onQuotePathMapChange = React.useCallback(
(pathMap: Obj) => {
updater.quotePathMap(pathMap);
},
[updater],
);
const onApiNameChange = React.useCallback(
(name: string) => {
updater.curApiName(name);
},
[updater],
);
const renderContent = (key: string) => {
if (key.startsWith('RESOURCE')) {
return (
<ApiResource
onQuoteChange={onQuotePathMapChange}
onApiNameChange={onApiNameChange}
quotePathMap={quotePathMap}
apiName={curApiName}
apiDetail={apiDetail}
/>
);
} else if (key.startsWith('DATATYPE')) {
return (
<DataTypeConfig
quotePathMap={quotePathMap}
dataTypeNameMap={dataTypeNameMap}
formData={dataTypeFormData}
key={dataTypeFormData?.name}
dataType={curDataType}
onQuoteNameChange={onQuotePathMapChange}
onDataTypeNameChange={(name) => updater.curDataType(name)}
isEditMode={!apiLockState}
/>
);
} else {
return <ApiSummary />;
}
};
const isDocLocked = React.useMemo(() => {
return wsQuery?.sessionID && apiLockState;
}, [apiLockState, wsQuery]);
const LockTipVisible = React.useMemo(() => isApiReadOnly || isDocLocked, [isApiReadOnly, isDocLocked]);
const docLockTip = React.useMemo(() => {
if (isApiReadOnly) {
return i18n.t('dop:protect branch, not editable');
} else if (isDocLocked) {
return lockUser + API_LOCK_WARNING;
} else {
return '';
}
}, [isApiReadOnly, isDocLocked, lockUser]);
const errorData = React.useMemo(() => {
return {
branchName: curTreeNodeData.branchName,
docName: `${curTreeNodeData.apiDocName}.yaml`,
msg: docValidData.msg,
};
}, [curTreeNodeData, docValidData]);
const onEditDocHandle = () => {
if (!apiWs) {
initApiWs({ inode: inodeQuery, pinode: pinodeQuery });
} else if (isDocLocked) {
message.warning(lockUser + API_LOCK_WARNING);
}
};
const onPublishApi = React.useCallback(
(values: any) => {
publishApi(values).then(() => {
apiWs && apiWs.close();
getApiDetail(inodeQuery as string).then((data: any) => {
getQuoteMap(data.openApiDoc);
updater.curTreeNodeData({
...curTreeNodeData,
asset: data.asset,
});
});
});
},
[apiWs, curTreeNodeData, getApiDetail, getQuoteMap, inodeQuery, publishApi, updater],
);
const onSelectDoc = React.useCallback(
(nodeData, reset) => {
if (reset) {
updateOpenApiDoc({});
resetDocValidData();
}
onContentChange('Summary');
update({
contentKey: 'SUMMARY',
curTreeNodeData: nodeData,
newTreeNode: {} as API_SETTING.IFileTree,
filterKey: '',
});
},
[onContentChange, resetDocValidData, update, updateOpenApiDoc],
);
const onToggleTreeVisible = React.useCallback(
(val: boolean) => {
updater.popVisible(val);
},
[updater],
);
const onConfirmPublish = React.useCallback(() => {
if (isDocChanged) {
confirm({
title: i18n.t('dop:The current document has not been saved. Publish the saved document?'),
onOk() {
updater.apiModalVisible(true);
},
});
} else {
updater.apiModalVisible(true);
}
}, [isDocChanged, updater]);
const showErrorDocTip = React.useMemo(() => {
return !docValidData.valid && !isDocChanged && !isEmpty(openApiDoc);
}, [docValidData.valid, isDocChanged, openApiDoc]);
return isExternalRepo === undefined ? (
<EmptyHolder relative />
) : (
<div className="api-design">
<TopButtonGroup>
<Button type="primary" onClick={() => updater.treeModalVisible(true)}>
{i18n.t('dop:New Document')}
</Button>
</TopButtonGroup>
<div className="api-design-wrap">
<div className="search-wrap mb-4 flex items-center justify-start">
<ApiDocTree
treeNodeData={curTreeNodeData}
newTreeNode={newTreeNode}
getQuoteMap={getQuoteMap}
onSelectDoc={onSelectDoc}
popVisible={popVisible}
onVisibleChange={onToggleTreeVisible}
/>
{LockTipVisible && (
<span className="ml-4">
<CustomIcon type="lock" />
{docLockTip}
</span>
)}
{showErrorDocTip && <ErrorPopover {...errorData} />}
{inodeQuery && !isEmpty(curTreeNodeData) && (
<div className="flex items-center flex-wrap justify-end flex-1">
{!apiWs || isDocLocked ? (
<WithAuth pass={!isApiReadOnly && docValidData.valid}>
<Button type="ghost" onClick={onEditDocHandle}>
{i18n.t('Edit')}
</Button>
</WithAuth>
) : (
<Button type="ghost" disabled={formErrorNum > 0} onClick={() => commitSaveApi()}>
{i18n.t('Save')}
</Button>
)}
<WithAuth pass={inodeQuery && docValidData.valid}>
<Button type="primary" className="ml-2" onClick={onConfirmPublish}>
{i18n.t('publisher:Release')}
</Button>
</WithAuth>
</div>
)}
</div>
<Spin spinning={getApiDocDetailLoading || commitSaveApiLoading || getTreeListLoading}>
{isEmpty(openApiDoc) ? (
<ErrorEmptyHolder {...errorData} isLoading={getTreeListLoading} />
) : (
<div className="api-design-content">
<div className="api-design-content-list flex flex-col justify-start">
<Input
placeholder={i18n.t('Search by keyword')}
className="mx-2 my-3 api-filter-input"
prefix={<ErdaIcon type="search1" size="14" className="mr-0.5 mt-0.5" />}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => updater.filterKey(e.target.value)}
/>
<div
className={`list-title py-3 border-bottom font-bold ${
contentKey === 'SUMMARY' ? 'list-title-active' : ''
}`}
onClick={() => onContentChange('SUMMARY')}
>
{i18n.t('dop:API overview')}
</div>
<div className="panel-list">
<Collapse
accordion
bordered={false}
defaultActiveKey={['RESOURCE']}
className="api-overview-collapse"
>
<Panel header={renderPanelHead('RESOURCE')} key="RESOURCE">
{!isEmpty(apiResourceList) ? (
map(apiResourceList, (name) => renderListItem('RESOURCE', name))
) : (
<EmptyHolder relative />
)}
</Panel>
<Panel header={renderPanelHead('DATATYPE')} key="DATATYPE">
{!isEmpty(apiDataTypeList) ? (
map(apiDataTypeList, (name) => renderListItem('DATATYPE', name))
) : (
<EmptyHolder relative />
)}
</Panel>
</Collapse>
</div>
</div>
<div className="api-design-content-detail px-4 py-3">{renderContent(contentKey)}</div>
</div>
)}
</Spin>
<ApiDocAddModal
visible={treeModalVisible}
onClose={() => updater.treeModalVisible(false)}
onSubmit={onCreateDoc}
/>
<ApiPublishModal
visible={apiModalVisible}
treeNodeData={curTreeNodeData as API_SETTING.ITreeNodeData}
onSubmit={onPublishApi}
onClose={() => updater.apiModalVisible(false)}
/>
</div>
<Prompt
when={isDocChanged}
message={(location: Location) => {
if (location.pathname.endsWith('apiDesign')) {
return false;
}
return `${i18n.t('dop:not saved yet, confirm to leave')}?`;
}}
/>
</div>
);
}
Example #28
Source File: Editor.tsx From legend-studio with Apache License 2.0 | 4 votes |
EditorInner = observer(() => {
const params = useParams<EditorPathParams | GroupEditorPathParams>();
const projectId = params.projectId;
const workspaceType = (params as { groupWorkspaceId: string | undefined })
.groupWorkspaceId
? WorkspaceType.GROUP
: WorkspaceType.USER;
const workspaceId =
workspaceType === WorkspaceType.GROUP
? (params as GroupEditorPathParams).groupWorkspaceId
: (params as EditorPathParams).workspaceId;
const editorStore = useEditorStore();
const applicationStore = useApplicationStore();
// Extensions
const extraEditorExtensionComponents = editorStore.pluginManager
.getStudioPlugins()
.flatMap(
(plugin) =>
plugin.getExtraEditorExtensionComponentRendererConfigurations?.() ?? [],
)
.filter(isNonNullable)
.map((config) => (
<Fragment key={config.key}>{config.renderer(editorStore)}</Fragment>
));
// Resize
const { ref, width, height } = useResizeDetector<HTMLDivElement>();
// These create snapping effect on panel resizing
const resizeSideBar = (handleProps: ResizablePanelHandlerProps): void =>
editorStore.sideBarDisplayState.setSize(
(handleProps.domElement as HTMLDivElement).getBoundingClientRect().width,
);
const resizeAuxPanel = (handleProps: ResizablePanelHandlerProps): void =>
editorStore.auxPanelDisplayState.setSize(
(handleProps.domElement as HTMLDivElement).getBoundingClientRect().height,
);
useEffect(() => {
if (ref.current) {
editorStore.auxPanelDisplayState.setMaxSize(ref.current.offsetHeight);
}
}, [editorStore, ref, height, width]);
// Hotkeys
const [hotkeyMapping, hotkeyHandlers] = buildReactHotkeysConfiguration(
editorStore.hotkeys,
);
// Cleanup the editor
useEffect(() => (): void => editorStore.cleanUp(), [editorStore]);
// Initialize the app
useEffect(() => {
flowResult(
editorStore.initialize(projectId, workspaceId, workspaceType),
).catch(applicationStore.alertUnhandledError);
}, [editorStore, applicationStore, projectId, workspaceId, workspaceType]);
// Browser Navigation Blocking (reload, close tab, go to another URL)
// NOTE: there is no way to customize the alert message for now since Chrome removed support for it
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Browser_compatibility
// There is also no way to customize this modal style-wise so we would only be able to do so for route navigation blocking
// See https://medium.com/@jozollo/blocking-navigation-with-react-router-v4-a3f2e359d096
useEffect(() => {
const onUnload = (event: BeforeUnloadEvent): void => {
/**
* NOTE: one subtle trick here. Since we have to use `useEffect` to set the event listener for `beforeunload` event,
* we have to be be careful, if we extract `editorStore.ignoreNavigationBlock` out to a variable such as `ignoreNavigationBlock`
* and then make this `useEffect` be called in response to that, there is a chance that if we set `editorStore.ignoreNavigationBlock`
* to `true` and then go on to call systematic refresh (i.e. `window.location.reload()`) immediately, the event listener on window will
* become stale and still show the blocking popup.
*
* This is almost guaranteed to happen as `useEffect` occurs after rendering, and thus will defnitely be called after the immediate
* `window.location.reload()`. As such, the best way is instead of expecting `useEffect` to watch out for the change in `ignoreNavigationBlock`
* we will access the value of `ignoreNavigationBlock` in the body of the `onUnload` function to make it more dynamic. This ensures the
* event listener will never go stale
*/
const showAlert =
editorStore.isInConflictResolutionMode ||
editorStore.hasUnpushedChanges;
if (!editorStore.ignoreNavigationBlocking && showAlert) {
event.returnValue = '';
}
};
window.removeEventListener('beforeunload', onUnload);
window.addEventListener('beforeunload', onUnload);
return (): void => window.removeEventListener('beforeunload', onUnload);
}, [editorStore]);
// Route Navigation Blocking
// See https://medium.com/@michaelchan_13570/using-react-router-v4-prompt-with-custom-modal-component-ca839f5faf39
const [blockedLocation, setBlockedLocation] = useState<
Location | undefined
>();
const retryBlockedLocation = useCallback(
(allowedNavigation: boolean): void => {
if (allowedNavigation && blockedLocation) {
applicationStore.navigator.goTo(blockedLocation.pathname);
}
},
[blockedLocation, applicationStore],
);
// NOTE: we have to use `useStateWithCallback` here because we want to guarantee that we call `history.push(blockedLocation.pathname)`
// after confirmedAllowNavigation is flipped, otherwise we would end up in the `false` case of handleBlockedNavigation again!
// Another way to go about this is to use `setTimeout(() => history.push(...), 0)` but it can potentially be more error-prone
// See https://www.robinwieruch.de/react-usestate-callback
const [confirmedAllowNavigation, setConfirmedAllowNavigation] =
useStateWithCallback<boolean>(false, retryBlockedLocation);
const onNavigationChangeIndicator = Boolean(
editorStore.changeDetectionState.workspaceLocalLatestRevisionState.changes
.length,
);
const handleRouteNavigationBlocking = (nextLocation: Location): boolean => {
// NOTE: as long as we're in conflict resolution, we want this block to be present
const showAlert =
editorStore.isInConflictResolutionMode || editorStore.hasUnpushedChanges;
if (
!editorStore.ignoreNavigationBlocking &&
!confirmedAllowNavigation &&
showAlert
) {
editorStore.setActionAlertInfo({
message: editorStore.isInConflictResolutionMode
? 'You have not accepted the conflict resolution, the current resolution will be discarded. Leave anyway?'
: 'You have unpushed changes. Leave anyway?',
type: ActionAlertType.CAUTION,
onEnter: (): void => editorStore.setBlockGlobalHotkeys(true),
onClose: (): void => editorStore.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Leave this page',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: (): void => setConfirmedAllowNavigation(true),
},
{
label: 'Stay on this page',
type: ActionAlertActionType.PROCEED,
default: true,
handler: (): void => setBlockedLocation(undefined),
},
],
});
setBlockedLocation(nextLocation);
return false;
}
// Reset the confirm flag and the blocked location here
setBlockedLocation(undefined);
setConfirmedAllowNavigation(false);
return true;
};
const editable =
editorStore.graphManagerState.graphBuildState.hasCompleted &&
editorStore.isInitialized;
const isResolvingConflicts =
editorStore.isInConflictResolutionMode &&
!editorStore.conflictResolutionState.hasResolvedAllConflicts;
const promptComponent = (
<Prompt
when={onNavigationChangeIndicator}
message={handleRouteNavigationBlocking}
/>
);
useApplicationNavigationContext(
LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT.EDITOR,
);
return (
<div className="app__page">
<div className="editor">
{promptComponent}
<GlobalHotKeys
keyMap={hotkeyMapping}
handlers={hotkeyHandlers}
allowChanges={true}
>
<div className="editor__body">
<ActivityBar />
<Backdrop className="backdrop" open={editorStore.backdrop} />
<div ref={ref} className="editor__content-container">
<div className="editor__content">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.sideBarDisplayState.size === 0,
{
onStopResize: resizeSideBar,
},
)}
size={editorStore.sideBarDisplayState.size}
direction={1}
>
<SideBar />
</ResizablePanel>
<ResizablePanelSplitter />
<ResizablePanel minSize={300}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.auxPanelDisplayState.isMaximized,
)}
>
{(isResolvingConflicts || editable) &&
editorStore.isInFormMode && <EditPanel />}
{editable && editorStore.isInGrammarTextMode && (
<GrammarTextEditor />
)}
{!editable && <EditPanelSplashScreen />}
</ResizablePanel>
<ResizablePanelSplitter>
<ResizablePanelSplitterLine
color={
editorStore.auxPanelDisplayState.isMaximized
? 'transparent'
: 'var(--color-dark-grey-250)'
}
/>
</ResizablePanelSplitter>
<ResizablePanel
{...getControlledResizablePanelProps(
editorStore.auxPanelDisplayState.size === 0,
{
onStopResize: resizeAuxPanel,
},
)}
flex={0}
direction={-1}
size={editorStore.auxPanelDisplayState.size}
>
<AuxiliaryPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</div>
</div>
{extraEditorExtensionComponents}
<StatusBar actionsDisabled={!editable} />
{editable && <ProjectSearchCommand />}
{editorStore.localChangesState.workspaceSyncState
.workspaceSyncConflictResolutionState.showModal && (
<WorkspaceSyncConflictResolver />
)}
</GlobalHotKeys>
</div>
</div>
);
})
Example #29
Source File: LocationContext.spec.ts From next-core with GNU General Public License v3.0 | 4 votes |
describe("LocationContext", () => {
const kernel: Kernel = {
mountPoints: {
main: {},
bg: document.createElement("div"),
portal: {},
},
bootstrapData: {
storyboards: [],
brickPackages: [
{
filePath: "all.js",
},
],
},
nextApp: {
id: "hello",
name: "Hello",
homepage: "/hello",
},
unsetBars: jest.fn(),
menuBar: {
element: {},
},
appBar: {
element: {
breadcrumb: [],
},
},
toggleBars: jest.fn(),
getFeatureFlags: jest.fn().mockReturnValue({
testing: true,
}),
loadDynamicBricksInBrickConf: jest.fn(),
} as any;
const getInitialMountResult = (): MountRoutesResult => ({
main: [],
menuInBg: [],
portal: [],
menuBar: {},
appBar: {
breadcrumb: kernel.appBar.element.breadcrumb,
},
flags: {
redirect: undefined,
},
});
afterEach(() => {
spyOnIsLoggedIn.mockReset();
jest.clearAllMocks();
});
describe("matchStoryboard", () => {
const storyboards: RuntimeStoryboard[] = [
{
app: {
id: "hello",
name: "Hello",
homepage: "/hello",
},
routes: [
{
path: "/hello",
public: true,
bricks: [],
},
],
},
{
app: {
id: "hello-world",
name: "Hello World",
homepage: "/hello/world",
},
routes: [
{
path: "/hello/world",
public: true,
bricks: [],
},
],
},
{
app: {
id: "oops",
name: "OOPS",
// Legacy storyboard with no homepage.
homepage: null,
},
},
];
it("should handle match missed", () => {
const context = new LocationContext(kernel, {
pathname: "/",
search: "",
hash: "",
state: {},
});
const storyboard = context.matchStoryboard(storyboards);
expect(storyboard).toBe(undefined);
});
it("should handle match hit", () => {
const context = new LocationContext(kernel, {
pathname: "/hello",
search: "",
hash: "",
state: {},
});
const storyboard = context.matchStoryboard(storyboards);
expect(storyboard.app.id).toBe("hello");
});
it("should handle match hit sub-path", () => {
const context = new LocationContext(kernel, {
pathname: "/hello/everyone",
search: "",
hash: "",
state: {},
});
const storyboard = context.matchStoryboard(storyboards);
expect(storyboard.app.id).toBe("hello");
});
it("should handle match more precisely", () => {
const context = new LocationContext(kernel, {
pathname: "/hello/world",
search: "",
hash: "",
state: {},
});
const storyboard = context.matchStoryboard(storyboards);
expect(storyboard.app.id).toBe("hello-world");
});
it("should handle standalone micro-apps", () => {
window.STANDALONE_MICRO_APPS = true;
const context = new LocationContext(kernel, {
pathname: "/hello/world",
search: "",
hash: "",
state: {},
});
const storyboard = context.matchStoryboard(storyboards.slice(0, 1));
expect(storyboard.app.id).toBe("hello");
window.STANDALONE_MICRO_APPS = false;
});
});
describe("mountRoutes", () => {
it("should mount nothing if match missed", async () => {
const context = new LocationContext(kernel, {
pathname: "/hello",
search: "",
hash: "",
state: {},
});
const result = await context.mountRoutes(
[],
undefined,
getInitialMountResult()
);
expect(result).toMatchObject({
main: [],
menuBar: {},
appBar: {},
flags: {
redirect: undefined,
},
});
});
it("should redirect if not logged in", async () => {
const location = {
pathname: "/",
search: "",
hash: "",
state: {},
};
const context = new LocationContext(kernel, location);
spyOnIsLoggedIn.mockReturnValueOnce(false);
const result = await context.mountRoutes(
[
{
path: "/",
bricks: [],
},
],
undefined,
getInitialMountResult()
);
expect(result).toMatchObject({
main: [],
menuBar: {},
appBar: {},
flags: {
unauthenticated: true,
redirect: undefined,
},
});
});
it("should redirect if match redirected", async () => {
const context = new LocationContext(kernel, {
pathname: "/hello",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(true);
const result = await context.mountRoutes(
[
{
path: "/hello",
redirect: "/oops",
},
],
undefined,
getInitialMountResult()
);
expect(result).toMatchObject({
main: [],
menuBar: {},
appBar: {},
flags: {
redirect: {
path: "/oops",
},
},
});
});
it("should validate permissions when permissionsPreCheck has been defined", async () => {
const context = new LocationContext(kernel, {
pathname: "/",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(true);
spyOnGetCurrentContext.mockReturnValueOnce(context.getCurrentContext());
await context.mountRoutes(
[
{
path: "/",
permissionsPreCheck: ["<% CTX.instanceUpdateAction %>"],
bricks: [],
context: [
{
name: "instanceUpdateAction",
value: "cmdb_HOST_instance_update",
},
],
},
],
undefined,
getInitialMountResult()
);
expect(validatePermissions).toHaveBeenCalledWith([
"cmdb_HOST_instance_update",
]);
});
it("should ignore validating permissions if not logged in", async () => {
const context = new LocationContext(kernel, {
pathname: "/",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(false);
await context.mountRoutes(
[
{
path: "/",
permissionsPreCheck: ["<% CTX.instanceUpdateAction %>"],
bricks: [],
},
],
undefined,
getInitialMountResult()
);
expect(validatePermissions).not.toBeCalled();
});
it("should mount if match hit", async () => {
const context = new LocationContext(kernel, {
pathname: "/",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(true);
spyOnGetCurrentContext.mockReturnValue(context.getCurrentContext());
spyOnGetResolver.mockReturnValueOnce({
async resolveOne(
type: any,
resolveConf: ResolveConf,
conf: Record<string, any>
): Promise<void> {
Object.assign(conf, resolveConf.transform);
},
} as any);
jest
.spyOn(context.resolver, "resolve")
.mockImplementation((brickConf) => {
if (brickConf.lifeCycle?.useResolves?.length > 0) {
return Promise.reject(new ResolveRequestError("Invalid request"));
}
return Promise.resolve();
});
const result = await context.mountRoutes(
[
{
path: "/",
providers: [
"provider-a",
{
brick: "provider-b",
properties: {
args: ["good"],
},
},
],
context: [
{
name: "myFreeContext",
value: "bad",
if: "<% FLAGS['should-not-enabled'] %>",
},
{
name: "myFreeContext",
value: "good",
if: "<% !FLAGS['should-not-enabled'] %>",
},
{
name: "myAsyncContext",
resolve: {
provider: "provider-c",
transform: {
value: "even better",
},
},
},
{
name: "myAsyncContext",
resolve: {
provider: "provider-d",
transform: {
value: "turns worse",
},
if: "<% CTX.myFreeContext === 'bad' %>",
},
},
{
name: "myFallbackToValueContext",
resolve: {
provider: "provider-c",
transform: {
value: "provider return value",
},
if: "<% CTX.myFreeContext === 'bad' %>",
},
value: "default value",
},
],
type: "routes",
routes: [
{
alias: "route alias",
path: "/",
analyticsData: {
prop1: "<% CTX.myAsyncContext %>",
},
bricks: [
{
if: "${FLAGS.testing}",
brick: "div",
iid: "5df55280297f6",
properties: {
title:
"<% `${CTX.myFreeContext} ${CTX.myAsyncContext} ${CTX.myFallbackToValueContext}` %>",
useBrick: {
brick: "useBrick-a",
properties: {
useBrick: {
brick: "useBrick-in-useBrick-b",
slots: {
content: {
bricks: [
{
brick: "slots-useBrick-in-useBrick-c",
},
],
type: "bricks",
},
},
},
},
slots: {
content: {
bricks: [
{
brick: "slots-in-useBrick-d",
slots: "error",
},
{
brick: "slots-in-useBrick-g",
slots: null,
},
{
brick: "slots-in-useBrick-h",
slots: [
{
brick: "slots-in-brick-i",
},
],
},
],
type: "bricks",
},
error1: {
bricks: {
brick: "error-brick",
},
},
error2: {
bricks: [
{
useBrick: "1",
slots: {
content: "slots-in-brick-j",
},
},
{
useBrick: 2,
},
{
useBrick: null,
},
{
useBrick: undefined,
},
],
},
},
},
columns: [
{
title: "title-1",
label: "label-1",
useBrick: true,
},
{
title: "title-2",
label: "label-2",
useBrick: {
brick: "deep-useBrick-e",
properties: {
useBrick: {
brick: "deep-useBrick-in-useBrick-f",
},
},
},
},
],
},
context: [
{
name: "myNewPropContext",
property: "title",
},
{
name: "myFreeContextDefinedOnBrick",
value: "some value",
onChange: {
action: "console.log",
},
},
],
exports: {
title: "CTX.myPropContext",
},
events: {
click: {
action: "history.push",
},
},
lifeCycle: {
onBeforePageLoad: {
action: "theme.setDarkTheme",
},
onPageLoad: {
action: "console.log",
},
onBeforePageLeave: {
action: "console.log",
},
onPageLeave: {
action: "console.log",
},
onAnchorLoad: {
action: "console.log",
},
onAnchorUnload: {
action: "console.log",
},
onMediaChange: {
action: "console.log",
},
onMessageClose: {
action: "console.log",
},
onMessage: {
channel: "pipelineChannel",
handlers: {
action: "console.log",
},
},
onScrollIntoView: {
threshold: 0.5,
handlers: [
{
action: "console.log",
},
],
},
},
slots: {
menu: {
type: "bricks",
bricks: [
{
brick: "p",
},
],
},
content: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: {
sidebarMenu: {
title: "menu title",
menuItems: [],
menuId: "test-menu-id",
},
pageTitle: "page title",
breadcrumb: {
items: [
{
text: "first breadcrumb",
},
],
},
},
},
],
},
extendA: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: {
type: "brick",
brick: "a",
},
},
],
},
extendB: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: {
type: "brick",
brick: "b",
events: {},
lifeCycle: {
onPageLoad: {
action: "console.info",
},
onBeforePageLeave: {
action: "console.info",
},
onPageLeave: {
action: "console.info",
},
onAnchorLoad: {
action: "console.info",
args: ["${EVENT.detail.anchor}"],
},
onAnchorUnload: {
action: "console.info",
},
onMessageClose: {
action: "console.info",
},
},
},
},
],
},
extendC: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: {
breadcrumb: {
overwrite: true,
items: [
{
text: "second breadcrumb",
},
],
noCurrentApp: true,
},
},
},
],
},
extendD: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: false,
},
],
},
extendE: {
type: "routes",
routes: [
{
path: "/",
bricks: [],
menu: {},
},
],
},
extendF: {
type: "invalid",
routes: [],
},
extendG: {
type: "routes",
routes: [
{
path: "/",
bricks: [
{
brick: "modal-a",
portal: true,
properties: {
args: ["a"],
},
slots: {
content: {
type: "bricks",
bricks: [
{
brick: "h2",
properties: {
textContent: "modal heading",
},
},
{
brick: "p",
portal: true,
properties: {
textContent: "portal in portal",
},
},
],
},
},
},
],
menu: {},
},
],
},
},
},
{
if: "${FLAGS.testing|not}",
brick: "div",
},
{
brick: "brick-will-fail",
lifeCycle: {
useResolves: [
{
useProvider: "provider-will-reject",
},
],
},
},
{
template: "template-will-fail",
lifeCycle: {
useResolves: [
{
useProvider: "provider-will-reject",
},
],
},
},
],
},
],
},
] as any,
undefined,
getInitialMountResult()
);
expect(result).toMatchObject({
route: expect.objectContaining({
path: "/",
alias: "route alias",
}),
menuBar: {
menu: {
title: "menu title",
menuItems: [],
menuId: "test-menu-id",
},
},
appBar: {
pageTitle: "page title",
breadcrumb: [
{
text: "second breadcrumb",
},
],
noCurrentApp: true,
},
flags: {
barsHidden: true,
redirect: undefined,
},
analyticsData: {
prop1: "even better",
},
});
expect(result.main).toMatchObject([
{
type: "div",
properties: {
title: "good even better default value",
},
events: {
click: {
action: "history.push",
},
},
children: [
{
type: "p",
slotId: "menu",
},
],
},
{
type: "basic-bricks.brick-error",
properties: {
brickName: "brick-will-fail",
errorType: "ResolveRequestError",
errorMessage: "Invalid request",
isLegacyTemplate: false,
},
},
{
type: "basic-bricks.brick-error",
properties: {
brickName: "template-will-fail",
errorType: "ResolveRequestError",
errorMessage: "Invalid request",
isLegacyTemplate: true,
},
},
]);
const { storyboardContext } = result.main[0].context;
expect(storyboardContext.get("myNewPropContext")).toMatchObject({
type: "brick-property",
prop: "title",
});
expect(
storyboardContext.get("myFreeContextDefinedOnBrick")
).toMatchObject({
type: "free-variable",
value: "some value",
eventTarget: expect.anything(),
});
expect(result.main[0].properties).toEqual({
title: "good even better default value",
useBrick: {
brick: "useBrick-a",
properties: {
useBrick: {
brick: "useBrick-in-useBrick-b",
slots: {
content: {
bricks: [
{
brick: "slots-useBrick-in-useBrick-c",
},
],
type: "bricks",
},
},
},
},
slots: {
content: {
bricks: [
{
brick: "slots-in-useBrick-d",
slots: "error",
},
{
brick: "slots-in-useBrick-g",
slots: null,
},
{
brick: "slots-in-useBrick-h",
slots: [
{
brick: "slots-in-brick-i",
},
],
},
],
type: "bricks",
},
error1: {
bricks: {
brick: "error-brick",
},
},
error2: {
bricks: [
{
useBrick: "1",
slots: {
content: "slots-in-brick-j",
},
},
{
useBrick: 2,
},
{
useBrick: null,
},
{
useBrick: undefined,
},
],
},
},
},
columns: [
{
title: "title-1",
label: "label-1",
useBrick: true,
},
{
title: "title-2",
label: "label-2",
useBrick: {
brick: "deep-useBrick-e",
properties: {
useBrick: {
brick: "deep-useBrick-in-useBrick-f",
},
},
},
},
],
});
expect(kernel.mountPoints.bg.children.length).toBe(2);
expect(kernel.mountPoints.bg.children[0].tagName).toBe("PROVIDER-A");
expect(kernel.mountPoints.bg.children[1].tagName).toBe("PROVIDER-B");
expect((kernel.mountPoints.bg.children[1] as any).args).toEqual(["good"]);
expect(result.portal).toMatchObject([
{
type: "modal-a",
properties: {
args: ["a"],
},
children: [
{
type: "h2",
properties: {
textContent: "modal heading",
},
children: [],
slotId: "content",
},
],
slotId: undefined,
},
{
type: "p",
properties: {
textContent: "portal in portal",
},
children: [],
slotId: undefined,
},
]);
context.handleBeforePageLoad();
expect(applyTheme).toBeCalledWith("dark");
context.handlePageLoad();
context.handleAnchorLoad();
(history.getHistory as jest.Mock).mockReturnValue({
location: {
hash: "#yes",
},
});
context.handleAnchorLoad();
context.handleScrollIntoView("5df55280297f6");
context.handleBeforePageLeave({
location: { pathname: "/home" } as Location,
action: "POP",
});
context.handlePageLeave();
const mediaChangeDetail = { breakpoint: MediaBreakpoint.xLarge };
context.handleMediaChange(mediaChangeDetail);
context.handleMessageClose(new CloseEvent("error"));
context.handleMessage();
context.handleBrickBindObserver();
expect(spyOnDispatchEvent).toBeCalledWith(
expect.objectContaining({ type: "page.load" })
);
// Assert `console.log()`.
expect(consoleLog).toHaveBeenNthCalledWith(
1,
new CustomEvent("page.load")
);
expect(consoleLog).toHaveBeenNthCalledWith(
2,
new CustomEvent("anchor.unload")
);
expect(consoleLog).toHaveBeenNthCalledWith(
3,
new CustomEvent("anchor.load", {
detail: {
hash: "#yes",
anchor: "yes",
},
})
);
expect(consoleLog).toHaveBeenNthCalledWith(
4,
new CustomEvent("page.beforeLeave", {
detail: {
location: { pathname: "/home" },
action: "POP",
},
})
);
expect(consoleLog).toHaveBeenNthCalledWith(
5,
new CustomEvent("page.leave")
);
expect(consoleLog).toHaveBeenNthCalledWith(
6,
new CustomEvent("media.change", { detail: mediaChangeDetail })
);
expect(consoleLog).toHaveBeenNthCalledWith(
7,
new CustomEvent("message.close")
);
expect(consoleLog).toHaveBeenNthCalledWith(
8,
new CustomEvent("scroll.into.view")
);
// Assert `console.info()`.
expect(consoleInfo).toHaveBeenNthCalledWith(
1,
new CustomEvent("page.load")
);
expect(consoleInfo).toHaveBeenNthCalledWith(
2,
new CustomEvent("anchor.unload")
);
expect(consoleInfo).toHaveBeenNthCalledWith(3, "yes");
expect(consoleInfo).toHaveBeenNthCalledWith(
4,
new CustomEvent("page.beforeLeave", {
detail: {
location: { pathname: "/home" },
action: "POP",
},
})
);
expect(consoleInfo).toHaveBeenNthCalledWith(
5,
new CustomEvent("page.leave")
);
expect(consoleInfo).toHaveBeenNthCalledWith(
6,
new CustomEvent("message.close")
);
});
it("resolve menu should work", async () => {
const context = new LocationContext(kernel, {
pathname: "/",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(true);
jest
.spyOn(context.resolver, "resolveOne")
.mockImplementationOnce(
(type: any, resolveConf: ResolveConf, conf: Record<string, any>) => {
Object.assign(conf, resolveConf.transform);
return Promise.resolve();
}
);
const resolveConf = {
provider: "provider-a",
transform: {
pageTitle: "A",
sidebarMenu: {
title: "title-a",
menuItems: [
{
text: "item-1",
},
],
},
},
};
const result = await context.mountRoutes(
[
{
path: "/",
providers: [
"provider-a",
{
brick: "provider-b",
properties: {
args: ["good"],
},
},
],
menu: {
type: "resolve",
resolve: resolveConf,
},
bricks: [
{
brick: "menu",
properties: {
menu: "<% APP.getMenu('menu-1') %>",
},
context: [
{
name: "menu",
value: "<% APP.getMenu('menu-2') %>",
},
],
},
],
},
],
undefined,
getInitialMountResult()
);
expect(result.menuBar).toMatchObject({
menu: {
title: "title-a",
menuItems: [
{
text: "item-1",
},
],
},
});
});
});
it("preFetchMenu should work", async () => {
const context = new LocationContext(kernel, {
pathname: "/hello/a/b",
search: "",
hash: "",
state: {},
});
spyOnIsLoggedIn.mockReturnValue(true);
jest
.spyOn(context.resolver, "resolveOne")
.mockImplementationOnce(
(type: any, resolveConf: ResolveConf, conf: Record<string, any>) => {
Object.assign(conf, resolveConf.transform);
return Promise.resolve();
}
);
await context.mountRoutes(
[
{
path: "/hello/a",
context: [
{
name: "menu",
value: "<% APP.getMenu('CTX-menu') %>",
},
],
exact: false,
providers: [],
routes: [
{
path: "/hello/a/b",
context: [
{
name: "menu",
value: "<% APP.getMenu('menu-1') %>",
},
],
bricks: [
{
brick: "menu",
properties: {
menu: "<% APP.getMenu('menu-2') %>",
},
},
{
brick: "tpl-a",
},
],
type: "bricks",
exact: true,
},
],
type: "routes",
},
],
undefined,
getInitialMountResult()
);
expect(jestConstructMenu).toBeCalledTimes(4);
expect(jestConstructMenu.mock.calls[0][0]).toStrictEqual(["CTX-menu"]);
expect(jestConstructMenu.mock.calls[1][0]).toStrictEqual([
"menu-1",
"menu-2",
]);
expect(jestConstructMenu.mock.calls[2][0]).toStrictEqual(["tpl-a-menu"]);
expect(jestConstructMenu.mock.calls[3][0]).toStrictEqual([
"tpl-b-menu-inner-tpl-a",
]);
});
it("getSubStoryboardByRoute should work", () => {
const context = new LocationContext(kernel, {
pathname: "/1/2",
search: "",
hash: "",
state: {},
});
const storyboard = {
app: {
id: "a",
homepage: "/1",
},
routes: [
{
path: "${APP.homepage}/2",
bricks: null,
},
{
path: "${APP.homepage}/3",
bricks: null,
},
],
} as Partial<Storyboard>;
spyOnIsLoggedIn.mockReturnValue(true);
expect(context.getSubStoryboardByRoute(storyboard as Storyboard)).toEqual({
app: {
id: "a",
homepage: "/1",
},
routes: [
{
path: "${APP.homepage}/2",
bricks: null,
},
],
});
});
});