mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
feat(core): make permission and invoice offline available (#8123)
This commit is contained in:
@@ -75,6 +75,14 @@ export class PermissionService {
|
|||||||
return owner.user;
|
return owner.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWorkspaceMemberCount(workspaceId: string) {
|
||||||
|
return this.prisma.workspaceUserPermission.count({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async tryGetWorkspaceOwner(workspaceId: string) {
|
async tryGetWorkspaceOwner(workspaceId: string) {
|
||||||
return this.prisma.workspaceUserPermission.findFirst({
|
return this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ export class QuotaManagementService {
|
|||||||
// quota was apply to owner's account
|
// quota was apply to owner's account
|
||||||
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
|
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
|
||||||
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||||
|
const memberCount =
|
||||||
|
await this.permissions.getWorkspaceMemberCount(workspaceId);
|
||||||
const {
|
const {
|
||||||
feature: {
|
feature: {
|
||||||
name,
|
name,
|
||||||
@@ -145,6 +147,7 @@ export class QuotaManagementService {
|
|||||||
humanReadable,
|
humanReadable,
|
||||||
usedSize,
|
usedSize,
|
||||||
unlimited,
|
unlimited,
|
||||||
|
memberCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (quota.unlimited) {
|
if (quota.unlimited) {
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ export class QuotaQueryType {
|
|||||||
@Field(() => SafeIntResolver)
|
@Field(() => SafeIntResolver)
|
||||||
memberLimit!: number;
|
memberLimit!: number;
|
||||||
|
|
||||||
|
@Field(() => SafeIntResolver)
|
||||||
|
memberCount!: number;
|
||||||
|
|
||||||
@Field(() => SafeIntResolver)
|
@Field(() => SafeIntResolver)
|
||||||
storageQuota!: number;
|
storageQuota!: number;
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,7 @@ export class WorkspaceResolver {
|
|||||||
complexity: 2,
|
complexity: 2,
|
||||||
})
|
})
|
||||||
memberCount(@Parent() workspace: WorkspaceType) {
|
memberCount(@Parent() workspace: WorkspaceType) {
|
||||||
return this.prisma.workspaceUserPermission.count({
|
return this.permissions.getWorkspaceMemberCount(workspace.id);
|
||||||
where: {
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => Boolean, {
|
@ResolveField(() => Boolean, {
|
||||||
@@ -388,13 +384,8 @@ export class WorkspaceResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// member limit check
|
// member limit check
|
||||||
const [memberCount, quota] = await Promise.all([
|
const quota = await this.quota.getWorkspaceUsage(workspaceId);
|
||||||
this.prisma.workspaceUserPermission.count({
|
if (quota.memberCount >= quota.memberLimit) {
|
||||||
where: { workspaceId },
|
|
||||||
}),
|
|
||||||
this.quota.getWorkspaceUsage(workspaceId),
|
|
||||||
]);
|
|
||||||
if (memberCount >= quota.memberLimit) {
|
|
||||||
return new MemberQuotaExceeded();
|
return new MemberQuotaExceeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -603,6 +603,7 @@ type QuotaQueryType {
|
|||||||
copilotActionLimit: SafeInt
|
copilotActionLimit: SafeInt
|
||||||
historyPeriod: SafeInt!
|
historyPeriod: SafeInt!
|
||||||
humanReadable: HumanReadableQuotaType!
|
humanReadable: HumanReadableQuotaType!
|
||||||
|
memberCount: SafeInt!
|
||||||
memberLimit: SafeInt!
|
memberLimit: SafeInt!
|
||||||
name: String!
|
name: String!
|
||||||
storageQuota: SafeInt!
|
storageQuota: SafeInt!
|
||||||
|
|||||||
@@ -6,19 +6,21 @@ import ReactPaginate from 'react-paginate';
|
|||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
export interface PaginationProps {
|
export interface PaginationProps {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
pageNum?: number;
|
||||||
countPerPage: number;
|
countPerPage: number;
|
||||||
onPageChange: (skip: number) => void;
|
onPageChange: (skip: number, pageNum: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pagination = ({
|
export const Pagination = ({
|
||||||
totalCount,
|
totalCount,
|
||||||
countPerPage,
|
countPerPage,
|
||||||
|
pageNum,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
}: PaginationProps) => {
|
}: PaginationProps) => {
|
||||||
const handlePageClick = useCallback(
|
const handlePageClick = useCallback(
|
||||||
(e: { selected: number }) => {
|
(e: { selected: number }) => {
|
||||||
const newOffset = (e.selected * countPerPage) % totalCount;
|
const newOffset = (e.selected * countPerPage) % totalCount;
|
||||||
onPageChange(newOffset);
|
onPageChange(newOffset, e.selected);
|
||||||
},
|
},
|
||||||
[countPerPage, onPageChange, totalCount]
|
[countPerPage, onPageChange, totalCount]
|
||||||
);
|
);
|
||||||
@@ -34,6 +36,7 @@ export const Pagination = ({
|
|||||||
pageRangeDisplayed={3}
|
pageRangeDisplayed={3}
|
||||||
marginPagesDisplayed={2}
|
marginPagesDisplayed={2}
|
||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
|
forcePage={pageNum}
|
||||||
previousLabel={<ArrowLeftSmallIcon />}
|
previousLabel={<ArrowLeftSmallIcon />}
|
||||||
nextLabel={<ArrowRightSmallIcon />}
|
nextLabel={<ArrowRightSmallIcon />}
|
||||||
pageClassName={styles.pageItem}
|
pageClassName={styles.pageItem}
|
||||||
|
|||||||
+81
-93
@@ -1,8 +1,5 @@
|
|||||||
import { notify } from '@affine/component';
|
import { notify } from '@affine/component';
|
||||||
import type {
|
import type { InviteModalProps } from '@affine/component/member-components';
|
||||||
InviteModalProps,
|
|
||||||
PaginationProps,
|
|
||||||
} from '@affine/component/member-components';
|
|
||||||
import {
|
import {
|
||||||
InviteModal,
|
InviteModal,
|
||||||
MemberLimitModal,
|
MemberLimitModal,
|
||||||
@@ -17,15 +14,16 @@ import { Tooltip } from '@affine/component/ui/tooltip';
|
|||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
import { useInviteMember } from '@affine/core/hooks/affine/use-invite-member';
|
import { useInviteMember } from '@affine/core/hooks/affine/use-invite-member';
|
||||||
import { useMemberCount } from '@affine/core/hooks/affine/use-member-count';
|
|
||||||
import type { Member } from '@affine/core/hooks/affine/use-members';
|
|
||||||
import { useMembers } from '@affine/core/hooks/affine/use-members';
|
|
||||||
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import {
|
||||||
|
type Member,
|
||||||
|
WorkspaceMembersService,
|
||||||
|
WorkspacePermissionService,
|
||||||
|
} from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Permission } from '@affine/graphql';
|
import { Permission, UserFriendlyError } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { MoreVerticalIcon } from '@blocksuite/icons/rc';
|
import { MoreVerticalIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
@@ -34,18 +32,11 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import {
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
Suspense,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type AuthAccountInfo,
|
type AuthAccountInfo,
|
||||||
@@ -55,7 +46,6 @@ import {
|
|||||||
} from '../../../../../modules/cloud';
|
} from '../../../../../modules/cloud';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
|
||||||
const COUNT_PER_PAGE = 8;
|
|
||||||
type OnRevoke = (memberId: string) => void;
|
type OnRevoke = (memberId: string) => void;
|
||||||
const MembersPanelLocal = () => {
|
const MembersPanelLocal = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -76,7 +66,6 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
serverConfig.features$.map(f => f?.payment)
|
serverConfig.features$.map(f => f?.payment)
|
||||||
);
|
);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const memberCount = useMemberCount(workspace.id);
|
|
||||||
|
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
@@ -84,42 +73,32 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
permissionService.permission.revalidate();
|
permissionService.permission.revalidate();
|
||||||
}, [permissionService]);
|
}, [permissionService]);
|
||||||
|
|
||||||
const checkMemberCountLimit = useCallback(
|
|
||||||
(memberCount: number, memberLimit?: number) => {
|
|
||||||
if (memberLimit === undefined) return false;
|
|
||||||
return memberCount >= memberLimit;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
workspaceQuotaService.quota.revalidate();
|
workspaceQuotaService.quota.revalidate();
|
||||||
}, [workspaceQuotaService]);
|
}, [workspaceQuotaService]);
|
||||||
|
const isLoading = useLiveData(workspaceQuotaService.quota.isLoading$);
|
||||||
|
const error = useLiveData(workspaceQuotaService.quota.error$);
|
||||||
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
const plan = useLiveData(
|
const plan = useLiveData(
|
||||||
subscriptionService.subscription.pro$.map(s => s?.plan)
|
subscriptionService.subscription.pro$.map(s => s?.plan)
|
||||||
);
|
);
|
||||||
const isLimited = workspaceQuota
|
const isLimited =
|
||||||
? checkMemberCountLimit(memberCount, workspaceQuota.memberLimit)
|
workspaceQuota && workspaceQuota.memberLimit
|
||||||
: null;
|
? workspaceQuota.memberCount >= workspaceQuota.memberLimit
|
||||||
|
: null;
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { invite, isMutating } = useInviteMember(workspace.id);
|
const { invite, isMutating } = useInviteMember(workspace.id);
|
||||||
const revokeMemberPermission = useRevokeMemberPermission(workspace.id);
|
const revokeMemberPermission = useRevokeMemberPermission(workspace.id);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [memberSkip, setMemberSkip] = useState(0);
|
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
const openModal = useCallback(() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPageChange = useCallback<PaginationProps['onPageChange']>(offset => {
|
|
||||||
setMemberSkip(offset);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
|
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
|
||||||
async ({ email, permission }) => {
|
async ({ email, permission }) => {
|
||||||
const success = await invite(
|
const success = await invite(
|
||||||
@@ -151,20 +130,6 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
});
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (
|
|
||||||
memberCount > COUNT_PER_PAGE &&
|
|
||||||
listContainerRef.current &&
|
|
||||||
memberListHeight === null
|
|
||||||
) {
|
|
||||||
const rect = listContainerRef.current.getBoundingClientRect();
|
|
||||||
setMemberListHeight(rect.height);
|
|
||||||
}
|
|
||||||
}, [listContainerRef, memberCount, memberListHeight]);
|
|
||||||
|
|
||||||
const onRevoke = useCallback<OnRevoke>(
|
const onRevoke = useCallback<OnRevoke>(
|
||||||
async memberId => {
|
async memberId => {
|
||||||
const res = await revokeMemberPermission(memberId);
|
const res = await revokeMemberPermission(memberId);
|
||||||
@@ -195,14 +160,23 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
}, [handleUpgradeConfirm, hasPaymentFeature, t, workspaceQuota]);
|
}, [handleUpgradeConfirm, hasPaymentFeature, t, workspaceQuota]);
|
||||||
|
|
||||||
if (workspaceQuota === null) {
|
if (workspaceQuota === null) {
|
||||||
// TODO(@eyhn): loading ui
|
if (isLoading) {
|
||||||
return null;
|
return <MembersPanelFallback />;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<span style={{ color: cssVar('errorColor') }}>
|
||||||
|
{UserFriendlyError.fromAnyError(error).message}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return; // never reach here
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={`${t['Members']()} (${memberCount}/${workspaceQuota.humanReadable.memberLimit})`}
|
name={`${t['Members']()} (${workspaceQuota.memberCount}/${workspaceQuota.humanReadable.memberLimit})`}
|
||||||
desc={desc}
|
desc={desc}
|
||||||
spreadCol={!!isOwner}
|
spreadCol={!!isOwner}
|
||||||
>
|
>
|
||||||
@@ -230,27 +204,8 @@ export const CloudWorkspaceMembersPanel = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
|
||||||
<div
|
<div className={style.membersPanel}>
|
||||||
className={style.membersPanel}
|
<MemberList isOwner={!!isOwner} onRevoke={onRevoke} />
|
||||||
ref={listContainerRef}
|
|
||||||
style={memberListHeight ? { height: memberListHeight } : {}}
|
|
||||||
>
|
|
||||||
<Suspense fallback={<MemberListFallback memberCount={memberCount} />}>
|
|
||||||
<MemberList
|
|
||||||
workspaceId={workspace.id}
|
|
||||||
isOwner={!!isOwner}
|
|
||||||
skip={memberSkip}
|
|
||||||
onRevoke={onRevoke}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{memberCount > COUNT_PER_PAGE && (
|
|
||||||
<Pagination
|
|
||||||
totalCount={memberCount}
|
|
||||||
countPerPage={COUNT_PER_PAGE}
|
|
||||||
onPageChange={onPageChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -271,12 +226,12 @@ export const MembersPanelFallback = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MemberListFallback = ({ memberCount }: { memberCount: number }) => {
|
const MemberListFallback = ({ memberCount }: { memberCount?: number }) => {
|
||||||
// prevent page jitter
|
// prevent page jitter
|
||||||
const height = useMemo(() => {
|
const height = useMemo(() => {
|
||||||
if (memberCount > COUNT_PER_PAGE) {
|
if (memberCount) {
|
||||||
// height and margin-bottom
|
// height and margin-bottom
|
||||||
return COUNT_PER_PAGE * 58 + (COUNT_PER_PAGE - 1) * 6;
|
return memberCount * 58 + (memberCount - 1) * 6;
|
||||||
}
|
}
|
||||||
return 'auto';
|
return 'auto';
|
||||||
}, [memberCount]);
|
}, [memberCount]);
|
||||||
@@ -296,31 +251,66 @@ const MemberListFallback = ({ memberCount }: { memberCount: number }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MemberList = ({
|
const MemberList = ({
|
||||||
workspaceId,
|
|
||||||
isOwner,
|
isOwner,
|
||||||
skip,
|
|
||||||
onRevoke,
|
onRevoke,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
skip: number;
|
|
||||||
onRevoke: OnRevoke;
|
onRevoke: OnRevoke;
|
||||||
}) => {
|
}) => {
|
||||||
const members = useMembers(workspaceId, skip, COUNT_PER_PAGE);
|
const membersService = useService(WorkspaceMembersService);
|
||||||
|
const memberCount = useLiveData(membersService.members.memberCount$);
|
||||||
|
const pageNum = useLiveData(membersService.members.pageNum$);
|
||||||
|
const isLoading = useLiveData(membersService.members.isLoading$);
|
||||||
|
const pageMembers = useLiveData(membersService.members.pageMembers$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
membersService.members.revalidate();
|
||||||
|
}, [membersService]);
|
||||||
|
|
||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
const account = useEnsureLiveData(session.account$);
|
const account = useEnsureLiveData(session.account$);
|
||||||
|
|
||||||
|
const handlePageChange = useCallback(
|
||||||
|
(_: number, pageNum: number) => {
|
||||||
|
membersService.members.setPageNum(pageNum);
|
||||||
|
membersService.members.revalidate();
|
||||||
|
},
|
||||||
|
[membersService]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.memberList}>
|
<div className={style.memberList}>
|
||||||
{members.map(member => (
|
{isLoading && pageMembers === undefined ? (
|
||||||
<MemberItem
|
<MemberListFallback
|
||||||
currentAccount={account}
|
memberCount={
|
||||||
key={member.id}
|
memberCount
|
||||||
member={member}
|
? Math.max(
|
||||||
isOwner={isOwner}
|
memberCount - pageNum * membersService.members.PAGE_SIZE,
|
||||||
onRevoke={onRevoke}
|
1
|
||||||
|
)
|
||||||
|
: 1
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
) : (
|
||||||
|
pageMembers?.map(member => (
|
||||||
|
<MemberItem
|
||||||
|
currentAccount={account}
|
||||||
|
key={member.id}
|
||||||
|
member={member}
|
||||||
|
isOwner={isOwner}
|
||||||
|
onRevoke={onRevoke}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{memberCount !== undefined &&
|
||||||
|
memberCount > membersService.members.PAGE_SIZE && (
|
||||||
|
<Pagination
|
||||||
|
totalCount={memberCount}
|
||||||
|
countPerPage={membersService.members.PAGE_SIZE}
|
||||||
|
pageNum={pageNum}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -409,9 +399,7 @@ export const MembersPanel = (): ReactElement | null => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AffineErrorBoundary>
|
<AffineErrorBoundary>
|
||||||
<Suspense fallback={<MembersPanelFallback />}>
|
<CloudWorkspaceMembersPanel />
|
||||||
<CloudWorkspaceMembersPanel />
|
|
||||||
</Suspense>
|
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { getMemberCountByWorkspaceIdQuery } from '@affine/graphql';
|
|
||||||
|
|
||||||
import { useQuery } from '../use-query';
|
|
||||||
|
|
||||||
export function useMemberCount(workspaceId: string) {
|
|
||||||
const { data } = useQuery({
|
|
||||||
query: getMemberCountByWorkspaceIdQuery,
|
|
||||||
variables: {
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.workspace.memberCount;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql';
|
|
||||||
import { getMembersByWorkspaceIdQuery, Permission } from '@affine/graphql';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { useQuery } from '../use-query';
|
|
||||||
|
|
||||||
export function calculateWeight(member: Member) {
|
|
||||||
const permissionWeight = {
|
|
||||||
[Permission.Owner]: 4,
|
|
||||||
[Permission.Admin]: 3,
|
|
||||||
[Permission.Write]: 2,
|
|
||||||
[Permission.Read]: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const factors = [
|
|
||||||
Number(member.permission === Permission.Owner), // Owner weight is the highest
|
|
||||||
Number(!member.accepted), // Unaccepted members are before accepted members
|
|
||||||
permissionWeight[member.permission] || 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
return factors.reduce((ret, factor, index, arr) => {
|
|
||||||
return ret + factor * Math.pow(10, arr.length - 1 - index);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Member = Omit<
|
|
||||||
GetMembersByWorkspaceIdQuery['workspace']['members'][number],
|
|
||||||
'__typename'
|
|
||||||
>;
|
|
||||||
export function useMembers(
|
|
||||||
workspaceId: string,
|
|
||||||
skip: number,
|
|
||||||
take: number = 8
|
|
||||||
) {
|
|
||||||
const { data } = useQuery({
|
|
||||||
query: getMembersByWorkspaceIdQuery,
|
|
||||||
variables: {
|
|
||||||
workspaceId,
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const members = data.workspace.members;
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
// sort members by weight
|
|
||||||
return members.sort((a, b) => {
|
|
||||||
const weightDifference = calculateWeight(b) - calculateWeight(a);
|
|
||||||
if (weightDifference !== 0) {
|
|
||||||
return weightDifference;
|
|
||||||
}
|
|
||||||
// if weight is the same, sort by name
|
|
||||||
if (a.name === null) return 1;
|
|
||||||
if (b.name === null) return -1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
}, [members]);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
type QueryOptions,
|
type QueryOptions,
|
||||||
type QueryResponse,
|
type QueryResponse,
|
||||||
UserFriendlyError,
|
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { fromPromise, Service } from '@toeverything/infra';
|
import { fromPromise, Service } from '@toeverything/infra';
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
@@ -39,13 +38,11 @@ export class GraphQLService extends Service {
|
|||||||
try {
|
try {
|
||||||
return await this.rawGql(options);
|
return await this.rawGql(options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const standardError = UserFriendlyError.fromAnyError(err);
|
if (err instanceof BackendError && err.status === 403) {
|
||||||
|
|
||||||
if (standardError.status === 403) {
|
|
||||||
this.framework.get(AuthService).session.revalidate();
|
this.framework.get(AuthService).session.revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BackendError(standardError);
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql';
|
||||||
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { distinctUntilChanged, EMPTY, map, mergeMap, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../../cloud';
|
||||||
|
import type { WorkspaceMembersStore } from '../stores/members';
|
||||||
|
|
||||||
|
export type Member =
|
||||||
|
GetMembersByWorkspaceIdQuery['workspace']['members'][number];
|
||||||
|
|
||||||
|
export class WorkspaceMembers extends Entity {
|
||||||
|
constructor(
|
||||||
|
private readonly store: WorkspaceMembersStore,
|
||||||
|
private readonly workspaceService: WorkspaceService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNum$ = new LiveData(0);
|
||||||
|
memberCount$ = new LiveData<number | undefined>(undefined);
|
||||||
|
pageMembers$ = new LiveData<Member[] | undefined>(undefined);
|
||||||
|
|
||||||
|
isLoading$ = new LiveData(false);
|
||||||
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
|
readonly PAGE_SIZE = 8;
|
||||||
|
|
||||||
|
readonly revalidate = effect(
|
||||||
|
map(() => this.pageNum$.value),
|
||||||
|
distinctUntilChanged<number>(),
|
||||||
|
switchMap(pageNum => {
|
||||||
|
return fromPromise(async signal => {
|
||||||
|
return this.store.fetchMembers(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
pageNum * this.PAGE_SIZE,
|
||||||
|
this.PAGE_SIZE,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
}).pipe(
|
||||||
|
mergeMap(data => {
|
||||||
|
this.memberCount$.setValue(data.memberCount);
|
||||||
|
this.pageMembers$.setValue(data.members);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$),
|
||||||
|
onStart(() => {
|
||||||
|
this.pageMembers$.setValue(undefined);
|
||||||
|
this.isLoading$.setValue(true);
|
||||||
|
}),
|
||||||
|
onComplete(() => this.isLoading$.setValue(false))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setPageNum(pageNum: number) {
|
||||||
|
this.pageNum$.setValue(pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this.revalidate.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export type { Member } from './entities/members';
|
||||||
|
export { WorkspaceMembersService } from './services/members';
|
||||||
export { WorkspacePermissionService } from './services/permission';
|
export { WorkspacePermissionService } from './services/permission';
|
||||||
|
|
||||||
import { GraphQLService } from '@affine/core/modules/cloud';
|
import { GraphQLService } from '@affine/core/modules/cloud';
|
||||||
@@ -8,8 +10,11 @@ import {
|
|||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceMembers } from './entities/members';
|
||||||
import { WorkspacePermission } from './entities/permission';
|
import { WorkspacePermission } from './entities/permission';
|
||||||
|
import { WorkspaceMembersService } from './services/members';
|
||||||
import { WorkspacePermissionService } from './services/permission';
|
import { WorkspacePermissionService } from './services/permission';
|
||||||
|
import { WorkspaceMembersStore } from './stores/members';
|
||||||
import { WorkspacePermissionStore } from './stores/permission';
|
import { WorkspacePermissionStore } from './stores/permission';
|
||||||
|
|
||||||
export function configurePermissionsModule(framework: Framework) {
|
export function configurePermissionsModule(framework: Framework) {
|
||||||
@@ -21,5 +26,8 @@ export function configurePermissionsModule(framework: Framework) {
|
|||||||
WorkspacePermissionStore,
|
WorkspacePermissionStore,
|
||||||
])
|
])
|
||||||
.store(WorkspacePermissionStore, [GraphQLService])
|
.store(WorkspacePermissionStore, [GraphQLService])
|
||||||
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore]);
|
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
||||||
|
.service(WorkspaceMembersService)
|
||||||
|
.store(WorkspaceMembersStore, [GraphQLService])
|
||||||
|
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceMembers } from '../entities/members';
|
||||||
|
|
||||||
|
export class WorkspaceMembersService extends Service {
|
||||||
|
members = this.framework.createEntity(WorkspaceMembers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
|
||||||
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { GraphQLService } from '../../cloud';
|
||||||
|
|
||||||
|
export class WorkspaceMembersStore extends Store {
|
||||||
|
constructor(private readonly graphqlService: GraphQLService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMembers(
|
||||||
|
workspaceId: string,
|
||||||
|
skip: number,
|
||||||
|
take: number,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
const data = await this.graphqlService.gql({
|
||||||
|
query: getMembersByWorkspaceIdQuery,
|
||||||
|
variables: {
|
||||||
|
workspaceId,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1297,6 +1297,7 @@ query workspaceQuota($id: String!) {
|
|||||||
storageQuota
|
storageQuota
|
||||||
historyPeriod
|
historyPeriod
|
||||||
memberLimit
|
memberLimit
|
||||||
|
memberCount
|
||||||
humanReadable {
|
humanReadable {
|
||||||
name
|
name
|
||||||
blobLimit
|
blobLimit
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ query workspaceQuota($id: String!) {
|
|||||||
storageQuota
|
storageQuota
|
||||||
historyPeriod
|
historyPeriod
|
||||||
memberLimit
|
memberLimit
|
||||||
|
memberCount
|
||||||
humanReadable {
|
humanReadable {
|
||||||
name
|
name
|
||||||
blobLimit
|
blobLimit
|
||||||
|
|||||||
@@ -901,6 +901,7 @@ export interface QuotaQueryType {
|
|||||||
copilotActionLimit: Maybe<Scalars['SafeInt']['output']>;
|
copilotActionLimit: Maybe<Scalars['SafeInt']['output']>;
|
||||||
historyPeriod: Scalars['SafeInt']['output'];
|
historyPeriod: Scalars['SafeInt']['output'];
|
||||||
humanReadable: HumanReadableQuotaType;
|
humanReadable: HumanReadableQuotaType;
|
||||||
|
memberCount: Scalars['SafeInt']['output'];
|
||||||
memberLimit: Scalars['SafeInt']['output'];
|
memberLimit: Scalars['SafeInt']['output'];
|
||||||
name: Scalars['String']['output'];
|
name: Scalars['String']['output'];
|
||||||
storageQuota: Scalars['SafeInt']['output'];
|
storageQuota: Scalars['SafeInt']['output'];
|
||||||
@@ -2423,6 +2424,7 @@ export type WorkspaceQuotaQuery = {
|
|||||||
storageQuota: number;
|
storageQuota: number;
|
||||||
historyPeriod: number;
|
historyPeriod: number;
|
||||||
memberLimit: number;
|
memberLimit: number;
|
||||||
|
memberCount: number;
|
||||||
usedSize: number;
|
usedSize: number;
|
||||||
humanReadable: {
|
humanReadable: {
|
||||||
__typename?: 'HumanReadableQuotaType';
|
__typename?: 'HumanReadableQuotaType';
|
||||||
|
|||||||
Reference in New Issue
Block a user