(core) Adds new view as banner

Summary:
Diff removes view-as pill in the document breadcrumbs and add new view-as banner.

Note: Banners are still missing mechanism to handle several banners. As of now both doc-usage and view-as banners could show up at the same time.

Test Plan: Refactored existing test.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3732
This commit is contained in:
Cyprien P
2023-01-03 11:52:25 +01:00
parent c0e9c18128
commit cabac3d9d8
15 changed files with 282 additions and 132 deletions

View File

@@ -1,4 +1,5 @@
import {buildDocumentBanners, buildHomeBanners} from 'app/client/components/Banners';
import {ViewAsBanner} from 'app/client/components/ViewAsBanner';
import {domAsync} from 'app/client/lib/domAsync';
import {loadBillingPage} from 'app/client/lib/imports';
import {createSessionObs, isBoolean, isNumber} from 'app/client/lib/sessionObs';
@@ -161,5 +162,6 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App)
testId,
contentTop: buildDocumentBanners(pageModel),
contentBottom: dom.create(createBottomBarDoc, pageModel, leftPanelOpen, rightPanelOpen),
banner: dom.create(ViewAsBanner, pageModel),
});
}

View File

@@ -9,7 +9,9 @@ import {transition, TransitionWatcher} from 'app/client/ui/transitions';
import {cssHideForNarrowScreen, isScreenResizing, mediaNotSmall, mediaSmall, theme} from 'app/client/ui2018/cssVars';
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {dom, DomElementArg, MultiHolder, noTestId, Observable, styled, subscribe, TestId} from "grainjs";
import {
dom, DomElementArg, DomElementMethod, MultiHolder, noTestId, Observable, styled, subscribe, TestId
} from "grainjs";
import noop from 'lodash/noop';
import once from 'lodash/once';
import {SessionObs} from 'app/client/lib/sessionObs';
@@ -36,6 +38,7 @@ export interface PageContents {
headerMain: DomElementArg;
contentMain: DomElementArg;
banner?: DomElementArg;
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
testId?: TestId;
@@ -50,12 +53,15 @@ export function pagePanels(page: PageContents) {
const onResize = page.onResize || (() => null);
const leftOverlap = Observable.create(null, false);
const dragResizer = Observable.create(null, false);
const bannerHeight = Observable.create(null, 0);
const isScreenResizingObs = isScreenResizing();
let lastLeftOpen = left.panelOpen.get();
let lastRightOpen = right?.panelOpen.get() || false;
let leftPaneDom: HTMLElement;
let rightPaneDom: HTMLElement;
let mainHeaderDom: HTMLElement;
let contentTopDom: HTMLElement;
let onLeftTransitionFinish = noop;
// When switching to mobile mode, close panels; when switching to desktop, restore the
@@ -107,13 +113,30 @@ export function pagePanels(page: PageContents) {
dom.autoDispose(sub2),
dom.autoDispose(commandsGroup),
dom.autoDispose(leftOverlap),
page.contentTop,
dom('div', page.contentTop, elem => { contentTopDom = elem; }),
dom.maybe(page.banner, () => {
let elem: HTMLElement;
const updateTop = () => {
const height = mainHeaderDom.getBoundingClientRect().bottom;
elem.style.top = height + 'px';
};
setTimeout(() => watchHeightElem(contentTopDom, updateTop));
const lis = isScreenResizingObs.addListener(val => val || updateTop());
return elem = cssBannerContainer(
page.banner,
watchHeight(h => bannerHeight.set(h)),
dom.autoDispose(lis),
);
}),
cssContentMain(
leftPaneDom = cssLeftPane(
testId('left-panel'),
cssOverflowContainer(
contentWrapper = cssLeftPanelContainer(
cssLeftPaneHeader(left.header),
cssLeftPaneHeader(
left.header,
dom.style('margin-bottom', use => use(bannerHeight) + 'px')
),
left.content,
),
),
@@ -242,7 +265,7 @@ export function pagePanels(page: PageContents) {
cssHideForNarrowScreen.cls('')),
cssMainPane(
cssTopHeader(
mainHeaderDom = cssTopHeader(
testId('top-header'),
(left.hideOpener ? null :
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
@@ -260,6 +283,7 @@ export function pagePanels(page: PageContents) {
dom.on('click', () => toggleObs(right.panelOpen)),
cssHideForNarrowScreen.cls(''))
),
dom.style('margin-bottom', use => use(bannerHeight) + 'px'),
),
page.contentMain,
cssMainPane.cls('-left-overlap', leftOverlap),
@@ -275,7 +299,10 @@ export function pagePanels(page: PageContents) {
rightPaneDom = cssRightPane(
testId('right-panel'),
cssRightPaneHeader(right.header),
cssRightPaneHeader(
right.header,
dom.style('margin-bottom', use => use(bannerHeight) + 'px')
),
right.content,
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
@@ -606,7 +633,11 @@ const cssHiddenInput = styled('input', `
font-size: 1;
z-index: -1;
`);
const cssBannerContainer = styled('div', `
position: absolute;
z-index: 11;
width: 100%;
`);
// watchElementForBlur does not work if focus is on body. Which never happens when running in Grist
// because focus is constantly given to the copypasteField. But it does happen when running inside a
// projects test. For that latter case we had a hidden <input> field to the dom and give it focus.
@@ -617,3 +648,15 @@ function maybePatchDomAndChangeFocus() {
hiddenInput.focus();
}
}
// Watch for changes in dom subtree and call callback with element height;
function watchHeight(callback: (height: number) => void): DomElementMethod {
return elem => watchHeightElem(elem, callback);
}
function watchHeightElem(elem: HTMLElement, callback: (height: number) => void) {
const onChange = () => callback(elem.getBoundingClientRect().height);
const observer = new MutationObserver(onChange);
observer.observe(elem, {childList: true, subtree: true, attributes: true});
dom.onDisposeElem(elem, () => observer.disconnect());
onChange();
}

View File

@@ -7,13 +7,10 @@ import {buildExamples} from 'app/client/ui/ExampleInfo';
import {createHelpTools, cssLinkText, cssPageEntry, cssPageEntryMain, cssPageEntrySmall,
cssPageIcon, cssPageLink, cssSectionHeader, cssSpacer, cssSplitPageEntry,
cssTools} from 'app/client/ui/LeftPanelCommon';
import {hoverTooltip, tooltipCloseButton} from 'app/client/ui/tooltips';
import {theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {menuAnnotate} from 'app/client/ui2018/menus';
import {confirmModal} from 'app/client/ui2018/modals';
import {userOverrideParams} from 'app/common/gristUrls';
import {isOwner} from 'app/common/roles';
import {Disposable, dom, makeTestId, Observable, observable, styled} from 'grainjs';
@@ -44,7 +41,6 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
menuAnnotate('Beta', cssBetaTag.cls(''))
),
_canViewAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
isOverridden ? addRevertViewAsUI() : null,
);
}),
testId('access-rules'),
@@ -178,49 +174,6 @@ export interface AutomaticHelpToolInfo {
markAsSeen: () => void;
}
// 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(t('ViewingAsYourself'),
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
),
tooltipCloseButton(ctl),
),
{
openOnClick: true,
closeOnClick: false,
openDelay: 100,
closeDelay: 400,
placement: 'top',
}
),
];
}
const cssConvertTooltip = styled('div', `
display: flex;
align-items: center;
--icon-color: ${theme.controlFg};
& > .${cssLink.className} {
margin-left: 8px;
}
`);
const cssExampleCardOpener = styled('div', `
cursor: pointer;
margin-right: 4px;
@@ -241,13 +194,6 @@ const cssExampleCardOpener = styled('div', `
}
`);
const cssRevertViewAsButton = styled(cssExampleCardOpener, `
background-color: ${theme.iconButtonSecondaryBg};
&:hover {
background-color: ${theme.iconButtonSecondaryHoverBg};
}
`);
const cssBetaTag = styled('div', `
.${cssPageEntry.className}-disabled & {
opacity: 0.4;

View File

@@ -82,7 +82,6 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
isFork: pageModel.isFork,
isBareFork: pageModel.isBareFork,
isRecoveryMode: pageModel.isRecoveryMode,
userOverride: pageModel.userOverride,
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
isSnapshot: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.idParts.snapshotId)),
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),

View File

@@ -1,6 +1,7 @@
import {colors, theme} from 'app/client/ui2018/cssVars';
import {FullUser} from 'app/common/LoginSessionAPI';
import {dom, DomElementArg, styled} from 'grainjs';
import {icon} from 'app/client/ui2018/icons';
export type Size = 'small' | 'medium' | 'large';
@@ -8,10 +9,11 @@ export type Size = 'small' | 'medium' | 'large';
* Returns a DOM element showing a circular icon with a user's picture, or the user's initials if
* picture is missing. Also varies the color of the circle when using initials.
*/
export function createUserImage(user: FullUser|null, size: Size, ...args: DomElementArg[]): HTMLElement {
export function createUserImage(user: FullUser|'exampleUser'|null, size: Size, ...args: DomElementArg[]): HTMLElement {
let initials: string;
return cssUserImage(
cssUserImage.cls('-' + size),
(user === 'exampleUser') ? [cssUserImage.cls('-example'), cssExampleUserIcon('EyeShow')] :
(!user || user.anonymous) ? cssUserImage.cls('-anon') :
[
(user.picture ? cssUserPicture({src: user.picture}, dom.on('error', (ev, el) => dom.hideElem(el, true))) : null),
@@ -113,6 +115,11 @@ export const cssUserImage = styled('div', `
&-reduced {
font-size: var(--reduced-font-size);
}
&-example {
background-color: ${colors.slate};
border: 1px solid ${colors.slate};
}
`);
const cssUserPicture = styled('img', `
@@ -124,3 +131,10 @@ const cssUserPicture = styled('img', `
border-radius: 100px;
box-sizing: content-box; /* keep the border outside of the size of the image */
`);
const cssExampleUserIcon = styled(icon, `
background-color: white;
width: 45px;
height: 45px;
transform: scaleY(0.75);
`);