fix(server): member loading (#15156)

#### PR Dependency Tree


* **PR #15156** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed Stripe subscription syncing for Team plans to update the correct
existing local subscription (avoiding duplicates) while refreshing
quantity and billing/trial period details.
* **UI/UX Improvements**
* Improved workspace member list loading/error states with shared UI
components and steadier pagination behavior.
  * Refined fallback styling for cleaner, more stable layout.
* **Tests**
* Expanded subscription and projection coverage and adjusted
seat-allocation/e2e assertions to be more robust.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-26 21:29:15 +08:00
committed by GitHub
parent 8e036a2f38
commit 4a7c931eca
11 changed files with 248 additions and 110 deletions
@@ -1,4 +1,4 @@
import { Button, Loading, notify, useConfirmModal } from '@affine/component';
import { Button, notify, useConfirmModal } from '@affine/component';
import {
InviteTeamMemberModal,
type InviteTeamMemberModalProps,
@@ -31,7 +31,7 @@ import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { SettingState } from '../../types';
import { MemberList } from './member-list';
import { MemberList, MemberListError, MemberListFallback } from './member-list';
import * as styles from './styles.css';
const parseCSV = async (blob: Blob): Promise<string[]> => {
@@ -290,13 +290,7 @@ export const CloudWorkspaceMembersPanel = ({
if (isLoading) {
return <MembersPanelFallback />;
} else {
return (
<span className={styles.errorStyle}>
{error
? UserFriendlyError.fromAny(error).message
: 'Failed to load members'}
</span>
);
return <MemberListError error={error} memberCount={1} />;
}
}
@@ -358,30 +352,6 @@ export const MembersPanelFallback = () => {
);
};
const MemberListFallback = ({ memberCount }: { memberCount?: number }) => {
// prevent page jitter
const height = useMemo(() => {
if (memberCount) {
// height and margin-bottom
return memberCount * 58 + (memberCount - 1) * 6;
}
return 'auto';
}, [memberCount]);
const t = useI18n();
return (
<div
style={{
height,
}}
className={styles.membersFallback}
>
<Loading size={20} />
<span>{t['com.affine.settings.member.loading']()}</span>
</div>
);
};
const ImportCSV = ({ onImport }: { onImport: (file: File) => void }) => {
const t = useI18n();
@@ -45,7 +45,6 @@ export const MemberList = ({
const handlePageChange = useCallback(
(_: number, pageNum: number) => {
membersService.members.setPageNum(pageNum);
membersService.members.revalidate();
},
[membersService]
);
@@ -57,7 +56,7 @@ export const MemberList = ({
return (
<div>
{pageMembers === undefined ? (
isLoading ? (
isLoading || !error ? (
<MemberListFallback
memberCount={
memberCount
@@ -70,11 +69,7 @@ export const MemberList = ({
}
/>
) : (
<span className={styles.errorStyle}>
{error
? UserFriendlyError.fromAny(error).message
: 'Failed to load members'}
</span>
<MemberListError error={error} />
)
) : (
pageMembers?.map(member => (
@@ -278,19 +273,24 @@ const getMemberStatus = (member: Member): I18nString => {
}
};
const getMembersFallbackHeight = (memberCount?: number) => {
if (memberCount) {
// height and margin-bottom
return memberCount * 58 + (memberCount - 1) * 6;
}
return 'auto';
};
export const MemberListFallback = ({
memberCount,
}: {
memberCount?: number;
}) => {
// prevent page jitter
const height = useMemo(() => {
if (memberCount) {
// height and margin-bottom
return memberCount * 58 + (memberCount - 1) * 6;
}
return 'auto';
}, [memberCount]);
const height = useMemo(
() => getMembersFallbackHeight(memberCount),
[memberCount]
);
const t = useI18n();
return (
@@ -305,3 +305,31 @@ export const MemberListFallback = ({
</div>
);
};
export const MemberListError = ({
error,
memberCount,
}: {
error?: unknown;
memberCount?: number;
}) => {
const height = useMemo(
() => getMembersFallbackHeight(memberCount),
[memberCount]
);
return (
<div
style={{
height,
}}
className={styles.membersFallback}
>
<span className={styles.errorStyle}>
{error
? UserFriendlyError.fromAny(error).message
: 'Failed to load members'}
</span>
</div>
);
};
@@ -55,11 +55,12 @@ export const errorStyle = style({
export const membersFallback = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'flexStart',
alignItems: 'center',
color: cssVarV2('text/secondary'),
gap: '4px',
padding: '8px',
padding: '16px 8px',
fontSize: cssVar('fontXs'),
textAlign: 'center',
});
export const memberListItem = style({
@@ -66,8 +66,9 @@ export class WorkspaceMembers extends Entity {
}
private async requestMembers(signal: AbortSignal) {
this.pageMembers$.setValue(undefined);
this.isLoading$.setValue(true);
this.error$.setValue(null);
this.pageMembers$.setValue(undefined);
try {
const pageNum = this.pageNum$.value;
return await this.store.fetchMembers(