Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): impl team workspace #9026

Merged
merged 4 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/backend/server/src/core/workspaces/resolvers/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,21 @@ export class WorkspaceResolver {
return data?.user?.id === user.id;
}

@Query(() => Boolean, {
description: 'Get is admin of workspace',
complexity: 2,
})
async isAdmin(
@CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: string
) {
return this.permissions.tryCheckWorkspaceIs(
workspaceId,
user.id,
Permission.Admin
);
}

@Query(() => [WorkspaceType], {
description: 'Get all accessible workspaces for current user',
complexity: 2,
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/server/src/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@ type Query {
"""send workspace invitation"""
getInviteInfo(inviteId: String!): InvitationType!

"""Get is admin of workspace"""
isAdmin(workspaceId: String!): Boolean!

"""Get is owner of workspace"""
isOwner(workspaceId: String!): Boolean!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface WorkspaceProfileInfo {
avatar?: string;
name?: string;
isOwner?: boolean;
isAdmin?: boolean;
isTeam?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { emailRegex } from '@affine/component/auth-components';
import type { WorkspaceInviteLinkExpireTime } from '@affine/graphql';
import { Permission } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { useCallback, useEffect, useState } from 'react';
Expand All @@ -14,6 +15,10 @@ export interface InviteTeamMemberModalProps {
onConfirm: (params: { email: string; permission: Permission }) => void;
isMutating: boolean;
copyTextToClipboard: (text: string) => Promise<boolean>;
onGenerateInviteLink: (
expireTime: WorkspaceInviteLinkExpireTime
) => Promise<string>;
onRevokeInviteLink: () => Promise<boolean>;
}

export const InviteTeamMemberModal = ({
Expand All @@ -22,6 +27,8 @@ export const InviteTeamMemberModal = ({
onConfirm,
isMutating,
copyTextToClipboard,
onGenerateInviteLink,
onRevokeInviteLink,
}: InviteTeamMemberModalProps) => {
const t = useI18n();
const [inviteEmail, setInviteEmail] = useState('');
Expand Down Expand Up @@ -91,6 +98,8 @@ export const InviteTeamMemberModal = ({
inviteMethod={inviteMethod}
onInviteMethodChange={setInviteMethod}
copyTextToClipboard={copyTextToClipboard}
onGenerateInviteLink={onGenerateInviteLink}
onRevokeInviteLink={onRevokeInviteLink}
/>
</ConfirmModal>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WorkspaceInviteLinkExpireTime } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons/rc';
import { cssVar } from '@toeverything/theme';
Expand All @@ -15,35 +16,43 @@ const getMenuItems = (t: ReturnType<typeof useI18n>) => [
label: t['com.affine.payment.member.team.invite.expiration-date']({
number: '1',
}),
value: 1,
value: WorkspaceInviteLinkExpireTime.OneDay,
},
{
label: t['com.affine.payment.member.team.invite.expiration-date']({
number: '3',
}),
value: 3,
value: WorkspaceInviteLinkExpireTime.ThreeDays,
},
{
label: t['com.affine.payment.member.team.invite.expiration-date']({
number: '7',
}),
value: 7,
value: WorkspaceInviteLinkExpireTime.OneWeek,
},
{
label: t['com.affine.payment.member.team.invite.expiration-date']({
number: '30',
}),
value: 30,
value: WorkspaceInviteLinkExpireTime.OneMonth,
},
];

export const LinkInvite = ({
copyTextToClipboard,
generateInvitationLink,
revokeInvitationLink,
}: {
generateInvitationLink: (
expireTime: WorkspaceInviteLinkExpireTime
) => Promise<string>;
revokeInvitationLink: () => Promise<boolean>;
copyTextToClipboard: (text: string) => Promise<boolean>;
}) => {
const t = useI18n();
const [selectedValue, setSelectedValue] = useState(7);
const [selectedValue, setSelectedValue] = useState(
WorkspaceInviteLinkExpireTime.OneWeek
);
const [invitationLink, setInvitationLink] = useState('');
const menuItems = getMenuItems(t);
const items = useMemo(() => {
Expand All @@ -59,10 +68,19 @@ export const LinkInvite = ({
[menuItems, selectedValue]
);

//TODO(@JimmFly): implement team feature
const onGenerate = useCallback(() => {
setInvitationLink('ggsimida');
}, []);
generateInvitationLink(selectedValue)
.then(link => {
setInvitationLink(link);
})
.catch(err => {
console.error('Failed to generate invitation link: ', err);
notify.error({
title: 'Failed to generate invitation link',
message: err.message,
});
});
}, [generateInvitationLink, selectedValue]);

const onCopy = useCallback(() => {
copyTextToClipboard(invitationLink)
Expand All @@ -81,8 +99,18 @@ export const LinkInvite = ({
}, [copyTextToClipboard, invitationLink, t]);

const onReset = useCallback(() => {
setInvitationLink('');
}, []);
revokeInvitationLink()
.then(() => {
setInvitationLink('');
})
.catch(err => {
console.error('Failed to revoke invitation link: ', err);
notify.error({
title: 'Failed to revoke invitation link',
message: err.message,
});
});
}, [revokeInvitationLink]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { WorkspaceInviteLinkExpireTime } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { EmailIcon, LinkIcon } from '@blocksuite/icons/rc';

Expand All @@ -16,6 +17,8 @@ export const ModalContent = ({
isMutating,
isValidEmail,
copyTextToClipboard,
onGenerateInviteLink,
onRevokeInviteLink,
}: {
inviteEmail: string;
setInviteEmail: (value: string) => void;
Expand All @@ -25,6 +28,10 @@ export const ModalContent = ({
isMutating: boolean;
isValidEmail: boolean;
copyTextToClipboard: (text: string) => Promise<boolean>;
onGenerateInviteLink: (
expireTime: WorkspaceInviteLinkExpireTime
) => Promise<string>;
onRevokeInviteLink: () => Promise<boolean>;
}) => {
const t = useI18n();

Expand Down Expand Up @@ -67,7 +74,11 @@ export const ModalContent = ({
isValidEmail={isValidEmail}
/>
) : (
<LinkInvite copyTextToClipboard={copyTextToClipboard} />
<LinkInvite
copyTextToClipboard={copyTextToClipboard}
generateInvitationLink={onGenerateInviteLink}
revokeInvitationLink={onRevokeInviteLink}
/>
)}
</div>
);
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ type TypeFormInfo = {
const getTypeFormLink = (id: string, info: TypeFormInfo) => {
const plans = Array.isArray(info.plan) ? info.plan : [info.plan];
const product_id = plans
.map(plan => (plan === SubscriptionPlan.AI ? 'ai' : 'cloud'))
.map(plan =>
plan === SubscriptionPlan.AI
? 'ai'
: plan === SubscriptionPlan.Team
? 'team'
: 'cloud'
)
.join('-');
const product_price =
info.recurring === SubscriptionRecurring.Monthly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
LocalWorkspaceIcon,
NoNetworkIcon,
SettingsIcon,
TeamWorkspaceIcon,
UnsyncIcon,
} from '@blocksuite/icons/rc';
import {
Expand Down Expand Up @@ -241,6 +242,7 @@ export const WorkspaceCard = forwardRef<
avatarSize?: number;
disable?: boolean;
hideCollaborationIcon?: boolean;
hideTeamWorkspaceIcon?: boolean;
active?: boolean;
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
Expand All @@ -257,6 +259,7 @@ export const WorkspaceCard = forwardRef<
className,
disable,
hideCollaborationIcon,
hideTeamWorkspaceIcon,
active,
...props
},
Expand Down Expand Up @@ -326,12 +329,9 @@ export const WorkspaceCard = forwardRef<
{hideCollaborationIcon || information?.isOwner ? null : (
<CollaborationIcon className={styles.collaborationIcon} />
)}
{/* {hideTeamWorkspaceIcon || !isTeamWorkspace ? null : (
<Tooltip content={'team Workspace owned by xxx'}>
<TeamWorkspaceIcon className={styles.collaborationIcon} />
</Tooltip>

)} */}
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
<TeamWorkspaceIcon className={styles.collaborationIcon} />
)}
{onClickOpenSettings && (
<div className={styles.settingButton} onClick={onOpenSettings}>
<SettingsIcon width={16} height={16} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,69 @@ export const CancelAction = ({
);
};

export const CancelTeamAction = ({
children,
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
} & PropsWithChildren) => {
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
const [isMutating, setIsMutating] = useState(false);
const subscription = useService(SubscriptionService).subscription;
const teamSubscription = useLiveData(subscription.team$);
const authService = useService(AuthService);
const downgradeNotify = useDowngradeNotify();

const downgrade = useAsyncCallback(async () => {
try {
const account = authService.session.account$.value;
const prevRecurring = teamSubscription?.recurring;
setIsMutating(true);
await subscription.cancelSubscription(idempotencyKey);
subscription.revalidate();
await subscription.isRevalidating$.waitFor(v => !v);
// refresh idempotency key
setIdempotencyKey(nanoid());
onOpenChange(false);

if (account && prevRecurring) {
downgradeNotify(
getDowngradeQuestionnaireLink({
email: account.email ?? '',
id: account.id,
name: account.info?.name ?? '',
plan: SubscriptionPlan.Team,
recurring: prevRecurring,
})
);
}
} finally {
setIsMutating(false);
}
}, [
authService.session.account$.value,
teamSubscription,
subscription,
idempotencyKey,
onOpenChange,
downgradeNotify,
]);

return (
<>
{children}
<DowngradeModal
open={open}
onCancel={downgrade}
onOpenChange={onOpenChange}
loading={isMutating}
/>
</>
);
};

/**
* Resume payment action with modal & request
* @param param0
Expand Down
Loading
Loading