From c58320e58de09701dcd7af735788e5470b72a9ef Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 12 Nov 2025 18:50:38 +0100 Subject: [PATCH 1/4] feat: refresh data --- .../experience/DeleteExperienceButton.tsx | 4 +++- .../shared/src/hooks/useRemoveExperience.ts | 17 ++++++++++++----- .../shared/src/hooks/useUserExperienceForm.ts | 17 +++++++++++++---- packages/shared/src/hooks/useUserInfoForm.ts | 16 +++++++++++++--- .../pages/settings/profile/experience/edit.tsx | 10 +++++++--- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/shared/src/features/profile/components/experience/DeleteExperienceButton.tsx b/packages/shared/src/features/profile/components/experience/DeleteExperienceButton.tsx index 5e0f4b92e4d..3d635442cdc 100644 --- a/packages/shared/src/features/profile/components/experience/DeleteExperienceButton.tsx +++ b/packages/shared/src/features/profile/components/experience/DeleteExperienceButton.tsx @@ -34,7 +34,9 @@ const DeleteExperienceButton = ({ experienceId, experienceType, }: DeleteExperienceButtonProps): React.ReactElement => { - const { removeExperience, isPending } = useRemoveExperience(); + const { removeExperience, isPending } = useRemoveExperience({ + type: experienceType, + }); const { showPrompt } = usePrompt(); const buttonCopy = getCopy(experienceType); diff --git a/packages/shared/src/hooks/useRemoveExperience.ts b/packages/shared/src/hooks/useRemoveExperience.ts index 26941316ef3..e661e906f9f 100644 --- a/packages/shared/src/hooks/useRemoveExperience.ts +++ b/packages/shared/src/hooks/useRemoveExperience.ts @@ -1,20 +1,27 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; +import type { UserExperienceType } from '../graphql/user/profile'; import { removeUserExperience } from '../graphql/user/profile'; import { useToastNotification } from './useToastNotification'; import { labels } from '../lib/labels'; import { webappUrl } from '../lib/constants'; +import { useUserExperiencesByType } from '../features/profile/hooks/useUserExperiencesByType'; +import { useAuthContext } from '../contexts/AuthContext'; -const useRemoveExperience = () => { +const useRemoveExperience = ({ type }: { type: UserExperienceType }) => { + const { user } = useAuthContext(); const router = useRouter(); const { displayToast } = useToastNotification(); - + const { queryKey: experienceQueryKey } = useUserExperiencesByType( + type, + user?.id, + ); + const qc = useQueryClient(); const { mutate, isPending } = useMutation({ mutationFn: (id: string) => removeUserExperience(id), onSuccess: () => { - const searchParams = new URLSearchParams(window.location.search); - const type = searchParams?.get('type'); router.push(`${webappUrl}/settings/profile/experience/${type}`); + qc.invalidateQueries({ queryKey: experienceQueryKey }); }, onError: () => { displayToast(labels.error.generic || 'Failed to delete experience'); diff --git a/packages/shared/src/hooks/useUserExperienceForm.ts b/packages/shared/src/hooks/useUserExperienceForm.ts index 6acc947aa0e..c06fe57d022 100644 --- a/packages/shared/src/hooks/useUserExperienceForm.ts +++ b/packages/shared/src/hooks/useUserExperienceForm.ts @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -20,6 +20,8 @@ import { labels } from '../lib/labels'; import { applyZodErrorsToForm } from '../lib/form'; import { useToastNotification } from './useToastNotification'; import { webappUrl } from '../lib/constants'; +import { useUserExperiencesByType } from '../features/profile/hooks/useUserExperiencesByType'; +import { useAuthContext } from '../contexts/AuthContext'; export const userExperienceInputBaseSchema = z .object({ @@ -28,7 +30,7 @@ export const userExperienceInputBaseSchema = z description: z.string().max(5000).optional(), subtitle: z.string().max(1000).optional(), startedAt: z.date({ message: 'Start date is required.' }), - endedAt: z.date().optional(), + endedAt: z.date().optional().nullable(), current: z.boolean().default(false), companyId: z.string().nullable().optional().default(null), customCompanyName: z @@ -75,6 +77,12 @@ const useUserExperienceForm = ({ }: { defaultValues: BaseUserExperience; }) => { + const qc = useQueryClient(); + const { user } = useAuthContext(); + const { queryKey: experienceQueryKey } = useUserExperiencesByType( + defaultValues.type, + user?.id, + ); const dirtyFormRef = useRef | null>(null); const router = useRouter(); const { displayToast } = useToastNotification(); @@ -88,10 +96,11 @@ const useUserExperienceForm = ({ type === UserExperienceType.Work ? upsertUserWorkExperience(data as UserExperienceWork, id) : upsertUserGeneralExperience(data, id), - onSuccess: () => { + onSuccess: (_, vars) => { + methods.reset(vars); dirtyFormRef.current?.allowNavigation(); + qc.invalidateQueries({ queryKey: experienceQueryKey }); router.push(`${webappUrl}settings/profile/experience/${type}`); - methods.reset(); }, onError: (error: ApiErrorResult) => { if ( diff --git a/packages/shared/src/hooks/useUserInfoForm.ts b/packages/shared/src/hooks/useUserInfoForm.ts index a46d68704aa..fe56c3a03c0 100644 --- a/packages/shared/src/hooks/useUserInfoForm.ts +++ b/packages/shared/src/hooks/useUserInfoForm.ts @@ -1,11 +1,11 @@ import { useContext, useRef } from 'react'; import { useForm } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import AuthContext from '../contexts/AuthContext'; import { UPDATE_USER_PROFILE_MUTATION } from '../graphql/users'; -import type { LoggedUser, UserProfile } from '../lib/user'; +import type { LoggedUser, PublicProfile, UserProfile } from '../lib/user'; import { useToastNotification } from './useToastNotification'; import type { ResponseError } from '../graphql/common'; import { errorMessage, gqlClient } from '../graphql/common'; @@ -15,6 +15,7 @@ import { useActions } from './useActions'; import { getCompletionItems } from '../features/profile/components/ProfileWidgets/ProfileCompletion'; import { useLogContext } from '../contexts/LogContext'; import { LogEvent } from '../lib/log'; +import { useProfile } from './profile/useProfile'; export interface ProfileFormHint { portfolio?: string; @@ -59,8 +60,10 @@ const socials = [ ]; const useUserInfoForm = (): UseUserInfoForm => { - const { logEvent } = useLogContext(); + const qc = useQueryClient(); const { user, updateUser } = useContext(AuthContext); + const { userQueryKey } = useProfile(user as PublicProfile); + const { logEvent } = useLogContext(); const { displayToast } = useToastNotification(); const { completeAction, checkHasCompleted, isActionsFetched } = useActions(); const router = useRouter(); @@ -105,6 +108,13 @@ const useUserInfoForm = (): UseUserInfoForm => { }), onSuccess: async (_, vars) => { + const oldProfileData = qc.getQueryData(userQueryKey); + if (oldProfileData) { + qc.setQueryData(userQueryKey, { + ...oldProfileData, + ...vars, + }); + } await updateUser({ ...user, ...vars }); dirtyFormRef.current?.allowNavigation(); diff --git a/packages/webapp/pages/settings/profile/experience/edit.tsx b/packages/webapp/pages/settings/profile/experience/edit.tsx index a586b25c55a..fdab8897aaa 100644 --- a/packages/webapp/pages/settings/profile/experience/edit.tsx +++ b/packages/webapp/pages/settings/profile/experience/edit.tsx @@ -113,6 +113,7 @@ export const getServerSideProps: GetServerSideProps = async ({ startedAtYear, endedAtMonth, endedAtYear, + current: !result?.endedAt, skills: result?.skills.map((skill) => skill.value), location: result?.location, locationId: result?.location?.id || '', @@ -121,7 +122,10 @@ export const getServerSideProps: GetServerSideProps = async ({ }; }; -const renderExperienceForm = (type?: UserExperienceType) => { +const renderExperienceForm = ( + type?: UserExperienceType, + experience?: DefaultValues, +) => { switch (type) { case UserExperienceType.Education: return ; @@ -133,7 +137,7 @@ const renderExperienceForm = (type?: UserExperienceType) => { case UserExperienceType.OpenSource: return ; default: - return ; + return ; } }; @@ -163,7 +167,7 @@ const Page = ({ experience }: PageProps): ReactElement => { } > - {renderExperienceForm(experience?.type)} + {renderExperienceForm(experience?.type, experience)} {experience?.id && ( Date: Wed, 12 Nov 2025 18:55:39 +0100 Subject: [PATCH 2/4] remove unnecessary conditional --- packages/shared/src/hooks/useUserInfoForm.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/hooks/useUserInfoForm.ts b/packages/shared/src/hooks/useUserInfoForm.ts index fe56c3a03c0..93c5bbe0827 100644 --- a/packages/shared/src/hooks/useUserInfoForm.ts +++ b/packages/shared/src/hooks/useUserInfoForm.ts @@ -109,12 +109,10 @@ const useUserInfoForm = (): UseUserInfoForm => { onSuccess: async (_, vars) => { const oldProfileData = qc.getQueryData(userQueryKey); - if (oldProfileData) { - qc.setQueryData(userQueryKey, { - ...oldProfileData, - ...vars, - }); - } + qc.setQueryData(userQueryKey, { + ...oldProfileData, + ...vars, + }); await updateUser({ ...user, ...vars }); dirtyFormRef.current?.allowNavigation(); From 046d6ba6b39f59b1402e9b03015c3d0427fad89c Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 12 Nov 2025 19:55:09 +0100 Subject: [PATCH 3/4] update tests --- .../src/hooks/useUserExperienceForm.spec.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/shared/src/hooks/useUserExperienceForm.spec.tsx b/packages/shared/src/hooks/useUserExperienceForm.spec.tsx index 9b1f4dd1aed..1c93e267b6a 100644 --- a/packages/shared/src/hooks/useUserExperienceForm.spec.tsx +++ b/packages/shared/src/hooks/useUserExperienceForm.spec.tsx @@ -29,6 +29,20 @@ jest.mock('../graphql/user/profile', () => ({ upsertUserGeneralExperience: jest.fn(), })); +// Mock AuthContext +jest.mock('../contexts/AuthContext', () => ({ + useAuthContext: jest.fn(() => ({ + user: { id: 'test-user-id' }, + })), +})); + +// Mock useUserExperiencesByType hook +jest.mock('../features/profile/hooks/useUserExperiencesByType', () => ({ + useUserExperiencesByType: jest.fn(() => ({ + queryKey: ['user-experiences', 'test-type', 'test-user-id'], + })), +})); + const mockRouter = { back: jest.fn(), pathname: '/profile/experience', @@ -59,6 +73,8 @@ type BaseUserExperience = { subtitle?: string | null; current?: boolean; companyId?: string | null; + customCompanyName?: string | null; + url?: string | null; }; describe('useUserExperienceForm', () => { From 72f9e0b80f42bf2b36298939942e1f80bfcc4a0a Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 12 Nov 2025 20:08:47 +0100 Subject: [PATCH 4/4] update test prop --- .../experience/forms/UserCertificationForm.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/features/profile/components/experience/forms/UserCertificationForm.spec.tsx b/packages/shared/src/features/profile/components/experience/forms/UserCertificationForm.spec.tsx index 1fb62677212..10ff18dbb6b 100644 --- a/packages/shared/src/features/profile/components/experience/forms/UserCertificationForm.spec.tsx +++ b/packages/shared/src/features/profile/components/experience/forms/UserCertificationForm.spec.tsx @@ -33,7 +33,7 @@ const FormWrapper: React.FC = ({ defaultValues: { title: '', customCompanyName: '', - currentPosition: false, + current: false, startedAt: null, endedAt: null, externalReferenceId: '', @@ -157,7 +157,7 @@ describe('UserCertificationForm', () => { const defaultValues = { title: 'Google Cloud Professional Data Engineer', customCompanyName: 'Google', - currentPosition: true, + current: true, externalReferenceId: 'GCP-987654321', url: 'https://example-cloud.com/certification/verify/987654321', description: 'Expertise in data engineering on Google Cloud Platform',