mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
c0e9c18128
commit
cabac3d9d8
@ -1,27 +1,21 @@
|
|||||||
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {createUserImage} from 'app/client/ui/UserImage';
|
import {createUserImage} from 'app/client/ui/UserImage';
|
||||||
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
||||||
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
||||||
import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
|
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
import {menuCssClass} from 'app/client/ui2018/menus';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
|
||||||
import {menuCssClass, menuDivider} from 'app/client/ui2018/menus';
|
|
||||||
import {PermissionDataWithExtraUsers} from 'app/common/ActiveDocAPI';
|
import {PermissionDataWithExtraUsers} from 'app/common/ActiveDocAPI';
|
||||||
import {userOverrideParams} from 'app/common/gristUrls';
|
import {userOverrideParams} from 'app/common/gristUrls';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
|
||||||
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
|
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
|
||||||
import {getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
import {getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
||||||
import {Disposable, dom, Observable, styled} from 'grainjs';
|
import {Disposable, dom, Observable, styled} from 'grainjs';
|
||||||
import {cssMenu, cssMenuWrap, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
|
import {cssMenu, cssMenuWrap, defaultMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
|
||||||
|
import {getUserRoleText} from 'app/common/UserAPI';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
|
||||||
const roleNames: {[role: string]: string} = {
|
const t = makeT('aclui.ViewAsDropdown');
|
||||||
[roles.OWNER]: 'Owner',
|
|
||||||
[roles.EDITOR]: 'Editor',
|
|
||||||
[roles.VIEWER]: 'Viewer',
|
|
||||||
};
|
|
||||||
|
|
||||||
function isSpecialEmail(email: string) {
|
function isSpecialEmail(email: string) {
|
||||||
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
|
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
|
||||||
@ -43,57 +37,53 @@ export class ACLUsersPopup extends Disposable {
|
|||||||
...user,
|
...user,
|
||||||
access: getRealAccess(user, permissionData),
|
access: getRealAccess(user, permissionData),
|
||||||
}))
|
}))
|
||||||
.filter(user => user.access && !isSpecialEmail(user.email));
|
.filter(user => user.access && !isSpecialEmail(user.email))
|
||||||
|
.filter(user => this._currentUser?.id !== user.id);
|
||||||
this._attributeTableUsers = permissionData.attributeTableUsers;
|
this._attributeTableUsers = permissionData.attributeTableUsers;
|
||||||
this._exampleUsers = permissionData.exampleUsers;
|
this._exampleUsers = permissionData.exampleUsers;
|
||||||
this.isInitialized.set(true);
|
this.isInitialized.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public attachPopup(elem: Element) {
|
public attachPopup(elem: Element, options: IPopupOptions) {
|
||||||
setPopupToCreateDom(elem, (ctl) => {
|
setPopupToCreateDom(elem, (ctl) => {
|
||||||
const buildRow = (user: UserAccessData) => this._buildUserRow(user, this._currentUser, ctl);
|
const buildRow =
|
||||||
|
(user: UserAccessData) => this._buildUserRow(user);
|
||||||
|
const buildExampleUserRow =
|
||||||
|
(user: UserAccessData) => this._buildUserRow(user, {isExampleUser: true});
|
||||||
return cssMenuWrap(cssMenu(
|
return cssMenuWrap(cssMenu(
|
||||||
dom.cls(menuCssClass),
|
dom.cls(menuCssClass),
|
||||||
cssUsers.cls(''),
|
cssUsers.cls(''),
|
||||||
|
cssHeader(t('ViewAs'), dom.show(this._shareUsers.length > 0)),
|
||||||
dom.forEach(this._shareUsers, buildRow),
|
dom.forEach(this._shareUsers, buildRow),
|
||||||
// Add a divider between users-from-shares and users from attribute tables.
|
(this._attributeTableUsers.length > 0) ? cssHeader(t('UsersFrom')) : null,
|
||||||
(this._attributeTableUsers.length > 0) ? menuDivider() : null,
|
dom.forEach(this._attributeTableUsers, buildExampleUserRow),
|
||||||
dom.forEach(this._attributeTableUsers, buildRow),
|
|
||||||
// Include example users only if there are not many "real" users.
|
// Include example users only if there are not many "real" users.
|
||||||
// It might be better to have an expandable section with these users, collapsed
|
// It might be better to have an expandable section with these users, collapsed
|
||||||
// by default, but that's beyond my UI ken.
|
// by default, but that's beyond my UI ken.
|
||||||
(this._shareUsers.length + this._attributeTableUsers.length < 5) ? [
|
(this._shareUsers.length + this._attributeTableUsers.length < 5) ? [
|
||||||
(this._exampleUsers.length > 0) ? menuDivider() : null,
|
(this._exampleUsers.length > 0) ? cssHeader(t('ExampleUsers')) : null,
|
||||||
dom.forEach(this._exampleUsers, buildRow)
|
dom.forEach(this._exampleUsers, buildExampleUserRow)
|
||||||
] : null,
|
] : null,
|
||||||
(el) => { setTimeout(() => el.focus(), 0); },
|
(el) => { setTimeout(() => el.focus(), 0); },
|
||||||
dom.onKeyDown({Escape: () => ctl.close()}),
|
dom.onKeyDown({Escape: () => ctl.close()}),
|
||||||
));
|
));
|
||||||
}, {...defaultMenuOptions, placement: 'bottom-end'});
|
}, {...defaultMenuOptions, ...options});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildUserRow(user: UserAccessData, currentUser: FullUser|null, ctl: IOpenController) {
|
private _buildUserRow(user: UserAccessData, opt: {isExampleUser?: boolean} = {}) {
|
||||||
const isCurrentUser = Boolean(currentUser && user.id === currentUser.id);
|
return dom('a',
|
||||||
return cssUserItem(
|
{class: cssMemberListItem.className + ' ' + cssUserItem.className},
|
||||||
cssMemberImage(
|
cssMemberImage(
|
||||||
createUserImage(user, 'large')
|
createUserImage(opt.isExampleUser ? 'exampleUser' : user, 'large')
|
||||||
),
|
),
|
||||||
cssMemberText(
|
cssMemberText(
|
||||||
cssMemberPrimary(user.name || dom('span', user.email),
|
cssMemberPrimary(user.name || dom('span', user.email),
|
||||||
cssRole('(', roleNames[user.access!] || user.access || 'no access', ')', testId('acl-user-access')),
|
cssRole('(', getUserRoleText(user), ')', testId('acl-user-access')),
|
||||||
),
|
),
|
||||||
user.name ? cssMemberSecondary(user.email) : null
|
user.name ? cssMemberSecondary(user.email) : null
|
||||||
),
|
),
|
||||||
basicButton(cssUserButton.cls(''), icon('Copy'), 'Copy Email',
|
this._viewAs(user),
|
||||||
testId('acl-user-copy'),
|
|
||||||
dom.on('click', async (ev, elem) => { await copyToClipboard(user.email); ctl.close(); }),
|
|
||||||
),
|
|
||||||
basicButtonLink(cssUserButton.cls(''), cssUserButton.cls('-disabled', isCurrentUser),
|
|
||||||
testId('acl-user-view-as'),
|
|
||||||
icon('FieldLink'), 'View As',
|
|
||||||
this._viewAs(user),
|
|
||||||
),
|
|
||||||
testId('acl-user-item'),
|
testId('acl-user-item'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -132,6 +122,9 @@ const cssUserItem = styled(cssMemberListItem, `
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${theme.lightHover};
|
background-color: ${theme.lightHover};
|
||||||
}
|
}
|
||||||
|
&, &:hover, &:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssRole = styled('span', `
|
const cssRole = styled('span', `
|
||||||
@ -139,18 +132,10 @@ const cssRole = styled('span', `
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssUserButton = styled('div', `
|
const cssHeader = styled('div', `
|
||||||
margin: 0 8px;
|
margin: 11px 24px 14px 24px;
|
||||||
border: none;
|
font-weight: 700;
|
||||||
display: inline-flex;
|
text-transform: uppercase;
|
||||||
white-space: nowrap;
|
font-size: ${vars.xsmallFontSize};
|
||||||
gap: 4px;
|
color: ${theme.darkText};
|
||||||
&:hover {
|
|
||||||
--icon-color: ${theme.controlFg};
|
|
||||||
color: ${theme.controlFg};
|
|
||||||
background-color: ${theme.hover};
|
|
||||||
}
|
|
||||||
&-disabled {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
`);
|
`);
|
||||||
|
@ -356,7 +356,8 @@ export class AccessRules extends Disposable {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
bigBasicButton(t('AddUserAttributes'), dom.on('click', () => this._addUserAttributes())),
|
bigBasicButton(t('AddUserAttributes'), dom.on('click', () => this._addUserAttributes())),
|
||||||
bigBasicButton(t('Users'), cssDropdownIcon('Dropdown'), elem => this._aclUsersPopup.attachPopup(elem),
|
bigBasicButton(t('ViewAs'), cssDropdownIcon('Dropdown'),
|
||||||
|
elem => this._aclUsersPopup.attachPopup(elem, {placement: 'bottom-end'}),
|
||||||
dom.style('visibility', use => use(this._aclUsersPopup.isInitialized) ? '' : 'hidden')),
|
dom.style('visibility', use => use(this._aclUsersPopup.isInitialized) ? '' : 'hidden')),
|
||||||
),
|
),
|
||||||
cssConditionError({style: 'margin-left: 16px'},
|
cssConditionError({style: 'margin-left: 16px'},
|
||||||
|
@ -16,7 +16,7 @@ export interface BannerOptions {
|
|||||||
* Warning banners have a yellow background. Error banners have a red
|
* Warning banners have a yellow background. Error banners have a red
|
||||||
* background.
|
* background.
|
||||||
*/
|
*/
|
||||||
style: 'warning' | 'error';
|
style: 'warning' | 'error' | 'info';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional variant of `content` to display when screen width becomes narrow.
|
* Optional variant of `content` to display when screen width becomes narrow.
|
||||||
@ -40,6 +40,11 @@ export interface BannerOptions {
|
|||||||
*/
|
*/
|
||||||
showExpandButton?: boolean;
|
showExpandButton?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If provided, applies the css class to the banner container.
|
||||||
|
*/
|
||||||
|
bannerCssClass?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that is called when the banner close button is clicked.
|
* Function that is called when the banner close button is clicked.
|
||||||
*
|
*
|
||||||
@ -59,7 +64,7 @@ export class Banner extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
return cssBanner(
|
return cssBanner({class: this._options.bannerCssClass || ''},
|
||||||
cssBanner.cls(`-${this._options.style}`),
|
cssBanner.cls(`-${this._options.style}`),
|
||||||
this._buildContent(),
|
this._buildContent(),
|
||||||
this._buildButtons(),
|
this._buildButtons(),
|
||||||
@ -114,6 +119,11 @@ const cssBanner = styled('div', `
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
color: black;
|
||||||
|
background: #FFFACD;
|
||||||
|
}
|
||||||
|
|
||||||
&-warning {
|
&-warning {
|
||||||
background: #E6A117;
|
background: #E6A117;
|
||||||
}
|
}
|
||||||
|
132
app/client/components/ViewAsBanner.ts
Normal file
132
app/client/components/ViewAsBanner.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { reportError } from 'app/client/models/AppModel';
|
||||||
|
import { Banner } from "app/client/components/Banner";
|
||||||
|
import { DocPageModel } from "app/client/models/DocPageModel";
|
||||||
|
import { icon } from "app/client/ui2018/icons";
|
||||||
|
import { primaryButtonLink } from 'app/client/ui2018/buttons';
|
||||||
|
import { Disposable, dom, styled } from "grainjs";
|
||||||
|
import { testId, theme } from 'app/client/ui2018/cssVars';
|
||||||
|
import { urlState } from 'app/client/models/gristUrlState';
|
||||||
|
import { userOverrideParams } from 'app/common/gristUrls';
|
||||||
|
import { cssMenuItem } from 'popweasel';
|
||||||
|
import { getUserRoleText } from 'app/common/UserAPI';
|
||||||
|
import { PermissionDataWithExtraUsers } from 'app/common/ActiveDocAPI';
|
||||||
|
import { waitGrainObs } from 'app/common/gutil';
|
||||||
|
import { cssSelectBtn } from 'app/client/ui2018/select';
|
||||||
|
import { ACLUsersPopup } from 'app/client/aclui/ACLUsers';
|
||||||
|
import { UserOverride } from 'app/common/DocListAPI';
|
||||||
|
import { makeT } from 'app/client/lib/localization';
|
||||||
|
|
||||||
|
const t = makeT('components.ViewAsBanner');
|
||||||
|
|
||||||
|
export class ViewAsBanner extends Disposable {
|
||||||
|
|
||||||
|
private _userOverride = this._docPageModel.userOverride;
|
||||||
|
private _usersPopup = ACLUsersPopup.create(this);
|
||||||
|
|
||||||
|
constructor (private _docPageModel: DocPageModel) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
return dom.maybe(this._userOverride, (userOverride) => {
|
||||||
|
this._initViewAsUsers().catch(reportError);
|
||||||
|
return dom.create(Banner, {
|
||||||
|
content: this._buildContent(userOverride),
|
||||||
|
style: 'info',
|
||||||
|
showCloseButton: false,
|
||||||
|
showExpandButton: false,
|
||||||
|
bannerCssClass: cssBanner.className,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildContent(userOverride: UserOverride) {
|
||||||
|
const {user, access} = userOverride;
|
||||||
|
return cssContent(
|
||||||
|
cssMessageText(
|
||||||
|
cssMessageIcon('EyeShow'),
|
||||||
|
'You are viewing this document as',
|
||||||
|
),
|
||||||
|
cssSelectBtn(
|
||||||
|
{tabIndex: '0'},
|
||||||
|
cssBtnText(
|
||||||
|
user ? cssMember(
|
||||||
|
user.name || user.email,
|
||||||
|
cssRole('(', getUserRoleText({...user, access}), ')', dom.show(Boolean(access))),
|
||||||
|
) : t('UnknownUser'),
|
||||||
|
),
|
||||||
|
dom(
|
||||||
|
'div', {style: 'flex: none;'},
|
||||||
|
cssInlineCollapseIcon('Collapse'),
|
||||||
|
),
|
||||||
|
elem => this._usersPopup.attachPopup(elem, {}),
|
||||||
|
testId('select-open'),
|
||||||
|
),
|
||||||
|
cssPrimaryButtonLink(
|
||||||
|
'View as Yourself', cssIcon('Convert'),
|
||||||
|
urlState().setHref(userOverrideParams(null)),
|
||||||
|
testId('revert'),
|
||||||
|
),
|
||||||
|
testId('view-as-banner'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initViewAsUsers() {
|
||||||
|
await waitGrainObs(this._docPageModel.gristDoc);
|
||||||
|
const permissionData = await this._getUsersForViewAs();
|
||||||
|
this._usersPopup.init(this._docPageModel, permissionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getUsersForViewAs(): Promise<PermissionDataWithExtraUsers> {
|
||||||
|
const docId = this._docPageModel.currentDocId.get()!;
|
||||||
|
const docApi = this._docPageModel.appModel.api.getDocAPI(docId);
|
||||||
|
return docApi.getUsersForViewAs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssContent = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
column-gap: 13px;
|
||||||
|
align-items: center;
|
||||||
|
& .${cssSelectBtn.className} {
|
||||||
|
width: 184px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const cssIcon = styled(icon, `
|
||||||
|
margin-left: 10px;
|
||||||
|
`);
|
||||||
|
const cssMember = styled('span', `
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${theme.text};
|
||||||
|
|
||||||
|
.${cssMenuItem.className}-sel & {
|
||||||
|
color: ${theme.menuItemSelectedFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const cssRole = styled('span', `
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 1ch;
|
||||||
|
`);
|
||||||
|
const cssMessageText = styled('span', `
|
||||||
|
`);
|
||||||
|
const cssMessageIcon = styled(icon, `
|
||||||
|
margin-right: 10px;
|
||||||
|
`);
|
||||||
|
const cssPrimaryButtonLink = styled(primaryButtonLink, `
|
||||||
|
margin-left: 5px;
|
||||||
|
`);
|
||||||
|
const cssBtnText = styled('div', `
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`);
|
||||||
|
const cssInlineCollapseIcon = styled(icon, `
|
||||||
|
margin: 0 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
`);
|
||||||
|
const cssBanner = styled('div', `
|
||||||
|
border-bottom: 1px solid ${theme.pagePanelsBorder};
|
||||||
|
height: 45px;
|
||||||
|
`);
|
@ -1,4 +1,5 @@
|
|||||||
import {buildDocumentBanners, buildHomeBanners} from 'app/client/components/Banners';
|
import {buildDocumentBanners, buildHomeBanners} from 'app/client/components/Banners';
|
||||||
|
import {ViewAsBanner} from 'app/client/components/ViewAsBanner';
|
||||||
import {domAsync} from 'app/client/lib/domAsync';
|
import {domAsync} from 'app/client/lib/domAsync';
|
||||||
import {loadBillingPage} from 'app/client/lib/imports';
|
import {loadBillingPage} from 'app/client/lib/imports';
|
||||||
import {createSessionObs, isBoolean, isNumber} from 'app/client/lib/sessionObs';
|
import {createSessionObs, isBoolean, isNumber} from 'app/client/lib/sessionObs';
|
||||||
@ -161,5 +162,6 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App)
|
|||||||
testId,
|
testId,
|
||||||
contentTop: buildDocumentBanners(pageModel),
|
contentTop: buildDocumentBanners(pageModel),
|
||||||
contentBottom: dom.create(createBottomBarDoc, pageModel, leftPanelOpen, rightPanelOpen),
|
contentBottom: dom.create(createBottomBarDoc, pageModel, leftPanelOpen, rightPanelOpen),
|
||||||
|
banner: dom.create(ViewAsBanner, pageModel),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ import {transition, TransitionWatcher} from 'app/client/ui/transitions';
|
|||||||
import {cssHideForNarrowScreen, isScreenResizing, mediaNotSmall, mediaSmall, theme} from 'app/client/ui2018/cssVars';
|
import {cssHideForNarrowScreen, isScreenResizing, mediaNotSmall, mediaSmall, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
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 noop from 'lodash/noop';
|
||||||
import once from 'lodash/once';
|
import once from 'lodash/once';
|
||||||
import {SessionObs} from 'app/client/lib/sessionObs';
|
import {SessionObs} from 'app/client/lib/sessionObs';
|
||||||
@ -36,6 +38,7 @@ export interface PageContents {
|
|||||||
|
|
||||||
headerMain: DomElementArg;
|
headerMain: DomElementArg;
|
||||||
contentMain: DomElementArg;
|
contentMain: DomElementArg;
|
||||||
|
banner?: DomElementArg;
|
||||||
|
|
||||||
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
|
onResize?: () => void; // Callback for when either pane is opened, closed, or resized.
|
||||||
testId?: TestId;
|
testId?: TestId;
|
||||||
@ -50,12 +53,15 @@ export function pagePanels(page: PageContents) {
|
|||||||
const onResize = page.onResize || (() => null);
|
const onResize = page.onResize || (() => null);
|
||||||
const leftOverlap = Observable.create(null, false);
|
const leftOverlap = Observable.create(null, false);
|
||||||
const dragResizer = Observable.create(null, false);
|
const dragResizer = Observable.create(null, false);
|
||||||
|
const bannerHeight = Observable.create(null, 0);
|
||||||
const isScreenResizingObs = isScreenResizing();
|
const isScreenResizingObs = isScreenResizing();
|
||||||
|
|
||||||
let lastLeftOpen = left.panelOpen.get();
|
let lastLeftOpen = left.panelOpen.get();
|
||||||
let lastRightOpen = right?.panelOpen.get() || false;
|
let lastRightOpen = right?.panelOpen.get() || false;
|
||||||
let leftPaneDom: HTMLElement;
|
let leftPaneDom: HTMLElement;
|
||||||
let rightPaneDom: HTMLElement;
|
let rightPaneDom: HTMLElement;
|
||||||
|
let mainHeaderDom: HTMLElement;
|
||||||
|
let contentTopDom: HTMLElement;
|
||||||
let onLeftTransitionFinish = noop;
|
let onLeftTransitionFinish = noop;
|
||||||
|
|
||||||
// When switching to mobile mode, close panels; when switching to desktop, restore the
|
// 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(sub2),
|
||||||
dom.autoDispose(commandsGroup),
|
dom.autoDispose(commandsGroup),
|
||||||
dom.autoDispose(leftOverlap),
|
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(
|
cssContentMain(
|
||||||
leftPaneDom = cssLeftPane(
|
leftPaneDom = cssLeftPane(
|
||||||
testId('left-panel'),
|
testId('left-panel'),
|
||||||
cssOverflowContainer(
|
cssOverflowContainer(
|
||||||
contentWrapper = cssLeftPanelContainer(
|
contentWrapper = cssLeftPanelContainer(
|
||||||
cssLeftPaneHeader(left.header),
|
cssLeftPaneHeader(
|
||||||
|
left.header,
|
||||||
|
dom.style('margin-bottom', use => use(bannerHeight) + 'px')
|
||||||
|
),
|
||||||
left.content,
|
left.content,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -242,7 +265,7 @@ export function pagePanels(page: PageContents) {
|
|||||||
cssHideForNarrowScreen.cls('')),
|
cssHideForNarrowScreen.cls('')),
|
||||||
|
|
||||||
cssMainPane(
|
cssMainPane(
|
||||||
cssTopHeader(
|
mainHeaderDom = cssTopHeader(
|
||||||
testId('top-header'),
|
testId('top-header'),
|
||||||
(left.hideOpener ? null :
|
(left.hideOpener ? null :
|
||||||
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
|
cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
|
||||||
@ -260,6 +283,7 @@ export function pagePanels(page: PageContents) {
|
|||||||
dom.on('click', () => toggleObs(right.panelOpen)),
|
dom.on('click', () => toggleObs(right.panelOpen)),
|
||||||
cssHideForNarrowScreen.cls(''))
|
cssHideForNarrowScreen.cls(''))
|
||||||
),
|
),
|
||||||
|
dom.style('margin-bottom', use => use(bannerHeight) + 'px'),
|
||||||
),
|
),
|
||||||
page.contentMain,
|
page.contentMain,
|
||||||
cssMainPane.cls('-left-overlap', leftOverlap),
|
cssMainPane.cls('-left-overlap', leftOverlap),
|
||||||
@ -275,7 +299,10 @@ export function pagePanels(page: PageContents) {
|
|||||||
|
|
||||||
rightPaneDom = cssRightPane(
|
rightPaneDom = cssRightPane(
|
||||||
testId('right-panel'),
|
testId('right-panel'),
|
||||||
cssRightPaneHeader(right.header),
|
cssRightPaneHeader(
|
||||||
|
right.header,
|
||||||
|
dom.style('margin-bottom', use => use(bannerHeight) + 'px')
|
||||||
|
),
|
||||||
right.content,
|
right.content,
|
||||||
|
|
||||||
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
|
dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),
|
||||||
@ -606,7 +633,11 @@ const cssHiddenInput = styled('input', `
|
|||||||
font-size: 1;
|
font-size: 1;
|
||||||
z-index: -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
|
// 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
|
// 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.
|
// 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();
|
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();
|
||||||
|
}
|
||||||
|
@ -7,13 +7,10 @@ import {buildExamples} from 'app/client/ui/ExampleInfo';
|
|||||||
import {createHelpTools, cssLinkText, cssPageEntry, cssPageEntryMain, cssPageEntrySmall,
|
import {createHelpTools, cssLinkText, cssPageEntry, cssPageEntryMain, cssPageEntrySmall,
|
||||||
cssPageIcon, cssPageLink, cssSectionHeader, cssSpacer, cssSplitPageEntry,
|
cssPageIcon, cssPageLink, cssSectionHeader, cssSpacer, cssSplitPageEntry,
|
||||||
cssTools} from 'app/client/ui/LeftPanelCommon';
|
cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||||
import {hoverTooltip, tooltipCloseButton} from 'app/client/ui/tooltips';
|
|
||||||
import {theme} from 'app/client/ui2018/cssVars';
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssLink} from 'app/client/ui2018/links';
|
|
||||||
import {menuAnnotate} from 'app/client/ui2018/menus';
|
import {menuAnnotate} from 'app/client/ui2018/menus';
|
||||||
import {confirmModal} from 'app/client/ui2018/modals';
|
import {confirmModal} from 'app/client/ui2018/modals';
|
||||||
import {userOverrideParams} from 'app/common/gristUrls';
|
|
||||||
import {isOwner} from 'app/common/roles';
|
import {isOwner} from 'app/common/roles';
|
||||||
import {Disposable, dom, makeTestId, Observable, observable, styled} from 'grainjs';
|
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(''))
|
menuAnnotate('Beta', cssBetaTag.cls(''))
|
||||||
),
|
),
|
||||||
_canViewAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
_canViewAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
||||||
isOverridden ? addRevertViewAsUI() : null,
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
testId('access-rules'),
|
testId('access-rules'),
|
||||||
@ -178,49 +174,6 @@ export interface AutomaticHelpToolInfo {
|
|||||||
markAsSeen: () => void;
|
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', `
|
const cssExampleCardOpener = styled('div', `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 4px;
|
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', `
|
const cssBetaTag = styled('div', `
|
||||||
.${cssPageEntry.className}-disabled & {
|
.${cssPageEntry.className}-disabled & {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
@ -82,7 +82,6 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
|||||||
isFork: pageModel.isFork,
|
isFork: pageModel.isFork,
|
||||||
isBareFork: pageModel.isBareFork,
|
isBareFork: pageModel.isBareFork,
|
||||||
isRecoveryMode: pageModel.isRecoveryMode,
|
isRecoveryMode: pageModel.isRecoveryMode,
|
||||||
userOverride: pageModel.userOverride,
|
|
||||||
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
|
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
|
||||||
isSnapshot: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.idParts.snapshotId)),
|
isSnapshot: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.idParts.snapshotId)),
|
||||||
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {colors, theme} from 'app/client/ui2018/cssVars';
|
import {colors, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import {dom, DomElementArg, styled} from 'grainjs';
|
import {dom, DomElementArg, styled} from 'grainjs';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
|
||||||
export type Size = 'small' | 'medium' | 'large';
|
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
|
* 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.
|
* 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;
|
let initials: string;
|
||||||
return cssUserImage(
|
return cssUserImage(
|
||||||
cssUserImage.cls('-' + size),
|
cssUserImage.cls('-' + size),
|
||||||
|
(user === 'exampleUser') ? [cssUserImage.cls('-example'), cssExampleUserIcon('EyeShow')] :
|
||||||
(!user || user.anonymous) ? cssUserImage.cls('-anon') :
|
(!user || user.anonymous) ? cssUserImage.cls('-anon') :
|
||||||
[
|
[
|
||||||
(user.picture ? cssUserPicture({src: user.picture}, dom.on('error', (ev, el) => dom.hideElem(el, true))) : null),
|
(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 {
|
&-reduced {
|
||||||
font-size: var(--reduced-font-size);
|
font-size: var(--reduced-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-example {
|
||||||
|
background-color: ${colors.slate};
|
||||||
|
border: 1px solid ${colors.slate};
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssUserPicture = styled('img', `
|
const cssUserPicture = styled('img', `
|
||||||
@ -124,3 +131,10 @@ const cssUserPicture = styled('img', `
|
|||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
box-sizing: content-box; /* keep the border outside of the size of the image */
|
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);
|
||||||
|
`);
|
||||||
|
@ -11,8 +11,6 @@ import { cssHideForNarrowScreen, mediaNotSmall, testId, theme } from 'app/client
|
|||||||
import { editableLabel } from 'app/client/ui2018/editableLabel';
|
import { editableLabel } from 'app/client/ui2018/editableLabel';
|
||||||
import { icon } from 'app/client/ui2018/icons';
|
import { icon } from 'app/client/ui2018/icons';
|
||||||
import { cssLink } from 'app/client/ui2018/links';
|
import { cssLink } from 'app/client/ui2018/links';
|
||||||
import { UserOverride } from 'app/common/DocListAPI';
|
|
||||||
import { userOverrideParams } from 'app/common/gristUrls';
|
|
||||||
import { BindableValue, dom, Observable, styled } from 'grainjs';
|
import { BindableValue, dom, Observable, styled } from 'grainjs';
|
||||||
import { tooltip } from 'popweasel';
|
import { tooltip } from 'popweasel';
|
||||||
|
|
||||||
@ -99,7 +97,6 @@ export function docBreadcrumbs(
|
|||||||
isBareFork: Observable<boolean>,
|
isBareFork: Observable<boolean>,
|
||||||
isFiddle: Observable<boolean>,
|
isFiddle: Observable<boolean>,
|
||||||
isRecoveryMode: Observable<boolean>,
|
isRecoveryMode: Observable<boolean>,
|
||||||
userOverride: Observable<UserOverride|null>,
|
|
||||||
isSnapshot?: Observable<boolean>,
|
isSnapshot?: Observable<boolean>,
|
||||||
isPublic?: Observable<boolean>,
|
isPublic?: Observable<boolean>,
|
||||||
}
|
}
|
||||||
@ -152,16 +149,6 @@ export function docBreadcrumbs(
|
|||||||
icon('CrossSmall')),
|
icon('CrossSmall')),
|
||||||
testId('recovery-mode-tag'));
|
testId('recovery-mode-tag'));
|
||||||
}
|
}
|
||||||
const userOverride = use(options.userOverride);
|
|
||||||
if (userOverride) {
|
|
||||||
return cssAlertTag(userOverride.user?.email || t('Override'),
|
|
||||||
dom('a',
|
|
||||||
urlState().setHref(userOverrideParams(null)),
|
|
||||||
icon('CrossSmall')
|
|
||||||
),
|
|
||||||
testId('user-override-tag')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (use(options.isFiddle)) {
|
if (use(options.isFiddle)) {
|
||||||
return cssTag(t('Fiddle'), tooltip({title: t('FiddleExplanation')}), testId('fiddle-tag'));
|
return cssTag(t('Fiddle'), tooltip({title: t('FiddleExplanation')}), testId('fiddle-tag'));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {ActionSummary} from 'app/common/ActionSummary';
|
import {ActionSummary} from 'app/common/ActionSummary';
|
||||||
import {ApplyUAResult, QueryFilters} from 'app/common/ActiveDocAPI';
|
import {ApplyUAResult, PermissionDataWithExtraUsers, QueryFilters} from 'app/common/ActiveDocAPI';
|
||||||
import {BaseAPI, IOptions} from 'app/common/BaseAPI';
|
import {BaseAPI, IOptions} from 'app/common/BaseAPI';
|
||||||
import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
||||||
import {BrowserSettings} from 'app/common/BrowserSettings';
|
import {BrowserSettings} from 'app/common/BrowserSettings';
|
||||||
@ -199,6 +199,16 @@ export function getRealAccess(user: UserAccessData, permissionData: PermissionDa
|
|||||||
return roles.getStrongestRole(user.access, inheritedAccess);
|
return roles.getStrongestRole(user.access, inheritedAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roleNames: {[role: string]: string} = {
|
||||||
|
[roles.OWNER]: 'Owner',
|
||||||
|
[roles.EDITOR]: 'Editor',
|
||||||
|
[roles.VIEWER]: 'Viewer',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getUserRoleText(user: UserAccessData) {
|
||||||
|
return roleNames[user.access!] || user.access || 'no access';
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActiveSessionInfo {
|
export interface ActiveSessionInfo {
|
||||||
user: FullUser & {helpScoutSignature?: string};
|
user: FullUser & {helpScoutSignature?: string};
|
||||||
org: Organization|null;
|
org: Organization|null;
|
||||||
@ -401,6 +411,9 @@ export interface DocAPI {
|
|||||||
// Upload a single attachment and return the resulting metadata row ID.
|
// Upload a single attachment and return the resulting metadata row ID.
|
||||||
// The arguments are passed to FormData.append.
|
// The arguments are passed to FormData.append.
|
||||||
uploadAttachment(value: string | Blob, filename?: string): Promise<number>;
|
uploadAttachment(value: string | Blob, filename?: string): Promise<number>;
|
||||||
|
|
||||||
|
// Get users that are worth proposing to "View As" for access control purposes.
|
||||||
|
getUsersForViewAs(): Promise<PermissionDataWithExtraUsers>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operations that are supported by a doc worker.
|
// Operations that are supported by a doc worker.
|
||||||
@ -833,6 +846,10 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getUsersForViewAs(): Promise<PermissionDataWithExtraUsers> {
|
||||||
|
return this.requestJson(`${this._url}/usersForViewAs`);
|
||||||
|
}
|
||||||
|
|
||||||
public async forceReload(): Promise<void> {
|
public async forceReload(): Promise<void> {
|
||||||
await this.request(`${this._url}/force-reload`, {
|
await this.request(`${this._url}/force-reload`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
@ -50,6 +50,7 @@ export class DocApiForwarder {
|
|||||||
app.use('/api/docs/:docId/apply', withDoc);
|
app.use('/api/docs/:docId/apply', withDoc);
|
||||||
app.use('/api/docs/:docId/attachments', withDoc);
|
app.use('/api/docs/:docId/attachments', withDoc);
|
||||||
app.use('/api/docs/:docId/snapshots', withDoc);
|
app.use('/api/docs/:docId/snapshots', withDoc);
|
||||||
|
app.use('/api/docs/:docId/usersForViewAs', withDoc);
|
||||||
app.use('/api/docs/:docId/replace', withDoc);
|
app.use('/api/docs/:docId/replace', withDoc);
|
||||||
app.use('/api/docs/:docId/flush', withDoc);
|
app.use('/api/docs/:docId/flush', withDoc);
|
||||||
app.use('/api/docs/:docId/states', withDoc);
|
app.use('/api/docs/:docId/states', withDoc);
|
||||||
|
@ -1504,7 +1504,7 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* Example users are always included.
|
* Example users are always included.
|
||||||
*/
|
*/
|
||||||
public async getUsersForViewAs(docSession: DocSession): Promise<PermissionDataWithExtraUsers> {
|
public async getUsersForViewAs(docSession: OptDocSession): Promise<PermissionDataWithExtraUsers> {
|
||||||
// Make sure we have rights to view access rules.
|
// Make sure we have rights to view access rules.
|
||||||
const db = this.getHomeDbManager();
|
const db = this.getHomeDbManager();
|
||||||
if (!db || !await this._granularAccess.hasAccessRulesPermission(docSession)) {
|
if (!db || !await this._granularAccess.hasAccessRulesPermission(docSession)) {
|
||||||
|
@ -673,6 +673,11 @@ export class DocWorkerApi {
|
|||||||
res.json({snapshots});
|
res.json({snapshots});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this._app.get('/api/docs/:docId/usersForViewAs', isOwner, withDoc(async (activeDoc, req, res) => {
|
||||||
|
const docSession = docSessionFromRequest(req);
|
||||||
|
res.json(await activeDoc.getUsersForViewAs(docSession));
|
||||||
|
}));
|
||||||
|
|
||||||
this._app.post('/api/docs/:docId/snapshots/remove', isOwner, withDoc(async (activeDoc, req, res) => {
|
this._app.post('/api/docs/:docId/snapshots/remove', isOwner, withDoc(async (activeDoc, req, res) => {
|
||||||
const docSession = docSessionFromRequest(req);
|
const docSession = docSessionFromRequest(req);
|
||||||
const snapshotIds = req.body.snapshotIds as string[];
|
const snapshotIds = req.body.snapshotIds as string[];
|
||||||
|
@ -567,7 +567,6 @@
|
|||||||
"Reset": "Reset",
|
"Reset": "Reset",
|
||||||
"AddTableRules": "Add Table Rules",
|
"AddTableRules": "Add Table Rules",
|
||||||
"AddUserAttributes": "Add User Attributes",
|
"AddUserAttributes": "Add User Attributes",
|
||||||
"Users": "Users",
|
|
||||||
"UserAttributes": "User Attributes",
|
"UserAttributes": "User Attributes",
|
||||||
"AttributeToLookUp": "Attribute to Look Up",
|
"AttributeToLookUp": "Attribute to Look Up",
|
||||||
"LookupTable": "Lookup Table",
|
"LookupTable": "Lookup Table",
|
||||||
@ -591,7 +590,13 @@
|
|||||||
"RemoveRulesMentioningTable": "Remove {{- tableId }} rules",
|
"RemoveRulesMentioningTable": "Remove {{- tableId }} rules",
|
||||||
"RemoveRulesMentioningColumn": "Remove column {{- colId }} from {{- tableId }} rules",
|
"RemoveRulesMentioningColumn": "Remove column {{- colId }} from {{- tableId }} rules",
|
||||||
"RemoveUserAttribute": "Remove {{- name }} user attribute",
|
"RemoveUserAttribute": "Remove {{- name }} user attribute",
|
||||||
"MemoEditorPlaceholder": "Type a message..."
|
"MemoEditorPlaceholder": "Type a message...",
|
||||||
|
"ViewAs": "View As"
|
||||||
|
},
|
||||||
|
"ViewAsDropdown": {
|
||||||
|
"ViewAs": "View As",
|
||||||
|
"UsersFrom": "Users from table",
|
||||||
|
"ExampleUsers": "Example Users"
|
||||||
},
|
},
|
||||||
"PermissionsWidget": {
|
"PermissionsWidget": {
|
||||||
"AllowAll": "Allow All",
|
"AllowAll": "Allow All",
|
||||||
@ -729,7 +734,7 @@
|
|||||||
"PluginColon": "Plugin: ",
|
"PluginColon": "Plugin: ",
|
||||||
"SectionColon": "Section: "
|
"SectionColon": "Section: "
|
||||||
},
|
},
|
||||||
"Drafts": {
|
"Drafts": {
|
||||||
"UndoDiscard":"Undo discard",
|
"UndoDiscard":"Undo discard",
|
||||||
"RestoreLastEdit":"Restore last edit"
|
"RestoreLastEdit":"Restore last edit"
|
||||||
},
|
},
|
||||||
@ -777,6 +782,9 @@
|
|||||||
"ValidationPanel": {
|
"ValidationPanel": {
|
||||||
"RuleLength":"Rule {{length}}",
|
"RuleLength":"Rule {{length}}",
|
||||||
"UpdateFormula":"Update formula (Shift+Enter)"
|
"UpdateFormula":"Update formula (Shift+Enter)"
|
||||||
|
},
|
||||||
|
"ViewAsBanner": {
|
||||||
|
"UnknownUser": "Unknown User"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user