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:
@@ -187,6 +187,7 @@ e2e('should allocate seats', async t => {
|
|||||||
source: 'Link',
|
source: 'Link',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invitationCount = app.queue.count('notification.sendInvitation');
|
||||||
await app.eventBus.emitAsync('workspace.members.allocateSeats', {
|
await app.eventBus.emitAsync('workspace.members.allocateSeats', {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
@@ -206,7 +207,7 @@ e2e('should allocate seats', async t => {
|
|||||||
WorkspaceMemberStatus.Accepted
|
WorkspaceMemberStatus.Accepted
|
||||||
);
|
);
|
||||||
|
|
||||||
t.is(app.queue.count('notification.sendInvitation'), 1);
|
t.is(app.queue.count('notification.sendInvitation') - invitationCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
e2e('should set all rests to NeedMoreSeat', async t => {
|
e2e('should set all rests to NeedMoreSeat', async t => {
|
||||||
|
|||||||
@@ -1552,6 +1552,48 @@ test('should be able to create team subscription', async t => {
|
|||||||
t.is(subInDB?.stripeSubscriptionId, sub.id);
|
t.is(subInDB?.stripeSubscriptionId, sub.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should replace old team subscription row when stripe creates a new subscription', async t => {
|
||||||
|
const { service, db } = t.context;
|
||||||
|
|
||||||
|
const old = await db.subscription.create({
|
||||||
|
data: {
|
||||||
|
targetId: 'ws_1',
|
||||||
|
stripeSubscriptionId: 'sub_old_team',
|
||||||
|
plan: SubscriptionPlan.Team,
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
status: SubscriptionStatus.Canceled,
|
||||||
|
start: new Date('2026-03-26T08:23:57.000Z'),
|
||||||
|
end: new Date('2027-03-26T08:23:57.000Z'),
|
||||||
|
quantity: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.saveStripeSubscription({
|
||||||
|
...teamSub,
|
||||||
|
id: 'sub_new_team',
|
||||||
|
status: SubscriptionStatus.Active,
|
||||||
|
items: {
|
||||||
|
...teamSub.items,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
...teamSub.items.data[0],
|
||||||
|
quantity: 11,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscriptions = await db.subscription.findMany({
|
||||||
|
where: { targetId: 'ws_1', plan: SubscriptionPlan.Team },
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(subscriptions.length, 1);
|
||||||
|
t.is(subscriptions[0].id, old.id);
|
||||||
|
t.is(subscriptions[0].stripeSubscriptionId, 'sub_new_team');
|
||||||
|
t.is(subscriptions[0].status, SubscriptionStatus.Active);
|
||||||
|
t.is(subscriptions[0].quantity, 11);
|
||||||
|
});
|
||||||
|
|
||||||
test('should be able to update team subscription', async t => {
|
test('should be able to update team subscription', async t => {
|
||||||
const { service, db, event } = t.context;
|
const { service, db, event } = t.context;
|
||||||
|
|
||||||
@@ -1586,6 +1628,77 @@ test('should be able to update team subscription', async t => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should persist mutable team subscription fields on same stripe subscription update', async t => {
|
||||||
|
const { service, db } = t.context;
|
||||||
|
|
||||||
|
await service.saveStripeSubscription(teamSub);
|
||||||
|
|
||||||
|
await service.saveStripeSubscription({
|
||||||
|
...teamSub,
|
||||||
|
current_period_start: 1780000000,
|
||||||
|
current_period_end: 1811536000,
|
||||||
|
trial_start: 1780000000,
|
||||||
|
trial_end: 1780604800,
|
||||||
|
items: {
|
||||||
|
...teamSub.items,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
...teamSub.items.data[0],
|
||||||
|
quantity: 9,
|
||||||
|
price: {
|
||||||
|
...PRICES[TEAM_YEARLY],
|
||||||
|
lookup_key: TEAM_YEARLY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const subInDB = await db.subscription.findFirst({
|
||||||
|
where: { targetId: 'ws_1' },
|
||||||
|
});
|
||||||
|
const entitlement = await db.entitlement.findFirst({
|
||||||
|
where: {
|
||||||
|
source: 'cloud_subscription',
|
||||||
|
subjectId: teamSub.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const providerFact = await db.providerSubscription.findUnique({
|
||||||
|
where: {
|
||||||
|
provider_externalSubscriptionId: {
|
||||||
|
provider: 'stripe',
|
||||||
|
externalSubscriptionId: teamSub.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.like(subInDB, {
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
quantity: 9,
|
||||||
|
start: new Date(1780000000 * 1000),
|
||||||
|
end: new Date(1811536000 * 1000),
|
||||||
|
trialStart: new Date(1780000000 * 1000),
|
||||||
|
trialEnd: new Date(1780604800 * 1000),
|
||||||
|
});
|
||||||
|
t.like(entitlement, {
|
||||||
|
plan: 'team',
|
||||||
|
quantity: 9,
|
||||||
|
startsAt: new Date(1780000000 * 1000),
|
||||||
|
expiresAt: new Date(1811536000 * 1000),
|
||||||
|
});
|
||||||
|
t.like(providerFact, {
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
externalPriceId: TEAM_YEARLY,
|
||||||
|
currency: 'usd',
|
||||||
|
amount: 14400,
|
||||||
|
quantity: 9,
|
||||||
|
periodStart: new Date(1780000000 * 1000),
|
||||||
|
periodEnd: new Date(1811536000 * 1000),
|
||||||
|
trialStart: new Date(1780000000 * 1000),
|
||||||
|
trialEnd: new Date(1780604800 * 1000),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should suspend on dispute and restore when dispute won', async t => {
|
test('should suspend on dispute and restore when dispute won', async t => {
|
||||||
const { service, db, stripe, event } = t.context;
|
const { service, db, stripe, event } = t.context;
|
||||||
|
|
||||||
|
|||||||
@@ -76,15 +76,15 @@ test('checker reports missing legacy projection and stale state', async t => {
|
|||||||
const user = await t.context.models.user.create({
|
const user = await t.context.models.user.create({
|
||||||
email: `${randomUUID()}@affine.pro`,
|
email: `${randomUUID()}@affine.pro`,
|
||||||
});
|
});
|
||||||
await t.context.entitlement.upsertFromCloudSubscription({
|
await t.context.entitlement.upsertFromCloudSubscription(
|
||||||
|
{
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
plan: 'pro',
|
plan: 'pro',
|
||||||
recurring: SubscriptionRecurring.Monthly,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
},
|
||||||
await t.context.db.subscription.delete({
|
{ emit: false }
|
||||||
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
|
);
|
||||||
});
|
|
||||||
await t.context.db.effectiveUserQuotaState.update({
|
await t.context.db.effectiveUserQuotaState.update({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -48,18 +48,24 @@ test('projects user entitlement to legacy user features and subscriptions', asyn
|
|||||||
email: `${randomUUID()}@affine.pro`,
|
email: `${randomUUID()}@affine.pro`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await t.context.entitlement.upsertFromCloudSubscription({
|
await t.context.entitlement.upsertFromCloudSubscription(
|
||||||
|
{
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
plan: SubscriptionPlan.Pro,
|
plan: SubscriptionPlan.Pro,
|
||||||
recurring: SubscriptionRecurring.Yearly,
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
},
|
||||||
await t.context.entitlement.upsertFromCloudSubscription({
|
{ emit: false }
|
||||||
|
);
|
||||||
|
await t.context.entitlement.upsertFromCloudSubscription(
|
||||||
|
{
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
plan: SubscriptionPlan.AI,
|
plan: SubscriptionPlan.AI,
|
||||||
recurring: SubscriptionRecurring.Monthly,
|
recurring: SubscriptionRecurring.Monthly,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
},
|
||||||
|
{ emit: false }
|
||||||
|
);
|
||||||
await t.context.projection.onEntitlementChanged({
|
await t.context.projection.onEntitlementChanged({
|
||||||
targetType: 'user',
|
targetType: 'user',
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
|
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
|
||||||
import { omit, pick } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { SubscriptionPlanNotFound, URLHelper } from '../../../base';
|
import { SubscriptionPlanNotFound, URLHelper } from '../../../base';
|
||||||
@@ -163,13 +163,14 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
|
|||||||
where: {
|
where: {
|
||||||
stripeSubscriptionId: stripeSubscription.id,
|
stripeSubscriptionId: stripeSubscription.id,
|
||||||
},
|
},
|
||||||
data: pick(subscriptionData, [
|
data: {
|
||||||
'status',
|
...omit(subscriptionData, ['provider', 'iapStore']),
|
||||||
'stripeScheduleId',
|
provider: Provider.stripe,
|
||||||
'nextBillAt',
|
iapStore: null,
|
||||||
'canceledAt',
|
rcEntitlement: null,
|
||||||
'end',
|
rcProductId: null,
|
||||||
]),
|
rcExternalRef: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await this.upsertStripeProviderSubscription(
|
await this.upsertStripeProviderSubscription(
|
||||||
saved.targetId,
|
saved.targetId,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
Provider,
|
Provider,
|
||||||
UserStripeCustomer,
|
UserStripeCustomer,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { omit, pick } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -272,13 +272,14 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
|||||||
const saved = existingByStripeId
|
const saved = existingByStripeId
|
||||||
? await this.db.subscription.update({
|
? await this.db.subscription.update({
|
||||||
where: { id: existingByStripeId.id },
|
where: { id: existingByStripeId.id },
|
||||||
data: pick(subscriptionData, [
|
data: {
|
||||||
'status',
|
...omit(subscriptionData, ['provider', 'iapStore']),
|
||||||
'stripeScheduleId',
|
provider: Provider.stripe,
|
||||||
'nextBillAt',
|
iapStore: null,
|
||||||
'canceledAt',
|
rcEntitlement: null,
|
||||||
'end',
|
rcProductId: null,
|
||||||
]),
|
rcExternalRef: null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
: await this.db.subscription.upsert({
|
: await this.db.subscription.upsert({
|
||||||
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
|
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
|
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
|
||||||
import { omit, pick } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -163,22 +163,38 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const saved = await this.db.subscription.upsert({
|
const existingByStripeId = await this.db.subscription.findUnique({
|
||||||
|
where: { stripeSubscriptionId: stripeSubscription.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const saved = existingByStripeId
|
||||||
|
? await this.db.subscription.update({
|
||||||
|
where: { id: existingByStripeId.id },
|
||||||
|
data: {
|
||||||
|
...omit(subscriptionData, ['provider', 'iapStore']),
|
||||||
|
provider: Provider.stripe,
|
||||||
|
iapStore: null,
|
||||||
|
rcEntitlement: null,
|
||||||
|
rcProductId: null,
|
||||||
|
rcExternalRef: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: await this.db.subscription.upsert({
|
||||||
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
|
// TODO(stable-upgrade): remove legacy subscriptions dual-write after stable supports provider facts.
|
||||||
// TODO(stable-upgrade): remove reliance on target_id_plan unique slot after contract cleanup.
|
// TODO(stable-upgrade): remove reliance on target_id_plan unique slot after contract cleanup.
|
||||||
where: {
|
where: {
|
||||||
provider: Provider.stripe,
|
targetId_plan: {
|
||||||
stripeSubscriptionId: stripeSubscription.id,
|
targetId: workspaceId,
|
||||||
|
plan: lookupKey.plan,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
...pick(subscriptionData, [
|
...omit(subscriptionData, ['provider', 'iapStore']),
|
||||||
'status',
|
provider: Provider.stripe,
|
||||||
'stripeScheduleId',
|
iapStore: null,
|
||||||
'nextBillAt',
|
rcEntitlement: null,
|
||||||
'canceledAt',
|
rcProductId: null,
|
||||||
'quantity',
|
rcExternalRef: null,
|
||||||
'end',
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
targetId: workspaceId,
|
targetId: workspaceId,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Loading, notify, useConfirmModal } from '@affine/component';
|
import { Button, notify, useConfirmModal } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
InviteTeamMemberModal,
|
InviteTeamMemberModal,
|
||||||
type InviteTeamMemberModalProps,
|
type InviteTeamMemberModalProps,
|
||||||
@@ -31,7 +31,7 @@ import { nanoid } from 'nanoid';
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { SettingState } from '../../types';
|
import type { SettingState } from '../../types';
|
||||||
import { MemberList } from './member-list';
|
import { MemberList, MemberListError, MemberListFallback } from './member-list';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const parseCSV = async (blob: Blob): Promise<string[]> => {
|
const parseCSV = async (blob: Blob): Promise<string[]> => {
|
||||||
@@ -290,13 +290,7 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <MembersPanelFallback />;
|
return <MembersPanelFallback />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return <MemberListError error={error} memberCount={1} />;
|
||||||
<span className={styles.errorStyle}>
|
|
||||||
{error
|
|
||||||
? UserFriendlyError.fromAny(error).message
|
|
||||||
: 'Failed to load members'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 ImportCSV = ({ onImport }: { onImport: (file: File) => void }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export const MemberList = ({
|
|||||||
const handlePageChange = useCallback(
|
const handlePageChange = useCallback(
|
||||||
(_: number, pageNum: number) => {
|
(_: number, pageNum: number) => {
|
||||||
membersService.members.setPageNum(pageNum);
|
membersService.members.setPageNum(pageNum);
|
||||||
membersService.members.revalidate();
|
|
||||||
},
|
},
|
||||||
[membersService]
|
[membersService]
|
||||||
);
|
);
|
||||||
@@ -57,7 +56,7 @@ export const MemberList = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{pageMembers === undefined ? (
|
{pageMembers === undefined ? (
|
||||||
isLoading ? (
|
isLoading || !error ? (
|
||||||
<MemberListFallback
|
<MemberListFallback
|
||||||
memberCount={
|
memberCount={
|
||||||
memberCount
|
memberCount
|
||||||
@@ -70,11 +69,7 @@ export const MemberList = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.errorStyle}>
|
<MemberListError error={error} />
|
||||||
{error
|
|
||||||
? UserFriendlyError.fromAny(error).message
|
|
||||||
: 'Failed to load members'}
|
|
||||||
</span>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
pageMembers?.map(member => (
|
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 = ({
|
export const MemberListFallback = ({
|
||||||
memberCount,
|
memberCount,
|
||||||
}: {
|
}: {
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
}) => {
|
}) => {
|
||||||
// prevent page jitter
|
// prevent page jitter
|
||||||
const height = useMemo(() => {
|
const height = useMemo(
|
||||||
if (memberCount) {
|
() => getMembersFallbackHeight(memberCount),
|
||||||
// height and margin-bottom
|
[memberCount]
|
||||||
return memberCount * 58 + (memberCount - 1) * 6;
|
);
|
||||||
}
|
|
||||||
return 'auto';
|
|
||||||
}, [memberCount]);
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -305,3 +305,31 @@ export const MemberListFallback = ({
|
|||||||
</div>
|
</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({
|
export const membersFallback = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'flexStart',
|
alignItems: 'center',
|
||||||
color: cssVarV2('text/secondary'),
|
color: cssVarV2('text/secondary'),
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
padding: '8px',
|
padding: '16px 8px',
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
|
textAlign: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const memberListItem = style({
|
export const memberListItem = style({
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ export class WorkspaceMembers extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async requestMembers(signal: AbortSignal) {
|
private async requestMembers(signal: AbortSignal) {
|
||||||
this.pageMembers$.setValue(undefined);
|
|
||||||
this.isLoading$.setValue(true);
|
this.isLoading$.setValue(true);
|
||||||
|
this.error$.setValue(null);
|
||||||
|
this.pageMembers$.setValue(undefined);
|
||||||
try {
|
try {
|
||||||
const pageNum = this.pageNum$.value;
|
const pageNum = this.pageNum$.value;
|
||||||
return await this.store.fetchMembers(
|
return await this.store.fetchMembers(
|
||||||
|
|||||||
Reference in New Issue
Block a user