mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
5e5bf3af9d
commit
3f29baaded
@ -7,11 +7,11 @@ import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menuCssClass} from 'app/client/ui2018/menus';
|
||||
import {userOverrideParams} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL, getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
||||
import {Disposable, dom, Observable, styled} from 'grainjs';
|
||||
import merge = require('lodash/merge');
|
||||
import {cssMenu, cssMenuWrap, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
|
||||
|
||||
const roleNames: {[role: string]: string} = {
|
||||
@ -38,10 +38,9 @@ function buildUserRow(user: UserAccessData, currentUser: FullUser|null, ctl: IOp
|
||||
),
|
||||
basicButtonLink(cssUserButton.cls(''), cssUserButton.cls('-disabled', isCurrentUser),
|
||||
testId('acl-user-view-as'),
|
||||
icon('FieldLink'), 'View As', {
|
||||
href: urlState().makeUrl(
|
||||
merge({}, urlState().state.get(), {docPage: '', params: {linkParameters: {aclAsUser: user.email}}})),
|
||||
}),
|
||||
icon('FieldLink'), 'View As',
|
||||
urlState().setHref(userOverrideParams(user.email, {docPage: undefined})),
|
||||
),
|
||||
testId('acl-user-item'),
|
||||
);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -131,19 +131,6 @@ export const cssPageLink = styled('a', `
|
||||
}
|
||||
`);
|
||||
|
||||
// Styled like a cssPageLink, but in a disabled mode, without an actual link.
|
||||
export const cssPageDisabledLink = styled('span', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding-left: 24px;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
`);
|
||||
|
||||
export const cssLinkText = styled('span', `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -3,9 +3,12 @@ import { urlState } from "app/client/models/gristUrlState";
|
||||
import { showExampleCard } from 'app/client/ui/ExampleCard';
|
||||
import { examples } from 'app/client/ui/ExampleInfo';
|
||||
import { createHelpTools, cssSectionHeader, cssSpacer, cssTools } from 'app/client/ui/LeftPanelCommon';
|
||||
import { cssLinkText, cssPageDisabledLink, cssPageEntry, cssPageIcon, cssPageLink } from 'app/client/ui/LeftPanelCommon';
|
||||
import { cssLinkText, cssPageEntry, cssPageIcon, cssPageLink } from 'app/client/ui/LeftPanelCommon';
|
||||
import { hoverTooltip, tooltipCloseButton } from 'app/client/ui/tooltips';
|
||||
import { colors } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { cssLink } from 'app/client/ui2018/links';
|
||||
import { userOverrideParams } from 'app/common/gristUrls';
|
||||
import { Disposable, dom, makeTestId, Observable, styled } from "grainjs";
|
||||
|
||||
const testId = makeTestId('test-tools-');
|
||||
@ -22,10 +25,11 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
(aclUIEnabled ?
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'acl'),
|
||||
cssPageEntry.cls('-disabled', !canUseAccessRules),
|
||||
(canUseAccessRules ? cssPageLink : cssPageDisabledLink)(cssPageIcon('EyeShow'),
|
||||
cssPageEntry.cls('-disabled', !isOwner),
|
||||
cssPageLink(cssPageIcon('EyeShow'),
|
||||
cssLinkText('Access Rules'),
|
||||
canUseAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null
|
||||
canUseAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
||||
isOverridden ? addRevertViewAsUI() : null,
|
||||
),
|
||||
testId('access-rules'),
|
||||
) :
|
||||
@ -83,6 +87,43 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
);
|
||||
}
|
||||
|
||||
// When viewing a page as another user, the "Access Rules" page link includes a button to revert
|
||||
// the user and open the page, and a click on the page link shows a tooltip to revert.
|
||||
function addRevertViewAsUI() {
|
||||
return [
|
||||
// A button that allows reverting back to yourself.
|
||||
dom('a',
|
||||
cssExampleCardOpener.cls(''),
|
||||
cssRevertViewAsButton.cls(''),
|
||||
icon('Convert'),
|
||||
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
|
||||
dom.on('click', (ev) => ev.stopPropagation()), // Avoid refreshing the tooltip.
|
||||
testId('revert-view-as'),
|
||||
),
|
||||
|
||||
// A tooltip that allows reverting back to yourself.
|
||||
hoverTooltip((ctl) =>
|
||||
cssConvertTooltip(icon('Convert'),
|
||||
cssLink('Return to viewing as yourself',
|
||||
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
|
||||
),
|
||||
tooltipCloseButton(ctl),
|
||||
),
|
||||
{openOnClick: true}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const cssConvertTooltip = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--icon-color: ${colors.lightGreen};
|
||||
|
||||
& > .${cssLink.className} {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssExampleCardOpener = styled('div', `
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
@ -98,4 +139,14 @@ const cssExampleCardOpener = styled('div', `
|
||||
&:hover {
|
||||
background-color: ${colors.darkGreen};
|
||||
}
|
||||
.${cssTools.className}-collapsed & {
|
||||
display: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssRevertViewAsButton = styled(cssExampleCardOpener, `
|
||||
background-color: ${colors.darkGrey};
|
||||
&:hover {
|
||||
background-color: ${colors.slate};
|
||||
}
|
||||
`);
|
||||
|
@ -44,7 +44,6 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
docNameSave: renameDoc,
|
||||
pageNameSave: getRenamePageFn(gristDoc),
|
||||
cancelRecoveryMode: getCancelRecoveryModeFn(gristDoc),
|
||||
cancelUserOverride: getCancelUserOverrideFn(gristDoc),
|
||||
isPageNameReadOnly: (use) => use(gristDoc.isReadonly) || typeof use(gristDoc.activeViewId) !== 'number',
|
||||
isDocNameReadOnly: (use) => use(gristDoc.isReadonly) || use(pageModel.isFork),
|
||||
isFork: pageModel.isFork,
|
||||
@ -105,15 +104,6 @@ function getCancelRecoveryModeFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
};
|
||||
}
|
||||
|
||||
function getCancelUserOverrideFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
return async () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('aclAsUser_');
|
||||
url.searchParams.delete('aclAsUserId_');
|
||||
window.location.assign(url.href);
|
||||
};
|
||||
}
|
||||
|
||||
function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element {
|
||||
return cssHoverCircle(
|
||||
cssTopBarUndoBtn(iconName),
|
||||
|
@ -7,7 +7,8 @@
|
||||
|
||||
import {prepareForTransition} from 'app/client/ui/transitions';
|
||||
import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {dom, DomContents, styled} from 'grainjs';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {dom, DomContents, DomElementMethod, styled} from 'grainjs';
|
||||
import Popper from 'popper.js';
|
||||
|
||||
export interface ITipOptions {
|
||||
@ -24,8 +25,27 @@ export interface ITransientTipOptions extends ITipOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface IHoverTipOptions extends ITransientTipOptions {
|
||||
// How soon after mouseenter to show it. Defaults to 100 ms.
|
||||
openDelay?: number;
|
||||
|
||||
// If set and non-zero, remove the tip automatically after this time.
|
||||
timeoutMs?: number;
|
||||
|
||||
// How soon after mouseleave to hide it. Defaults to 400 ms. It also gives the pointer some time
|
||||
// to be outside of the trigger and the tooltip content if the user moves the pointer from one
|
||||
// to the other.
|
||||
closeDelay?: number;
|
||||
|
||||
// Also show the tip on clicking the element it's attached to.
|
||||
openOnClick?: boolean;
|
||||
}
|
||||
|
||||
export type ITooltipContentFunc = (ctl: ITooltipControl) => DomContents;
|
||||
|
||||
export interface ITooltipControl {
|
||||
close(): void;
|
||||
getDom(): HTMLElement; // The tooltip DOM.
|
||||
}
|
||||
|
||||
// Map of open tooltips, mapping the key (from ITipOptions) to ITooltipControl that allows
|
||||
@ -50,7 +70,7 @@ export function showTransientTooltip(refElem: Element, tipContent: DomContents,
|
||||
* See also ITipOptions.
|
||||
*/
|
||||
export function showTooltip(
|
||||
refElem: Element, tipContent: (ctl: ITooltipControl) => DomContents, options: ITipOptions = {}
|
||||
refElem: Element, tipContent: ITooltipContentFunc, options: ITipOptions = {}
|
||||
): ITooltipControl {
|
||||
const placement: Popper.Placement = options.placement || 'top';
|
||||
const key = options.key;
|
||||
@ -65,10 +85,10 @@ export function showTooltip(
|
||||
content.remove();
|
||||
if (key) { openTooltips.delete(key); }
|
||||
}
|
||||
const ctl: ITooltipControl = {close};
|
||||
const ctl: ITooltipControl = {close, getDom: () => content};
|
||||
|
||||
// Add the content element.
|
||||
const content = cssTooltip({role: 'tooltip'}, tipContent(ctl), testId(`transient-tooltip`));
|
||||
const content = cssTooltip({role: 'tooltip'}, tipContent(ctl), testId(`tooltip`));
|
||||
document.body.appendChild(content);
|
||||
|
||||
// Create a popper for positioning the tooltip content relative to refElem.
|
||||
@ -86,11 +106,83 @@ export function showTooltip(
|
||||
return ctl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a tooltip on hover. Suitable for use during dom construction, e.g.
|
||||
* dom('div', 'Trigger', hoverTooltip(() => 'Hello!'))
|
||||
*/
|
||||
export function hoverTooltip(tipContent: ITooltipContentFunc, options?: IHoverTipOptions): DomElementMethod {
|
||||
return (elem) => setHoverTooltip(elem, tipContent, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a tooltip to the given element, to be rendered on hover.
|
||||
*/
|
||||
export function setHoverTooltip(refElem: Element, tipContent: ITooltipContentFunc, options: IHoverTipOptions = {}) {
|
||||
const {openDelay = 100, timeoutMs, closeDelay = 400} = options;
|
||||
|
||||
// Controller for closing the tooltip, if one is open.
|
||||
let tipControl: ITooltipControl|undefined;
|
||||
|
||||
// Timer to open or close the tooltip, depending on whether tipControl is set.
|
||||
let timer: ReturnType<typeof setTimeout>|undefined;
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) { clearTimeout(timer); timer = undefined; }
|
||||
}
|
||||
function resetTimer(func: () => void, delay: number) {
|
||||
clearTimer();
|
||||
timer = setTimeout(func, delay);
|
||||
}
|
||||
function scheduleCloseIfOpen() {
|
||||
clearTimer();
|
||||
if (tipControl) { resetTimer(close, closeDelay); }
|
||||
}
|
||||
function open() {
|
||||
clearTimer();
|
||||
tipControl = showTooltip(refElem, ctl => tipContent({...ctl, close}), options);
|
||||
dom.onElem(tipControl.getDom(), 'mouseenter', clearTimer);
|
||||
dom.onElem(tipControl.getDom(), 'mouseleave', scheduleCloseIfOpen);
|
||||
if (timeoutMs) { resetTimer(close, timeoutMs); }
|
||||
}
|
||||
function close() {
|
||||
clearTimer();
|
||||
tipControl?.close();
|
||||
tipControl = undefined;
|
||||
}
|
||||
|
||||
// We simulate hover effect by handling mouseenter/mouseleave.
|
||||
dom.onElem(refElem, 'mouseenter', () => {
|
||||
if (!tipControl && !timer) {
|
||||
resetTimer(open, openDelay);
|
||||
} else if (tipControl) {
|
||||
// Already shown, reset to newly-shown state.
|
||||
clearTimer();
|
||||
if (timeoutMs) { resetTimer(close, timeoutMs); }
|
||||
}
|
||||
});
|
||||
|
||||
dom.onElem(refElem, 'mouseleave', scheduleCloseIfOpen);
|
||||
|
||||
if (options.openOnClick) {
|
||||
// If request, re-open on click.
|
||||
dom.onElem(refElem, 'click', () => { close(); open(); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a handy button for closing a tooltip.
|
||||
*/
|
||||
export function tooltipCloseButton(ctl: ITooltipControl): HTMLElement {
|
||||
return cssTooltipCloseButton(icon('CrossSmall'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('tooltip-close'),
|
||||
);
|
||||
}
|
||||
|
||||
const cssTooltip = styled('div', `
|
||||
position: absolute;
|
||||
z-index: 5000; /* should be higher than a modal */
|
||||
background-color: black;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||
text-align: center;
|
||||
@ -100,6 +192,22 @@ const cssTooltip = styled('div', `
|
||||
font-size: 10pt;
|
||||
padding: 8px 16px;
|
||||
margin: 4px;
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s;
|
||||
`);
|
||||
|
||||
const cssTooltipCloseButton = styled('div', `
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
margin: -4px -4px -4px 8px;
|
||||
--icon-color: white;
|
||||
border-radius: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
--icon-color: black;
|
||||
}
|
||||
`);
|
||||
|
@ -10,6 +10,7 @@ import { colors, cssHideForNarrowScreen, mediaNotSmall, testId } from 'app/clien
|
||||
import { editableLabel } from 'app/client/ui2018/editableLabel';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { UserOverride } from 'app/common/DocListAPI';
|
||||
import { userOverrideParams } from 'app/common/gristUrls';
|
||||
import { BindableValue, dom, Observable, styled } from 'grainjs';
|
||||
import { tooltip } from 'popweasel';
|
||||
|
||||
@ -102,7 +103,6 @@ export function docBreadcrumbs(
|
||||
docNameSave: (val: string) => Promise<void>,
|
||||
pageNameSave: (val: string) => Promise<void>,
|
||||
cancelRecoveryMode: () => Promise<void>,
|
||||
cancelUserOverride: () => Promise<void>,
|
||||
isDocNameReadOnly?: BindableValue<boolean>,
|
||||
isPageNameReadOnly?: BindableValue<boolean>,
|
||||
isFork: Observable<boolean>,
|
||||
@ -154,9 +154,12 @@ export function docBreadcrumbs(
|
||||
const userOverride = use(options.userOverride);
|
||||
if (userOverride) {
|
||||
return cssAlertTag(userOverride.user?.email || 'override',
|
||||
dom('a', dom.on('click', () => options.cancelUserOverride()),
|
||||
icon('CrossSmall')),
|
||||
testId('user-override-tag'));
|
||||
dom('a',
|
||||
urlState().setHref(userOverrideParams(null)),
|
||||
icon('CrossSmall')
|
||||
),
|
||||
testId('user-override-tag')
|
||||
);
|
||||
}
|
||||
if (use(options.isFiddle)) {
|
||||
return cssTag('fiddle', tooltip({title: fiddleExplanation}), testId('fiddle-tag'));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {ITooltipControl, showTooltip} from 'app/client/ui/tooltips';
|
||||
import {ITooltipControl, showTooltip, tooltipCloseButton} from 'app/client/ui/tooltips';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
@ -11,7 +11,7 @@ export function showTooltipToCreateFormula(editorDom: HTMLElement, convert: () =
|
||||
dom.on('mousedown', (ev) => { ev.preventDefault(); convert(); }),
|
||||
testId('editor-tooltip-convert'),
|
||||
),
|
||||
cssCloseButton(icon('CrossSmall'), dom.on('click', ctl.close)),
|
||||
tooltipCloseButton(ctl),
|
||||
);
|
||||
}
|
||||
const offerCtl = showTooltip(editorDom, buildTooltip, {key: 'col-to-formula'});
|
||||
@ -32,20 +32,3 @@ const cssConvertTooltip = styled('div', `
|
||||
margin-left: 8px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssCloseButton = styled('div', `
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
margin: -4px -4px -4px 8px;
|
||||
--icon-color: white;
|
||||
border-radius: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
--icon-color: black;
|
||||
}
|
||||
`);
|
||||
|
@ -297,6 +297,23 @@ export function useNewUI(newui: boolean|undefined) {
|
||||
return newui !== false;
|
||||
}
|
||||
|
||||
// Returns a function suitable for user with makeUrl/setHref/etc, which updates aclAsUser*
|
||||
// linkParameters in the current state, unsetting them if email is null. Optional extraState
|
||||
// allows setting other properties (e.g. 'docPage') at the same time.
|
||||
export function userOverrideParams(email: string|null, extraState?: IGristUrlState) {
|
||||
return function(prevState: IGristUrlState): IGristUrlState {
|
||||
const combined = {...prevState, ...extraState};
|
||||
const linkParameters = combined.params?.linkParameters || {};
|
||||
if (email) {
|
||||
linkParameters.aclAsUser = email;
|
||||
} else {
|
||||
delete linkParameters.aclAsUser;
|
||||
}
|
||||
delete linkParameters.aclAsUserId;
|
||||
return {...combined, params: {...combined.params, linkParameters}};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* parseDocPage is a noop if p is 'new' or 'code', otherwise parse to integer
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user