mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
#1047 access signaling using workspace/doc icon
This PR signals access of each workspace and document using their icons. The color of icon is different for each access level: * shared with everyone or anonymous -> icon is green; * shared with other people than yourself (the owner) but not with everybody -> icon is yellow; * shared only with you (the owner)-> icon is gray.
This commit is contained in:
parent
a1b5358c86
commit
acc88fe36c
@ -137,7 +137,10 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
hasFeaturedTemplates ? t("More Examples and Templates") : t("Examples and Templates")
|
||||
) :
|
||||
page === 'trash' ? t("Trash") :
|
||||
workspace && [css.docHeaderIcon('Folder'), workspaceName(home.app, workspace)]
|
||||
workspace && [css.docHeaderIcon('Folder',
|
||||
css.docHeaderIcon.cls(workspace.shareType ? `-${workspace.shareType}` : '')
|
||||
),
|
||||
workspaceName(home.app, workspace)]
|
||||
),
|
||||
testId('doc-header'),
|
||||
)
|
||||
@ -201,7 +204,9 @@ function buildAllDocsBlock(
|
||||
return css.docBlock(
|
||||
css.docBlockHeaderLink(
|
||||
css.wsLeft(
|
||||
css.docHeaderIcon('Folder'),
|
||||
css.docHeaderIcon('Folder',
|
||||
css.docHeaderIcon.cls(ws.shareType ? `-${ws.shareType}` : '')
|
||||
),
|
||||
workspaceName(home.app, ws),
|
||||
),
|
||||
|
||||
@ -278,7 +283,9 @@ function buildAllTemplates(home: HomeModel, templateWorkspaces: Observable<Works
|
||||
return css.templatesDocBlock(
|
||||
css.templateBlockHeader(
|
||||
css.wsLeft(
|
||||
css.docHeaderIcon('Folder'),
|
||||
css.docHeaderIcon('Folder',
|
||||
css.docHeaderIcon.cls(workspace.shareType ? `-${workspace.shareType}` : '')
|
||||
),
|
||||
workspace.name,
|
||||
),
|
||||
testId('templates-header'),
|
||||
@ -403,7 +410,9 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI
|
||||
css.docLeft(
|
||||
css.docName(doc.name, testId('doc-name')),
|
||||
css.docPinIcon('PinSmall', dom.show(doc.isPinned)),
|
||||
doc.public ? css.docPublicIcon('Public', testId('public')) : null,
|
||||
doc.public ? css.docPublicIcon('Public',
|
||||
css.docPublicIcon.cls(doc.shareType ? `-${doc.shareType}` : ''),
|
||||
testId('public')) : null,
|
||||
),
|
||||
css.docRowUpdatedAt(
|
||||
(doc.removedAt ?
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {transientInput} from 'app/client/ui/transientInput';
|
||||
import {mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {colors, mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {styled} from 'grainjs';
|
||||
import {bigBasicButton} from 'app/client/ui2018/buttons';
|
||||
@ -123,6 +123,12 @@ export const docHeaderIcon = styled(icon, `
|
||||
margin-right: 8px;
|
||||
margin-top: -3px;
|
||||
--icon-color: ${theme.lightText};
|
||||
&-everyone {
|
||||
--icon-color: ${colors.lightGreen};
|
||||
}
|
||||
&-others {
|
||||
--icon-color: ${colors.warning};
|
||||
}
|
||||
`);
|
||||
|
||||
export const pinnedDocsIcon = styled(docHeaderIcon, `
|
||||
@ -227,7 +233,13 @@ export const docPinIcon = styled(icon, `
|
||||
export const docPublicIcon = styled(icon, `
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
--icon-color: ${theme.lightText};
|
||||
&-everyone {
|
||||
--icon-color: ${colors.lightGreen};
|
||||
}
|
||||
&-others {
|
||||
--icon-color: ${colors.warning};
|
||||
}
|
||||
`);
|
||||
|
||||
export const docEditorInput = styled(transientInput, `
|
||||
|
@ -74,7 +74,10 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
dom.autoDispose(isTrivial),
|
||||
dom.hide(isTrivial),
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentWSId) === ws.id),
|
||||
cssPageLink(cssPageIcon('Folder'), cssLinkText(workspaceName(home.app, ws)),
|
||||
cssPageLink(
|
||||
cssPageIcon('Folder',
|
||||
cssPageIcon.cls(ws.shareType ? `-${ws.shareType}` : '')
|
||||
), cssLinkText(workspaceName(home.app, ws)),
|
||||
dom.hide(isRenaming),
|
||||
urlState().setLinkUrl({ws: ws.id}),
|
||||
// Don't show menu if workspace is personal and shared by another user; we could
|
||||
@ -93,7 +96,10 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
),
|
||||
cssPageEntry.cls('-renaming', isRenaming),
|
||||
dom.maybe(isRenaming, () =>
|
||||
cssPageLink(cssPageIcon('Folder'),
|
||||
cssPageLink(
|
||||
cssPageIcon('Folder',
|
||||
cssPageIcon.cls(ws.shareType ? `-${ws.shareType}` : '')
|
||||
),
|
||||
cssEditorInput({
|
||||
initialValue: ws.name || '',
|
||||
save: async (val) => (val !== ws.name) ? home.renameWorkspace(ws.id, val) : undefined,
|
||||
|
@ -16,7 +16,7 @@
|
||||
import {beaconOpenMessage} from 'app/client/lib/helpScout';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
@ -155,6 +155,13 @@ export const cssLinkText = styled('span', `
|
||||
export const cssPageIcon = styled(icon, `
|
||||
flex: none;
|
||||
margin-right: var(--page-icon-margin, 8px);
|
||||
--icon-color: ${theme.lightText};
|
||||
&-everyone {
|
||||
--icon-color: ${colors.lightGreen};
|
||||
}
|
||||
&-others {
|
||||
--icon-color: ${colors.warning};
|
||||
}
|
||||
.${cssTools.className}-collapsed & {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -45,7 +45,9 @@ export function buildPinnedDoc(home: HomeModel, doc: Document, workspace: Worksp
|
||||
cssImage({src: doc.options.icon}) :
|
||||
[docInitials(doc.name), pinnedDocThumbnail()]
|
||||
),
|
||||
(doc.public && !isExample ? cssPublicIcon('PublicFilled', testId('public')) : null),
|
||||
(!isExample ? cssPublicIcon('PublicFilled',
|
||||
cssPublicIcon.cls(doc.shareType ? `-${doc.shareType}` : ''),
|
||||
testId('public')) : null),
|
||||
pinnedDocPreview.cls('-with-icon', Boolean(doc.options?.icon)),
|
||||
),
|
||||
pinnedDocFooter(
|
||||
@ -274,5 +276,11 @@ const cssPublicIcon = styled(icon, `
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
--icon-color: ${theme.accentIcon};
|
||||
--icon-color: ${theme.lightText};
|
||||
&-everyone {
|
||||
--icon-color: ${colors.lightGreen};
|
||||
}
|
||||
&-others {
|
||||
--icon-color: ${colors.warning};
|
||||
}
|
||||
`);
|
||||
|
@ -116,6 +116,7 @@ export interface Workspace extends WorkspaceProperties {
|
||||
org: Organization;
|
||||
orgDomain?: string;
|
||||
access: roles.Role;
|
||||
shareType?: string;
|
||||
owner?: FullUser; // Set when workspaces are in the "docs" pseudo-organization,
|
||||
// assembled from multiple personal organizations.
|
||||
// Not set when workspaces are all from the same organization.
|
||||
@ -158,6 +159,7 @@ export interface Document extends DocumentProperties {
|
||||
id: string;
|
||||
workspace: Workspace;
|
||||
access: roles.Role;
|
||||
shareType?: string;
|
||||
trunkAccess?: roles.Role|null;
|
||||
forks?: Fork[];
|
||||
}
|
||||
|
@ -1016,7 +1016,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
options: QueryOptions = {}): Promise<QueryResult<Workspace[]>> {
|
||||
const query = this._orgWorkspaces(scope, orgKey, options);
|
||||
// Allow an empty result for the merged org for the anonymous user. The anonymous user
|
||||
// has no home org or workspace. For all other sitations, expect at least one workspace.
|
||||
// has no home org or workspace. For all other situations, expect at least one workspace.
|
||||
const emptyAllowed = this.isMergedOrg(orgKey) && scope.userId === this.getAnonymousUserId();
|
||||
const result = await this._verifyAclPermissions(query, { scope, emptyAllowed });
|
||||
// Return the workspaces, not the org(s).
|
||||
@ -1027,6 +1027,39 @@ export class HomeDBManager extends EventEmitter {
|
||||
ws.owner = o.owner;
|
||||
// Include the org's domain so that the UI can build doc URLs that include the org.
|
||||
ws.orgDomain = o.domain;
|
||||
|
||||
// Include shareType based on users permissions to set icon color
|
||||
const {maxInheritedRole, users } = this.unwrapQueryResult(
|
||||
await this.getWorkspaceAccess({userId: scope.userId}, ws.id, true)
|
||||
);
|
||||
const permissionDataUsers = users.filter((user) => {
|
||||
// effectiveRole - Gives the effective role of the member on the resource, taking everything into account.
|
||||
return !!roles.getStrongestRole(user.access,
|
||||
roles.getWeakestRole(<roles.Role>user.parentAccess, <roles.Role>maxInheritedRole));
|
||||
});
|
||||
if (permissionDataUsers?.length > 1) {
|
||||
ws.shareType = permissionDataUsers.find((user) => user.email === EVERYONE_EMAIL
|
||||
|| user.email === ANONYMOUS_USER_EMAIL)
|
||||
? 'everyone'
|
||||
: permissionDataUsers.find((user) => user.id !== scope.userId)
|
||||
? 'others'
|
||||
: '';
|
||||
|
||||
for(const doc of ws.docs) {
|
||||
const permissionDataUsersDoc = this.unwrapQueryResult(
|
||||
await this.getDocAccess({userId: scope.userId, urlId: doc.urlId}, {
|
||||
flatten: true, excludeUsersWithoutAccess: true, realAccess: true
|
||||
})).users;
|
||||
if (permissionDataUsersDoc && permissionDataUsersDoc.length > 1) {
|
||||
doc.shareType = permissionDataUsersDoc.find((user) => user.email === EVERYONE_EMAIL
|
||||
|| user.email === ANONYMOUS_USER_EMAIL)
|
||||
? 'everyone'
|
||||
: permissionDataUsersDoc.find((user) => user.id !== scope.userId)
|
||||
? 'others'
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// For org-specific requests, we still have the org's workspaces, plus the Samples workspace
|
||||
@ -2421,7 +2454,8 @@ export class HomeDBManager extends EventEmitter {
|
||||
// Returns UserAccessData for all users with any permissions on the ORG, as well as the
|
||||
// maxInheritedRole set on the workspace. Note that information for all users in the org
|
||||
// is given to indicate which users have access to the org but not to this particular workspace.
|
||||
public async getWorkspaceAccess(scope: Scope, wsId: number): Promise<QueryResult<PermissionData>> {
|
||||
public async getWorkspaceAccess(scope: Scope, wsId: number,
|
||||
realAccess?: boolean): Promise<QueryResult<PermissionData>> {
|
||||
// Run the query for the workspace and org in a transaction. This brings some isolation protection
|
||||
// against changes to the workspace or org while we are querying.
|
||||
const { workspace, org, queryFailure } = await this._connection.transaction(async manager => {
|
||||
@ -2467,7 +2501,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
};
|
||||
});
|
||||
const maxInheritedRole = this._getMaxInheritedRole(workspace);
|
||||
const personal = this._filterAccessData(scope, users, maxInheritedRole);
|
||||
const personal = this._filterAccessData(scope, users, maxInheritedRole, realAccess);
|
||||
return {
|
||||
status: 200,
|
||||
data: {
|
||||
@ -2498,6 +2532,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
public async getDocAccess(scope: DocScope, options?: {
|
||||
flatten?: boolean,
|
||||
excludeUsersWithoutAccess?: boolean,
|
||||
realAccess?: boolean
|
||||
}): Promise<QueryResult<PermissionData>> {
|
||||
// Doc permissions of forks are based on the "trunk" document, so make sure
|
||||
// we look up permissions of trunk if we are on a fork (we'll fix the permissions
|
||||
@ -2550,7 +2585,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
maxInheritedRole = null;
|
||||
}
|
||||
|
||||
const personal = this._filterAccessData(scope, users, maxInheritedRole, doc.id);
|
||||
const personal = this._filterAccessData(scope, users, maxInheritedRole, options?.realAccess, doc.id);
|
||||
|
||||
// If we are on a fork, make any access changes needed. Assumes results
|
||||
// have been flattened.
|
||||
@ -4867,6 +4902,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
scope: Scope,
|
||||
users: UserAccessData[],
|
||||
maxInheritedRole: roles.BasicRole|null,
|
||||
realAccess?: boolean,
|
||||
docId?: string
|
||||
): {personal: true, public: boolean}|undefined {
|
||||
if (scope.userId === this.getPreviewerUserId()) { return; }
|
||||
@ -4877,15 +4913,15 @@ export class HomeDBManager extends EventEmitter {
|
||||
const thisUser = this.getAnonymousUserId() === scope.userId
|
||||
? null
|
||||
: users.find(user => user.id === scope.userId);
|
||||
const realAccess = thisUser ? getRealAccess(thisUser, { maxInheritedRole, users }) : null;
|
||||
const realAccessUser = thisUser ? getRealAccess(thisUser, { maxInheritedRole, users }) : null;
|
||||
|
||||
// If we are an owner, don't filter user information.
|
||||
if (thisUser && realAccess === 'owners') { return; }
|
||||
if ((thisUser && realAccessUser === 'owners') || realAccess) { return; }
|
||||
|
||||
// Limit user information returned to being about the current user.
|
||||
users.length = 0;
|
||||
if (thisUser) { users.push(thisUser); }
|
||||
return { personal: true, public: !realAccess };
|
||||
return { personal: true, public: !realAccessUser };
|
||||
}
|
||||
|
||||
private _getWorkspaceWithACLRules(scope: Scope, wsId: number, options: Partial<QueryOptions> = {}) {
|
||||
|
Loading…
Reference in New Issue
Block a user