mirror of
https://github.com/Redocly/redoc.git
synced 2025-07-31 02:19:47 +03:00
Merge pull request #2 from sofico-global/feat/queryparamnavigation
feat(): Add QueryParam navigation
This commit is contained in:
commit
01dad756d4
|
@ -251,6 +251,7 @@ You can use all of the following options with the standalone version of the <red
|
|||
* **path-only**: displays a path in the sidebar navigation item.
|
||||
* **id-only**: displays the operation id with a fallback to the path in the sidebar navigation item.
|
||||
* `showWebhookVerb` - when set to `true`, shows the HTTP request method for webhooks in operations and in the sidebar.
|
||||
* `userQueryParamToNavigate` - when set to `true`, the navigation in the URL will now use query param instead with the key `redoc`
|
||||
|
||||
### `<redoc>` theme object
|
||||
* `spacing`
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
describe('Search', () => {
|
||||
const getSearchInput = () => cy.get('[role="search"] input');
|
||||
const getSearchResults = () => cy.get('[data-role="search:results"]');
|
||||
const getResult = i => cy.get('[role=search] [role=menuitem]').eq(i);
|
||||
const getResult = i => cy.get('[role=search] [role=menuitem]').eq(i).find('label');
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('e2e/standalone.html');
|
||||
|
@ -45,7 +45,7 @@ describe('Search', () => {
|
|||
|
||||
getSearchInput().type('{enter}', { force: true });
|
||||
|
||||
cy.contains('[role=menu] [role=menuitem]', 'Introduction').should('have.class', 'active');
|
||||
cy.contains('[role=menu] [role=menuitem] label', 'Introduction').should('have.class', 'active');
|
||||
});
|
||||
|
||||
it('should mark search results', () => {
|
||||
|
|
|
@ -292,6 +292,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/packSize",
|
||||
|
@ -563,6 +564,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/type",
|
||||
|
@ -821,6 +823,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog",
|
||||
|
@ -1141,6 +1144,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Cat/properties/type",
|
||||
|
@ -1424,6 +1428,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Cat/properties/packSize",
|
||||
|
@ -1678,6 +1683,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Cat",
|
||||
|
@ -1957,6 +1963,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Pet",
|
||||
|
@ -2266,6 +2273,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/packSize",
|
||||
|
@ -2537,6 +2545,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog/properties/type",
|
||||
|
@ -2795,6 +2804,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
|
|||
},
|
||||
"unstable_ignoreMimeParameters": false,
|
||||
"untrustedSpec": false,
|
||||
"userQueryParamToNavigate": false,
|
||||
},
|
||||
"pattern": undefined,
|
||||
"pointer": "#/components/schemas/Dog",
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Lambda, observe } from 'mobx';
|
|||
|
||||
import type { OpenAPISpec } from '../types';
|
||||
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
|
||||
import { history } from './HistoryService';
|
||||
import { HistoryService } from './HistoryService';
|
||||
import { MarkerService } from './MarkerService';
|
||||
import { MenuStore } from './MenuStore';
|
||||
import { SpecStore } from './models';
|
||||
|
@ -53,6 +53,7 @@ export class AppStore {
|
|||
options: RedocNormalizedOptions;
|
||||
search?: SearchStore<string>;
|
||||
marker = new MarkerService();
|
||||
history;
|
||||
|
||||
private scroll: ScrollService;
|
||||
private disposer: Lambda | null = null;
|
||||
|
@ -65,16 +66,17 @@ export class AppStore {
|
|||
) {
|
||||
this.rawOptions = options;
|
||||
this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
|
||||
this.history = new HistoryService(this.options);
|
||||
this.scroll = new ScrollService(this.options);
|
||||
|
||||
// update position statically based on hash (in case of SSR)
|
||||
MenuStore.updateOnHistory(history.currentId, this.scroll);
|
||||
MenuStore.updateOnHistory(this.history.currentId, this.scroll);
|
||||
|
||||
// override the openApi standard to version 3.1.0
|
||||
// TODO remove when fully supporting open API 3.1.0
|
||||
spec.openapi = "3.1.0";
|
||||
this.spec = new SpecStore(spec, specUrl, this.options);
|
||||
this.menu = new MenuStore(this.spec, this.scroll, history);
|
||||
this.menu = new MenuStore(this.spec, this.scroll, this.history);
|
||||
|
||||
if (!this.options.disableSearch) {
|
||||
this.search = new SearchStore();
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import { bind, debounce } from 'decko';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { IS_BROWSER } from '../utils/';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
const EVENT = 'hashchange';
|
||||
|
||||
export class HistoryService {
|
||||
private _emiter;
|
||||
private options: RedocNormalizedOptions;
|
||||
|
||||
constructor() {
|
||||
constructor(options: RedocNormalizedOptions) {
|
||||
this.options = options;
|
||||
this._emiter = new EventEmitter();
|
||||
this.bind();
|
||||
}
|
||||
|
||||
get currentId(): string {
|
||||
return IS_BROWSER ? decodeURIComponent(window.location.hash.substring(1)) : '';
|
||||
if (IS_BROWSER) {
|
||||
if (this.shouldQueryParamNavigationBeUsed()) {
|
||||
return this.getQueryParams(window.location.search);
|
||||
} else {
|
||||
return decodeURIComponent(window.location.hash.substring(1));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
linkForId(id: string) {
|
||||
if (!id) {
|
||||
return '';
|
||||
}
|
||||
return '#' + id;
|
||||
return this.getHrefSplitCharacter() + id;
|
||||
}
|
||||
|
||||
subscribe(cb): () => void {
|
||||
|
@ -58,20 +68,32 @@ export class HistoryService {
|
|||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
window.location.href.split('#')[0] + this.linkForId(id),
|
||||
window.location.href.split(this.getHrefSplitCharacter())[0] + this.linkForId(id),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id));
|
||||
window.history.pushState(
|
||||
null,
|
||||
'',
|
||||
window.location.href.split(this.getHrefSplitCharacter())[0] + this.linkForId(id),
|
||||
);
|
||||
this.emit();
|
||||
}
|
||||
}
|
||||
|
||||
export const history = new HistoryService();
|
||||
private shouldQueryParamNavigationBeUsed(): boolean {
|
||||
return this.options?.userQueryParamToNavigate;
|
||||
}
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
history.dispose();
|
||||
});
|
||||
private getQueryParams(search: string): string {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
if (search != null) {
|
||||
return queryParams.get('redoc') != null ? (queryParams.get('redoc') as string) : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getHrefSplitCharacter(): string {
|
||||
return this.shouldQueryParamNavigationBeUsed() ? '?redoc=' : '#';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { action, observable, makeObservable } from 'mobx';
|
|||
import { querySelector } from '../utils/dom';
|
||||
import { escapeHTMLAttrChars, flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
|
||||
|
||||
import { history as historyInst, HistoryService } from './HistoryService';
|
||||
import { HistoryService } from './HistoryService';
|
||||
import { GROUP_DEPTH } from './MenuBuilder';
|
||||
|
||||
import type { SpecStore } from './models';
|
||||
|
@ -21,7 +21,7 @@ export class MenuStore {
|
|||
* Statically try update scroll position
|
||||
* Used before hydrating from server-side rendered html to scroll page faster
|
||||
*/
|
||||
static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
|
||||
static updateOnHistory(id: string, scroll: ScrollService) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ export interface RedocRawOptions {
|
|||
hideFab?: boolean;
|
||||
minCharacterLengthToInitSearch?: number;
|
||||
showWebhookVerb?: boolean;
|
||||
userQueryParamToNavigate?: boolean;
|
||||
}
|
||||
|
||||
export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
|
||||
|
@ -256,6 +257,7 @@ export class RedocNormalizedOptions {
|
|||
hideFab: boolean;
|
||||
minCharacterLengthToInitSearch: number;
|
||||
showWebhookVerb: boolean;
|
||||
userQueryParamToNavigate: boolean;
|
||||
|
||||
nonce?: string;
|
||||
|
||||
|
@ -335,5 +337,6 @@ export class RedocNormalizedOptions {
|
|||
this.hideFab = argValueToBoolean(raw.hideFab);
|
||||
this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3;
|
||||
this.showWebhookVerb = argValueToBoolean(raw.showWebhookVerb);
|
||||
this.userQueryParamToNavigate = argValueToBoolean(raw.userQueryParamToNavigate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
import { history } from '../HistoryService';
|
||||
import { HistoryService } from '../HistoryService';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
|
||||
const options = new RedocNormalizedOptions({});
|
||||
|
||||
describe('History service', () => {
|
||||
function mockWindowLocationForSearch(): void {
|
||||
const mockResponse = jest.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
hash: {
|
||||
endsWith: mockResponse,
|
||||
includes: mockResponse,
|
||||
},
|
||||
assign: mockResponse,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
test('should be an instance', () => {
|
||||
const history = new HistoryService(options);
|
||||
expect(typeof history).not.toBe('function');
|
||||
expect(history.subscribe).toBeDefined();
|
||||
});
|
||||
|
||||
test('History subscribe', () => {
|
||||
const fn = jest.fn();
|
||||
const history = new HistoryService(options);
|
||||
history.subscribe(fn);
|
||||
history.emit();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
|
@ -15,6 +34,7 @@ describe('History service', () => {
|
|||
|
||||
test('History subscribe should return unsubscribe function', () => {
|
||||
const fn = jest.fn();
|
||||
const history = new HistoryService(options);
|
||||
const unsubscribe = history.subscribe(fn);
|
||||
history.emit();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
|
@ -23,16 +43,44 @@ describe('History service', () => {
|
|||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('currentId should return correct id', () => {
|
||||
window.location.hash = '#testid';
|
||||
expect(history.currentId).toEqual('testid');
|
||||
describe('History with config property: `userQueryParamToNavigate` false', () => {
|
||||
test('currentId should return correct id', () => {
|
||||
window.location.hash = '#testid';
|
||||
const history = new HistoryService(options);
|
||||
expect(history.currentId).toEqual('testid');
|
||||
});
|
||||
|
||||
test('should return correct link for id', () => {
|
||||
const history = new HistoryService(options);
|
||||
expect(history.linkForId('testid')).toEqual('#testid');
|
||||
});
|
||||
|
||||
test('should return empty link for empty id', () => {
|
||||
const history = new HistoryService(options);
|
||||
expect(history.linkForId('')).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct link for id', () => {
|
||||
expect(history.linkForId('testid')).toEqual('#testid');
|
||||
});
|
||||
describe('History with config property: `userQueryParamToNavigate` true', () => {
|
||||
const overrideOptions = new RedocNormalizedOptions({
|
||||
userQueryParamToNavigate: true,
|
||||
});
|
||||
|
||||
test('should return empty link for empty id', () => {
|
||||
expect(history.linkForId('')).toEqual('');
|
||||
test('currentId should return correct id', () => {
|
||||
mockWindowLocationForSearch();
|
||||
window.location.search = '?redoc=testid';
|
||||
const history = new HistoryService(overrideOptions);
|
||||
expect(history.currentId).toEqual('testid');
|
||||
});
|
||||
|
||||
test('should return correct link for id', () => {
|
||||
const history = new HistoryService(overrideOptions);
|
||||
expect(history.linkForId('testid')).toEqual('?redoc=testid');
|
||||
});
|
||||
|
||||
test('should return empty link for empty id', () => {
|
||||
const history = new HistoryService(overrideOptions);
|
||||
expect(history.linkForId('')).toEqual('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user