feat: add teams (#848)

## Description

Add support for teams which will allow users to collaborate on
documents.

Teams features allows users to:

- Create, manage and transfer teams
- Manage team members
- Manage team emails
- Manage a shared team inbox and documents

These changes do NOT include the following, which are planned for a
future release:

- Team templates
- Team API
- Search menu integration

## Testing Performed

- Added E2E tests for general team management
- Added E2E tests to validate document counts

## Checklist

- [X] I have tested these changes locally and they work as expected.
- [X] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [X] I have followed the project's coding style guidelines.
This commit is contained in:
David Nguyen
2024-02-06 16:16:10 +11:00
committed by GitHub
parent c6457e75e0
commit 0c339b78b6
200 changed files with 12916 additions and 968 deletions

View File

@ -0,0 +1,16 @@
import type { Subscription } from '.prisma/client';
import { SubscriptionStatus } from '.prisma/client';
/**
* Returns true if there is a subscription that is active and is a community plan.
*/
export const subscriptionsContainsActiveCommunityPlan = (
subscriptions: Subscription[],
communityPlanPriceIds: string[],
) => {
return subscriptions.some(
(subscription) =>
subscription.status === SubscriptionStatus.ACTIVE &&
communityPlanPriceIds.includes(subscription.priceId),
);
};

View File

@ -0,0 +1,30 @@
/**
* From an unknown string, parse it into an integer array.
*
* Filter out unknown values.
*/
export const parseToIntegerArray = (value: unknown): number[] => {
if (typeof value !== 'string') {
return [];
}
return value
.split(',')
.map((value) => parseInt(value, 10))
.filter((value) => !isNaN(value));
};
type GetRootHrefOptions = {
returnEmptyRootString?: boolean;
};
export const getRootHref = (
params: Record<string, string | string[]> | null,
options: GetRootHrefOptions = {},
) => {
if (typeof params?.teamUrl === 'string') {
return `/t/${params.teamUrl}`;
}
return options.returnEmptyRootString ? '' : '/';
};

View File

@ -1,6 +1,6 @@
import type { Recipient } from '@documenso/prisma/client';
export const recipientInitials = (text: string) =>
export const extractInitials = (text: string) =>
text
.split(' ')
.map((name: string) => name.slice(0, 1).toUpperCase())
@ -8,5 +8,5 @@ export const recipientInitials = (text: string) =>
.join('');
export const recipientAbbreviation = (recipient: Recipient) => {
return recipientInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase();
return extractInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase();
};

View File

@ -0,0 +1,42 @@
import { WEBAPP_BASE_URL } from '../constants/app';
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../constants/teams';
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
const formattedBaseUrl = (baseUrl ?? WEBAPP_BASE_URL).replace(/https?:\/\//, '');
return `${formattedBaseUrl}/t/${teamUrl}`;
};
export const formatDocumentsPath = (teamUrl?: string) => {
return teamUrl ? `/t/${teamUrl}/documents` : '/documents';
};
/**
* Determines whether a team member can execute a given action.
*
* @param action The action the user is trying to execute.
* @param role The current role of the user.
* @returns Whether the user can execute the action.
*/
export const canExecuteTeamAction = (
action: keyof typeof TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
role: keyof typeof TEAM_MEMBER_ROLE_MAP,
) => {
return TEAM_MEMBER_ROLE_PERMISSIONS_MAP[action].some((i) => i === role);
};
/**
* Compares the provided `currentUserRole` with the provided `roleToCheck` to determine
* whether the `currentUserRole` has permission to modify the `roleToCheck`.
*
* @param currentUserRole Role of the current user
* @param roleToCheck Role of another user to see if the current user can modify
* @returns True if the current user can modify the other user, false otherwise
*/
export const isTeamRoleWithinUserHierarchy = (
currentUserRole: keyof typeof TEAM_MEMBER_ROLE_MAP,
roleToCheck: keyof typeof TEAM_MEMBER_ROLE_MAP,
) => {
return TEAM_MEMBER_ROLE_HIERARCHY[currentUserRole].some((i) => i === roleToCheck);
};

View File

@ -0,0 +1,21 @@
import type { DurationLike } from 'luxon';
import { DateTime } from 'luxon';
import { nanoid } from 'nanoid';
/**
* Create a token verification object.
*
* @param expiry The date the token expires, or the duration until the token expires.
*/
export const createTokenVerification = (expiry: Date | DurationLike) => {
const expiresAt = expiry instanceof Date ? expiry : DateTime.now().plus(expiry).toJSDate();
return {
expiresAt,
token: nanoid(32),
};
};
export const isTokenExpired = (expiresAt: Date) => {
return expiresAt < new Date();
};