Skip to content

Commit

Permalink
fix(core): page status
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Mar 1, 2024
1 parent 4f21f97 commit fa146ef
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 27 deletions.
30 changes: 30 additions & 0 deletions packages/backend/server/src/core/users/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@nestjs/graphql';
import { PrismaClient, type User } from '@prisma/client';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { isNil, omitBy } from 'lodash-es';

import {
CloudThrottlerGuard,
Expand All @@ -19,13 +20,15 @@ import {
} from '../../fundamentals';
import { CurrentUser } from '../auth/current-user';
import { Public } from '../auth/guard';
import { sessionUser } from '../auth/session';
import { FeatureManagementService } from '../features';
import { QuotaService } from '../quota';
import { AvatarStorage } from '../storage';
import { UsersService } from './service';
import {
DeleteAccount,
RemoveAvatar,
UpdateUserInput,
UserOrLimitedUser,
UserQuotaType,
UserType,
Expand Down Expand Up @@ -137,6 +140,33 @@ export class UserResolver {
});
}

@Throttle({
default: {
limit: 10,
ttl: 60,
},
})
@Mutation(() => UserType, {
name: 'updateProfile',
})
async updateUserProfile(
@CurrentUser() user: CurrentUser,
@Args('input', { type: () => UpdateUserInput }) input: UpdateUserInput
): Promise<UserType> {
input = omitBy(input, isNil);

if (Object.keys(input).length === 0) {
return user;
}

return sessionUser(
await this.prisma.user.update({
where: { id: user.id },
data: input,
})
);
}

@Throttle({
default: {
limit: 10,
Expand Down
14 changes: 13 additions & 1 deletion packages/backend/server/src/core/users/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createUnionType, Field, ID, ObjectType } from '@nestjs/graphql';
import {
createUnionType,
Field,
ID,
InputType,
ObjectType,
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars';

Expand Down Expand Up @@ -107,3 +113,9 @@ export class RemoveAvatar {
@Field()
success!: boolean;
}

@InputType()
export class UpdateUserInput implements Partial<User> {
@Field({ description: 'User name', nullable: true })
name?: string;
}
6 changes: 6 additions & 0 deletions packages/backend/server/src/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type Mutation {
sharePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "renamed to publicPage")
signIn(email: String!, password: String!): UserType!
signUp(email: String!, name: String!, password: String!): UserType!
updateProfile(input: UpdateUserInput!): UserType!
updateSubscriptionRecurring(idempotencyKey: String!, recurring: SubscriptionRecurring!): UserSubscription!

"""Update workspace"""
Expand Down Expand Up @@ -297,6 +298,11 @@ enum SubscriptionStatus {
Unpaid
}

input UpdateUserInput {
"""User name"""
name: String
}

input UpdateWorkspaceInput {
id: ID!

Expand Down
10 changes: 5 additions & 5 deletions packages/frontend/core/src/components/affine/awareness/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { useLiveData } from '@toeverything/infra/livedata';
import { Suspense, useEffect } from 'react';

import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { useSession } from '../../../hooks/affine/use-current-user';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';

const SyncAwarenessInnerLoggedIn = () => {
const currentUser = useCurrentUser();
const { user } = useSession();
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);

useEffect(() => {
if (currentUser && currentWorkspace) {
if (user && currentWorkspace) {
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
'user',
{
name: currentUser.name,
name: user.name,
// todo: add avatar?
}
);
Expand All @@ -30,7 +30,7 @@ const SyncAwarenessInnerLoggedIn = () => {
};
}
return;
}, [currentUser, currentWorkspace]);
}, [user, currentWorkspace]);

return null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
allBlobSizesQuery,
removeAvatarMutation,
SubscriptionPlan,
updateUserProfileMutation,
uploadAvatarMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
Expand Down Expand Up @@ -58,10 +59,10 @@ export const UserAvatar = () => {
async (file: File) => {
try {
const reducedFile = await validateAndReduceImage(file);
await avatarTrigger({
const data = await avatarTrigger({
avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger
});
user.update();
user.update({ avatarUrl: data.uploadAvatar.avatarUrl });
pushNotification({
title: 'Update user avatar success',
type: 'success',
Expand All @@ -81,7 +82,7 @@ export const UserAvatar = () => {
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await removeAvatarTrigger();
user.update();
user.update({ avatarUrl: null });
},
[removeAvatarTrigger, user]
);
Expand Down Expand Up @@ -113,14 +114,30 @@ export const AvatarAndName = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [input, setInput] = useState<string>(user.name);
const pushNotification = useSetAtom(pushNotificationAtom);

const { trigger: updateProfile } = useMutation({
mutation: updateUserProfileMutation,
});
const allowUpdate = !!input && input !== user.name;
const handleUpdateUserName = useCallback(() => {
const handleUpdateUserName = useAsyncCallback(async () => {
if (!allowUpdate) {
return;
}
user.update({ name: input });
}, [allowUpdate, input, user]);

try {
const data = await updateProfile({
input: { name: input },
});
user.update({ name: data.updateProfile.name });
} catch (e) {
pushNotification({
title: 'Failed to update user name.',
message: String(e),
type: 'error',
});
}
}, [allowUpdate, input, user, updateProfile, pushNotification]);

return (
<SettingRow
Expand Down
15 changes: 9 additions & 6 deletions packages/frontend/core/src/hooks/affine/use-current-user.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { DebugLogger } from '@affine/debug';
import { getBaseUrl } from '@affine/graphql';
import { useCallback, useMemo, useReducer } from 'react';
import { useMemo, useReducer } from 'react';
import useSWR from 'swr';

import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
import { useAsyncCallback } from '../affine-async-hooks';

const logger = new DebugLogger('auth');

Expand All @@ -16,7 +17,7 @@ interface User {
emailVerified: string | null;
}

interface Session {
export interface Session {
user?: User | null;
status: 'authenticated' | 'unauthenticated' | 'loading';
reload: () => Promise<void>;
Expand Down Expand Up @@ -54,7 +55,7 @@ export function useSession(): Session {
: data?.user
? 'authenticated'
: 'unauthenticated',
reload: () => {
reload: async () => {
return mutate().then(e => {
console.error(e);
});
Expand Down Expand Up @@ -122,14 +123,16 @@ export function useCurrentUser(): CheckedUser {
}
);

const update = useCallback(
(changes?: Partial<User>) => {
const update = useAsyncCallback(
async (changes?: Partial<User>) => {
dispatcher({
type: 'update',
payload: changes,
});

await session.reload();
},
[dispatcher]
[dispatcher, session]
);

return useMemo(
Expand Down
7 changes: 2 additions & 5 deletions packages/frontend/core/src/providers/session-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

import { useOnceSignedInEvents } from '../atoms/event';

const SessionDefence = (props: PropsWithChildren) => {
export const CloudSessionProvider = (props: PropsWithChildren) => {
const session = useSession();
const prevSession = useRef<ReturnType<typeof useSession>>();
const pushNotification = useSetAtom(pushNotificationAtom);
Expand Down Expand Up @@ -49,9 +49,6 @@ const SessionDefence = (props: PropsWithChildren) => {
prevSession.current = session;
}
}, [session, prevSession, pushNotification, refreshAfterSignedInEvents, t]);
return props.children;
};

export const CloudSessionProvider = ({ children }: PropsWithChildren) => {
return <SessionDefence>{children}</SessionDefence>;
return props.children;
};
2 changes: 1 addition & 1 deletion packages/frontend/core/src/utils/cloud-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const signOutCloud = async (redirectUri?: string) => {
CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY
).postMessage(1);

if (redirectUri) {
if (redirectUri && location.href !== redirectUri) {
setTimeout(() => {
location.href = redirectUri;
}, 0);
Expand Down
14 changes: 14 additions & 0 deletions packages/frontend/graphql/src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,20 @@ mutation updateSubscription($recurring: SubscriptionRecurring!, $idempotencyKey:
}`,
};

export const updateUserProfileMutation = {
id: 'updateUserProfileMutation' as const,
operationName: 'updateUserProfile',
definitionName: 'updateProfile',
containsFile: false,
query: `
mutation updateUserProfile($input: UpdateUserInput!) {
updateProfile(input: $input) {
id
name
}
}`,
};

export const uploadAvatarMutation = {
id: 'uploadAvatarMutation' as const,
operationName: 'uploadAvatar',
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/graphql/src/graphql/update-user-profile.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation updateUserProfile($input: UpdateUserInput!) {
updateProfile(input: $input) {
id
name
}
}
25 changes: 22 additions & 3 deletions packages/frontend/graphql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export enum SubscriptionStatus {
Unpaid = 'Unpaid',
}

export interface UpdateUserInput {
/** User name */
name: InputMaybe<Scalars['String']['input']>;
}

export interface UpdateWorkspaceInput {
id: Scalars['ID']['input'];
/** is Public workspace */
Expand Down Expand Up @@ -265,7 +270,7 @@ export type EarlyAccessUsersQuery = {
name: string;
email: string;
avatarUrl: string | null;
emailVerified: string;
emailVerified: boolean;
subscription: {
__typename?: 'UserSubscription';
plan: SubscriptionPlan;
Expand Down Expand Up @@ -295,7 +300,7 @@ export type GetCurrentUserQuery = {
id: string;
name: string;
email: string;
emailVerified: string;
emailVerified: boolean;
avatarUrl: string | null;
token: { __typename?: 'tokenType'; sessionToken: string | null };
} | null;
Expand Down Expand Up @@ -358,7 +363,7 @@ export type GetMembersByWorkspaceIdQuery = {
permission: Permission;
inviteId: string;
accepted: boolean;
emailVerified: string | null;
emailVerified: boolean | null;
}>;
};
};
Expand Down Expand Up @@ -733,6 +738,15 @@ export type UpdateSubscriptionMutation = {
};
};

export type UpdateUserProfileMutationVariables = Exact<{
input: UpdateUserInput;
}>;

export type UpdateUserProfileMutation = {
__typename?: 'Mutation';
updateProfile: { __typename?: 'UserType'; id: string; name: string };
};

export type UploadAvatarMutationVariables = Exact<{
avatar: Scalars['Upload']['input'];
}>;
Expand Down Expand Up @@ -1152,6 +1166,11 @@ export type Mutations =
variables: UpdateSubscriptionMutationVariables;
response: UpdateSubscriptionMutation;
}
| {
name: 'updateUserProfileMutation';
variables: UpdateUserProfileMutationVariables;
response: UpdateUserProfileMutation;
}
| {
name: 'uploadAvatarMutation';
variables: UploadAvatarMutationVariables;
Expand Down
1 change: 1 addition & 0 deletions tests/affine-cloud/e2e/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test.describe('login first', () => {
await page.getByTestId('workspace-modal-account-option').click();
await page.getByTestId('workspace-modal-sign-out-option').click();
await page.getByTestId('confirm-sign-out-button').click();
await page.reload();
await clickSideBarCurrentWorkspaceBanner(page);
const signInButton = page.getByTestId('cloud-signin-button');
await expect(signInButton).toBeVisible();
Expand Down

0 comments on commit fa146ef

Please sign in to comment.