Support for .screen() plugin API
Adds `.screen()` plugin API method for adding plugin-provided screens. Such screens only have common headers and footers, while rest of the page is filled with the registered web component. Any navigation to URL containing `#/x/` hash results in client-only redirect to `/x/` and showing appropriate plugin-provided screen. Adds a `plugin.screenUrl()` method for generating consistent URLs for such screens. Adds an example plugin with number of ways the provided API can be used. Adds partial support for GWT UI `.screen()` method. Notable difference - does not support RegExp for the screen matching. Change-Id: I0be3dee8eba6f8535a1fb2be05f473f3649bad8f
This commit is contained in:
parent
c35670bf62
commit
6ea8c8a31e
@ -161,11 +161,14 @@ Note: TODO
|
||||
`plugin.hook(endpointName, opt_options)`
|
||||
|
||||
See list of supported link:pg-plugin-endpoints.html[endpoints].
|
||||
|
||||
Note: TODO
|
||||
|
||||
=== registerCustomComponent
|
||||
`plugin.registerCustomComponent(endpointName, opt_moduleName, opt_options)`
|
||||
|
||||
See list of supported link:pg-plugin-endpoints.html[endpoints].
|
||||
|
||||
Note: TODO
|
||||
|
||||
=== registerStyleModule
|
||||
@ -239,6 +242,28 @@ Note: TODO
|
||||
|
||||
Note: TODO
|
||||
|
||||
=== screen
|
||||
`plugin.screen(screenName, opt_moduleName)`
|
||||
|
||||
.Params:
|
||||
- `*string* screenName` URL path fragment of the screen, e.g.
|
||||
`/x/pluginname/*screenname*`
|
||||
- `*string* opt_moduleName` (Optional) Web component to be instantiated for this
|
||||
screen.
|
||||
|
||||
.Returns:
|
||||
- Instance of GrDomHook.
|
||||
|
||||
=== screenUrl
|
||||
`plugin.url(opt_screenName)`
|
||||
|
||||
.Params:
|
||||
- `*string* screenName` (optional) URL path fragment of the screen, e.g.
|
||||
`/x/pluginname/*screenname*`
|
||||
|
||||
.Returns:
|
||||
- Absolute URL for the screen, e.g. `http://localhost/base/x/pluginname/screenname`
|
||||
|
||||
=== theme
|
||||
`plugin.theme()`
|
||||
|
||||
|
@ -72,14 +72,15 @@ limitations under the License.
|
||||
|
||||
View: {
|
||||
ADMIN: 'admin',
|
||||
CHANGE: 'change',
|
||||
AGREEMENTS: 'agreements',
|
||||
CHANGE: 'change',
|
||||
DASHBOARD: 'dashboard',
|
||||
DIFF: 'diff',
|
||||
EDIT: 'edit',
|
||||
GROUP: 'group',
|
||||
PLUGIN_SCREEN: 'plugin-screen',
|
||||
SEARCH: 'search',
|
||||
SETTINGS: 'settings',
|
||||
GROUP: 'group',
|
||||
},
|
||||
|
||||
GroupDetailView: {
|
||||
|
@ -130,6 +130,8 @@
|
||||
// Matches /c/<changeNum>/ /<URL tail>
|
||||
// Catches improperly encoded URLs (context: Issue 7100)
|
||||
IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
|
||||
|
||||
PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -621,6 +623,14 @@
|
||||
page((ctx, next) => {
|
||||
document.body.scrollTop = 0;
|
||||
|
||||
if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
|
||||
// Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
|
||||
// This is needed to allow plugins to add basic #/x/ screen links to
|
||||
// any location.
|
||||
this._redirect(ctx.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire asynchronously so that the URL is changed by the time the event
|
||||
// is processed.
|
||||
this.async(() => {
|
||||
@ -748,6 +758,8 @@
|
||||
this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
|
||||
'_handleImproperlyEncodedPlusRoute');
|
||||
|
||||
this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
|
||||
|
||||
// Note: this route should appear last so it only catches URLs unmatched
|
||||
// by other patterns.
|
||||
this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
|
||||
@ -1276,6 +1288,13 @@
|
||||
this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
|
||||
},
|
||||
|
||||
_handlePluginScreen(ctx) {
|
||||
const view = Gerrit.Nav.View.PLUGIN_SCREEN;
|
||||
const plugin = ctx.params[0];
|
||||
const screen = ctx.params[1];
|
||||
this._setParams({view, plugin, screen});
|
||||
},
|
||||
|
||||
/**
|
||||
* Catchall route for when no other route is matched.
|
||||
*/
|
||||
|
@ -168,6 +168,7 @@ limitations under the License.
|
||||
'_handleTagListFilterOffsetRoute',
|
||||
'_handleTagListFilterRoute',
|
||||
'_handleTagListOffsetRoute',
|
||||
'_handlePluginScreen',
|
||||
];
|
||||
|
||||
// Handler names that check authentication themselves, and thus don't need
|
||||
@ -1332,6 +1333,16 @@ limitations under the License.
|
||||
assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
|
||||
});
|
||||
});
|
||||
|
||||
test('_handlePluginScreen', () => {
|
||||
const ctx = {params: ['foo', 'bar']};
|
||||
assertDataToParams(ctx, '_handlePluginScreen', {
|
||||
view: Gerrit.Nav.View.PLUGIN_SCREEN,
|
||||
plugin: 'foo',
|
||||
screen: 'bar',
|
||||
});
|
||||
assert.isFalse(redirectStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
suite('_parseQueryString', () => {
|
||||
|
@ -48,6 +48,7 @@ limitations under the License.
|
||||
<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
|
||||
<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
|
||||
<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
|
||||
<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
|
||||
<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
|
||||
<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
|
||||
<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
|
||||
@ -172,6 +173,11 @@ limitations under the License.
|
||||
<gr-admin-view path="[[_path]]"
|
||||
params=[[params]]></gr-admin-view>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
|
||||
<gr-endpoint-decorator name="[[_pluginScreenName]]">
|
||||
<gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
|
||||
</gr-endpoint-decorator>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showCLAView]]" restamp="true">
|
||||
<gr-cla-view path="[[_path]]"></gr-cla-view>
|
||||
</template>
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
properties: {
|
||||
/**
|
||||
* @type {{ query: string, view: string }}
|
||||
* @type {{ query: string, view: string, screen: string }}
|
||||
*/
|
||||
params: Object,
|
||||
keyEventTarget: {
|
||||
@ -72,6 +72,7 @@
|
||||
_showAdminView: Boolean,
|
||||
_showCLAView: Boolean,
|
||||
_showEditorView: Boolean,
|
||||
_showPluginScreen: Boolean,
|
||||
/** @type {?} */
|
||||
_viewState: Object,
|
||||
/** @type {?} */
|
||||
@ -79,6 +80,10 @@
|
||||
_lastSearchPage: String,
|
||||
_path: String,
|
||||
_isShadowDom: Boolean,
|
||||
_pluginScreenName: {
|
||||
type: String,
|
||||
computed: '_computePluginScreenName(params)',
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
@ -160,6 +165,14 @@
|
||||
view === Gerrit.Nav.View.GROUP);
|
||||
this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
|
||||
this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
|
||||
const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
|
||||
this.set('_showPluginScreen', false);
|
||||
// Navigation within plugin screens does not restamp gr-endpoint-decorator
|
||||
// because _showPluginScreen value does not change. To force restamp,
|
||||
// change _showPluginScreen value between true and false.
|
||||
if (isPluginScreen) {
|
||||
this.async(() => this.set('_showPluginScreen', true), 1);
|
||||
}
|
||||
if (this.params.justRegistered) {
|
||||
this.$.registration.open();
|
||||
}
|
||||
@ -282,5 +295,9 @@
|
||||
Gerrit.Nav.navigateToStatusSearch(status);
|
||||
}
|
||||
},
|
||||
|
||||
_computePluginScreenName({plugin, screen}) {
|
||||
return Gerrit._getPluginScreenName(plugin, screen);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -456,5 +456,33 @@ limitations under the License.
|
||||
assert.isFalse(stub.called);
|
||||
});
|
||||
});
|
||||
|
||||
suite('screen', () => {
|
||||
test('screenUrl()', () => {
|
||||
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
|
||||
assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin');
|
||||
assert.equal(
|
||||
plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo');
|
||||
});
|
||||
|
||||
test('deprecated works', () => {
|
||||
const stub = sandbox.stub();
|
||||
const hookStub = {onAttached: sandbox.stub()};
|
||||
sandbox.stub(plugin, 'hook').returns(hookStub);
|
||||
plugin.deprecated.screen('foo', stub);
|
||||
assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
|
||||
const fakeEl = {style: {display: ''}};
|
||||
hookStub.onAttached.callArgWith(0, fakeEl);
|
||||
assert.isTrue(stub.called);
|
||||
assert.equal(fakeEl.style.display, 'none');
|
||||
});
|
||||
|
||||
test('works', () => {
|
||||
sandbox.stub(plugin, 'registerCustomComponent');
|
||||
plugin.screen('foo', 'some-module');
|
||||
assert.isTrue(plugin.registerCustomComponent.calledWith(
|
||||
'testplugin-screen-foo', 'some-module'));
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -23,7 +23,7 @@
|
||||
*/
|
||||
const plugins = {};
|
||||
|
||||
const stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel'];
|
||||
const stubbedMethods = ['_loadedGwt', 'settingsScreen', 'panel'];
|
||||
const GWT_PLUGIN_STUB = {};
|
||||
for (const name of stubbedMethods) {
|
||||
GWT_PLUGIN_STUB[name] = warnNotSupported.bind(null, name);
|
||||
@ -92,7 +92,11 @@
|
||||
url.href, '— Unable to determine name.');
|
||||
return;
|
||||
}
|
||||
return pathname.split('/')[2];
|
||||
// Pathname should normally look like this:
|
||||
// /plugins/PLUGINNAME/static/SCRIPTNAME.html
|
||||
// Or, for app/samples:
|
||||
// /plugins/PLUGINNAME.html
|
||||
return pathname.split('/')[2].split('.')[0];
|
||||
}
|
||||
|
||||
function Plugin(opt_url) {
|
||||
@ -105,8 +109,9 @@
|
||||
}
|
||||
this.deprecated = {
|
||||
install: deprecatedAPI.install.bind(this),
|
||||
popup: deprecatedAPI.popup.bind(this),
|
||||
onAction: deprecatedAPI.onAction.bind(this),
|
||||
popup: deprecatedAPI.popup.bind(this),
|
||||
screen: deprecatedAPI.screen.bind(this),
|
||||
};
|
||||
|
||||
this._url = new URL(opt_url);
|
||||
@ -159,6 +164,13 @@
|
||||
this._name + (opt_path || '/');
|
||||
};
|
||||
|
||||
Plugin.prototype.screenUrl = function(opt_screenName) {
|
||||
const origin = this._url.origin;
|
||||
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
|
||||
const tokenPart = opt_screenName ? '/' + opt_screenName : '';
|
||||
return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
|
||||
};
|
||||
|
||||
Plugin.prototype._send = function(method, url, opt_callback, opt_payload) {
|
||||
return send(method, this.url(url), opt_callback, opt_payload);
|
||||
};
|
||||
@ -237,6 +249,15 @@
|
||||
return api.open();
|
||||
};
|
||||
|
||||
Plugin.prototype.screen = function(screenName, opt_moduleName) {
|
||||
if (opt_moduleName && typeof opt_moduleName !== 'string') {
|
||||
throw new Error('deprecated, use deprecated.screen');
|
||||
}
|
||||
return this.registerCustomComponent(
|
||||
Gerrit._getPluginScreenName(this.getPluginName(), screenName),
|
||||
opt_moduleName);
|
||||
};
|
||||
|
||||
const deprecatedAPI = {
|
||||
install() {
|
||||
console.log('Installing deprecated APIs is deprecated!');
|
||||
@ -277,6 +298,29 @@
|
||||
});
|
||||
},
|
||||
|
||||
screen(pattern, callback) {
|
||||
console.warn('plugin.deprecated.screen is deprecated,' +
|
||||
' use plugin.screen instead!');
|
||||
if (pattern instanceof RegExp) {
|
||||
console.error('deprecated.screen() does not support RegExp. ' +
|
||||
'Please use strings for patterns.');
|
||||
return;
|
||||
}
|
||||
this.hook(Gerrit._getPluginScreenName(this.getPluginName(), pattern))
|
||||
.onAttached(el => {
|
||||
el.style.display = 'none';
|
||||
callback({
|
||||
body: el,
|
||||
token: el.token,
|
||||
onUnload: () => {},
|
||||
setTitle: () => {},
|
||||
setWindowTitle: () => {},
|
||||
show: () => {
|
||||
el.style.display = 'initial';
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const Gerrit = window.Gerrit || {};
|
||||
@ -420,5 +464,9 @@
|
||||
return Gerrit._pluginsPending === 0;
|
||||
};
|
||||
|
||||
Gerrit._getPluginScreenName = function(pluginName, screenName) {
|
||||
return `${pluginName}-screen-${screenName}`;
|
||||
};
|
||||
|
||||
window.Gerrit = Gerrit;
|
||||
})(window);
|
||||
|
49
polygerrit-ui/app/samples/some-screen.html
Normal file
49
polygerrit-ui/app/samples/some-screen.html
Normal file
@ -0,0 +1,49 @@
|
||||
<dom-module id="some-screen">
|
||||
<script>
|
||||
Gerrit.install(plugin => {
|
||||
// Recommended approach for screen() API.
|
||||
plugin.screen('main', 'some-screen-main');
|
||||
|
||||
const mainUrl = plugin.screenUrl('main');
|
||||
|
||||
// Support for deprecated screen API.
|
||||
plugin.deprecated.screen('foo', ({token, body, show}) => {
|
||||
body.innerHTML = `This is a plugin screen at ${token}<br/>` +
|
||||
`<a href="${mainUrl}">Go to main plugin screen</a>`;
|
||||
show();
|
||||
});
|
||||
|
||||
// Quick and dirty way to get something on screen.
|
||||
plugin.screen('bar').onAttached(el => {
|
||||
el.innerHTML = `This is a plugin screen at ${el.token}<br/>` +
|
||||
`<a href="${mainUrl}">Go to main plugin screen</a>`;
|
||||
});
|
||||
|
||||
// Add a "Plugin screen" link to the change view screen.
|
||||
plugin.hook('change-metadata-item').onAttached(el => {
|
||||
el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
||||
<dom-module id="some-screen-main">
|
||||
<template>
|
||||
This is the <b>main</b> plugin screen at [[token]]
|
||||
<ul>
|
||||
<li><a href$="[[rootUrl]]/foo">via deprecated</a></li>
|
||||
<li><a href$="[[rootUrl]]/bar">without component</a></li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'some-screen-main',
|
||||
properties: {
|
||||
rootUrl: String,
|
||||
},
|
||||
attached() {
|
||||
this.rootUrl = `${this.plugin.screenUrl()}`;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
@ -200,7 +200,7 @@ type server struct{}
|
||||
|
||||
// Any path prefixes that should resolve to index.html.
|
||||
var (
|
||||
fePaths = []string{"/q/", "/c/", "/p/", "/dashboard/", "/admin/"}
|
||||
fePaths = []string{"/q/", "/c/", "/p/", "/x/", "/dashboard/", "/admin/"}
|
||||
issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user