mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) annotate shares listed in UserManager for documents
Summary: This gives more guidance to users when editing document shares in the UserManager dialog. * For a document on a team site, any shares with team members are marked `Team member`. * Shares that count as external collaborators are marked for documents on a team or personal site as `collaborator` (personal site) or `outside collaborator` (team site). * Collaborators are marked `1 of 2`, `2 of 2`, and then `limit exceeded`. * On a team site, links are offered for each collaborator to add them to the team. The links lead to a prefilled dialog for managing team membership which can be confirmed immediately, allowing the user to continue without interruption. * On a personal site, for the last collaborator and beyond, a link is added for creating a team. This isn't seamless since creating a team involves billing etc. There's a small unrelated tweak in tests to remove a confusing import from `test/browser` in `test/server`. One thing I didn't get to is checking if owner of doc is owner of site. If they aren't, they may try to add a member and be denied at that point - it would be more polite to change messaging earlier for them. Test Plan: added and updated tests Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3083
This commit is contained in:
90
app/common/ShareAnnotator.ts
Normal file
90
app/common/ShareAnnotator.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { normalizeEmail } from 'app/common/emails';
|
||||
import { Features } from 'app/common/Features';
|
||||
import { PermissionData, PermissionDelta } from 'app/common/UserAPI';
|
||||
|
||||
/**
|
||||
* Mark that the share is share number #at of a maximum of #top. The #at values
|
||||
* start at 1.
|
||||
*/
|
||||
export interface ShareLimitAnnotation {
|
||||
at: number;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some facts about a share.
|
||||
*/
|
||||
export interface ShareAnnotation {
|
||||
isMember?: boolean; // Is the share for a team member.
|
||||
isSupport?: boolean; // Is the share for a support user.
|
||||
collaboratorLimit?: ShareLimitAnnotation; // Does the share count towards a collaborator limit.
|
||||
}
|
||||
|
||||
/**
|
||||
* Facts about all shares for a resource.
|
||||
*/
|
||||
export interface ShareAnnotations {
|
||||
hasTeam?: boolean; // Is the resource in a team site?
|
||||
users: Map<string, ShareAnnotation>; // Annotations keyed by normalized user email.
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for annotating users mentioned in a proposed change of shares, given the
|
||||
* current shares in place.
|
||||
*/
|
||||
export class ShareAnnotator {
|
||||
constructor(private _features: Features, private _state: PermissionData) {
|
||||
}
|
||||
|
||||
public updateState(state: PermissionData) {
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
public annotateChanges(change: PermissionDelta): ShareAnnotations {
|
||||
const features = this._features;
|
||||
const annotations: ShareAnnotations = {
|
||||
hasTeam: features.maxWorkspacesPerOrg !== 1,
|
||||
users: new Map(),
|
||||
};
|
||||
if (features.maxSharesPerDocPerRole || features.maxSharesPerWorkspace) {
|
||||
// For simplicity, don't try to annotate if limits not used at the time of writing
|
||||
// are in place.
|
||||
return annotations;
|
||||
}
|
||||
const top = features.maxSharesPerDoc;
|
||||
let at = 0;
|
||||
const makeAnnotation = (user: {email: string, isMember?: boolean, access: string|null}) => {
|
||||
const annotation: ShareAnnotation = {
|
||||
isMember: user.isMember,
|
||||
};
|
||||
if (user.email === 'support@getgrist.com') {
|
||||
return { isSupport: true };
|
||||
}
|
||||
if (!annotation.isMember && user.access) {
|
||||
at++;
|
||||
annotation.collaboratorLimit = {
|
||||
at,
|
||||
top
|
||||
};
|
||||
}
|
||||
return annotation;
|
||||
};
|
||||
const removed = new Set(
|
||||
Object.entries(change?.users||{}).filter(([, v]) => v === null)
|
||||
.map(([k, ]) => normalizeEmail(k)));
|
||||
for (const user of this._state.users) {
|
||||
if (removed.has(user.email)) { continue; }
|
||||
annotations.users.set(user.email, makeAnnotation(user));
|
||||
}
|
||||
const tweaks = new Set(
|
||||
Object.entries(change?.users||{}).filter(([, v]) => v !== null)
|
||||
.map(([k, ]) => normalizeEmail(k)));
|
||||
for (const email of tweaks) {
|
||||
const annotation = annotations.users.get(email) || makeAnnotation({
|
||||
email, isMember: false, access: '<set>',
|
||||
});
|
||||
annotations.users.set(email, annotation);
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,9 @@ export interface UserAccessData {
|
||||
// access to the resource. Lack of access to the parent resource is represented by a null value.
|
||||
// If parent has non-inheritable access, this should be null.
|
||||
parentAccess?: roles.BasicRole|null;
|
||||
orgAccess?: roles.BasicRole|null;
|
||||
anonymous?: boolean; // If set to true, the user is the anonymous user.
|
||||
isMember?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface IGristUrlState {
|
||||
welcome?: WelcomePage;
|
||||
welcomeTour?: boolean;
|
||||
docTour?: boolean;
|
||||
manageUsers?: boolean;
|
||||
params?: {
|
||||
billingPlan?: string;
|
||||
billingTask?: BillingTask;
|
||||
@@ -214,6 +215,8 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
|
||||
url.hash = 'repeat-welcome-tour';
|
||||
} else if (state.docTour) {
|
||||
url.hash = 'repeat-doc-tour';
|
||||
} else if (state.manageUsers) {
|
||||
url.hash = 'manage-users';
|
||||
} else {
|
||||
url.hash = '';
|
||||
}
|
||||
@@ -315,6 +318,7 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
|
||||
}
|
||||
state.welcomeTour = hashMap.get('#') === 'repeat-welcome-tour';
|
||||
state.docTour = hashMap.get('#') === 'repeat-doc-tour';
|
||||
state.manageUsers = hashMap.get('#') === 'manage-users';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user