(core) Add a button and a tooltip to Access Rules page item, in View-As mode.

Summary:
- When in View-As mode, clicking the Access Rules page now shows a tooltip
  with a link to return to normal mode and open the Access Rules page.
- A "revert" button is shown next to the item with the same behavior.
- Implemented hoverTooltip() with various options. (It will have other uses.)
- Simplify creation of links based on UrlState:
  - Allow merging with previous urlState using a function
  - Add a helper function to merge in aclAsUser parameter.
  - Add setHref() method to UrlState

Test Plan:
Added test cases:
  - for tooltips generally in test/projects
  - for updating UrlState using a callback
  - for Access Rules tooltip and button behavior

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2749
This commit is contained in:
Dmitry S
2021-03-08 16:08:13 -05:00
parent 5e5bf3af9d
commit 3f29baaded
9 changed files with 229 additions and 68 deletions

View File

@@ -33,10 +33,12 @@ export interface UrlStateSpec<IUrlState> {
delayPushUrl(prevState: IUrlState, newState: IUrlState): Promise<void>;
}
export type UpdateFunc<IUrlState> = (prevState: IUrlState) => IUrlState;
/**
* Represents the state of a page in browser history, as encoded in window.location URL.
*/
export class UrlState<IUrlState> extends Disposable {
export class UrlState<IUrlState extends object> extends Disposable {
// Current state. This gets initialized in the constructor, and updated on navigation events.
public state = observable<IUrlState>(this._getState());
@@ -56,9 +58,11 @@ export class UrlState<IUrlState> extends Disposable {
* Creates a new history entry (navigable with Back/Forward buttons), encoding the given state
* in the URL. This is similar to navigating to a new URL, but does not reload the page.
*/
public async pushUrl(urlState: IUrlState, options: {replace?: boolean, avoidReload?: boolean} = {}) {
public async pushUrl(urlState: IUrlState|UpdateFunc<IUrlState>,
options: {replace?: boolean, avoidReload?: boolean} = {}) {
const prevState = this.state.get();
const newState = this._stateImpl.updateState(prevState, urlState);
const newState = this._mergeState(prevState, urlState);
const newUrl = this._stateImpl.encodeUrl(newState, this._window.location);
// Don't create a new history entry if nothing changed as it would only be annoying.
@@ -87,19 +91,32 @@ export class UrlState<IUrlState> extends Disposable {
/**
* Creates a URL (e.g. to use in a link's href) encoding the given state. The `use` argument
* allows for this to be used in a computed, and is used by setLinkUrl().
* allows for this to be used in a computed, and is used by setLinkUrl() and setHref().
*
* If urlState is an object (such as IGristUrlState), it gets merged with previous state
* according to rules (in gristUrlState's updateState). Alternatively, it can be a function that
* takes previous state and returns the new one.
*/
public makeUrl(urlState: IUrlState, use: UseCB = unwrap): string {
const fullState = this._stateImpl.updateState(use(this.state), urlState);
public makeUrl(urlState: IUrlState|UpdateFunc<IUrlState>, use: UseCB = unwrap): string {
const fullState = this._mergeState(use(this.state), urlState);
return this._stateImpl.encodeUrl(fullState, this._window.location);
}
/**
* Sets href on a dom element, e.g. dom('a', setHref({...})).
* This is similar to {href: makeUrl(urlState)}, but the destination URL will reflect the
* current url state (e.g. due to switching pages).
*/
public setHref(urlState: IUrlState|UpdateFunc<IUrlState>): DomElementMethod {
return dom.attr('href', (use) => this.makeUrl(urlState, use));
}
/**
* Applies to an <a> element to create a smart link, e.g. dom('a', setLinkUrl({ws: wsId})). It
* both sets the href (e.g. to allow the link to be opened to a new tab), AND intercepts plain
* clicks on it to "follow" the link without reloading the page.
*/
public setLinkUrl(urlState: IUrlState): DomElementMethod[] {
public setLinkUrl(urlState: IUrlState|UpdateFunc<IUrlState>): DomElementMethod[] {
return [
dom.attr('href', (use) => this.makeUrl(urlState, use)),
dom.on('click', (ev) => {
@@ -123,6 +140,12 @@ export class UrlState<IUrlState> extends Disposable {
private _getState(): IUrlState {
return this._stateImpl.decodeUrl(this._window.location);
}
private _mergeState(prevState: IUrlState, newState: IUrlState|UpdateFunc<IUrlState>): IUrlState {
return (typeof newState === 'object') ?
this._stateImpl.updateState(prevState, newState) :
newState(prevState);
}
}
// This is what we expect from the global Window object. Tests may override with a mock.