Gate the workspace setup AI chat to workspace creators (#23881)
The server already refuses to start a workspace setup chat for anyone who isn't the workspace creator (`workspace-setup-chat.service.ts`, via `userWorkspaceService.isWorkspaceCreator`), but nothing on the client checked that. An invitee finishing onboarding was still routed to `/workspace-setup`, where the kickoff mutation returned `UNAVAILABLE` and the effect silently returned — leaving them on a dead-end page with the onboarding header and an empty chat that never starts. Exposes `isWorkspaceCreator` on `User` as a resolve field next to `onboardingStatus`, reusing the existing service method, and gates both the post-onboarding redirect and the page itself on it. Invitees now land on the default home page instead.
This commit is contained in:
@@ -957,6 +957,7 @@ type User {
|
||||
deletedWorkspaceMembers: [DeletedWorkspaceMember!]
|
||||
hasPassword: Boolean!
|
||||
supportUserHash: String
|
||||
isWorkspaceCreator: Boolean
|
||||
workspaces: [UserWorkspace!]!
|
||||
availableWorkspaces: AvailableWorkspaces!
|
||||
}
|
||||
|
||||
@@ -680,6 +680,7 @@ export interface User {
|
||||
deletedWorkspaceMembers?: DeletedWorkspaceMember[]
|
||||
hasPassword: Scalars['Boolean']
|
||||
supportUserHash?: Scalars['String']
|
||||
isWorkspaceCreator?: Scalars['Boolean']
|
||||
workspaces: UserWorkspace[]
|
||||
availableWorkspaces: AvailableWorkspaces
|
||||
__typename: 'User'
|
||||
@@ -3898,6 +3899,7 @@ export interface UserGenqlSelection{
|
||||
deletedWorkspaceMembers?: DeletedWorkspaceMemberGenqlSelection
|
||||
hasPassword?: boolean | number
|
||||
supportUserHash?: boolean | number
|
||||
isWorkspaceCreator?: boolean | number
|
||||
workspaces?: UserWorkspaceGenqlSelection
|
||||
availableWorkspaces?: AvailableWorkspacesGenqlSelection
|
||||
__typename?: boolean | number
|
||||
|
||||
@@ -1845,6 +1845,9 @@ export default {
|
||||
"supportUserHash": [
|
||||
1
|
||||
],
|
||||
"isWorkspaceCreator": [
|
||||
3
|
||||
],
|
||||
"workspaces": [
|
||||
54
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ export type CurrentUser = Pick<
|
||||
| 'canAccessFullAdminPanel'
|
||||
| 'canImpersonate'
|
||||
| 'onboardingStatus'
|
||||
| 'isWorkspaceCreator'
|
||||
| 'userVars'
|
||||
| 'firstName'
|
||||
| 'lastName'
|
||||
|
||||
+13
@@ -36,6 +36,7 @@ type RenderHooksOptions = {
|
||||
isOnboardingAiChatEnabled?: boolean;
|
||||
isBookCallOnboardingStepEnabled?: boolean;
|
||||
isBookCallOnboardingStepPending?: boolean;
|
||||
isWorkspaceCreator?: boolean;
|
||||
};
|
||||
|
||||
const renderHooks = (
|
||||
@@ -47,6 +48,7 @@ const renderHooks = (
|
||||
isOnboardingAiChatEnabled = false,
|
||||
isBookCallOnboardingStepEnabled = false,
|
||||
isBookCallOnboardingStepPending = false,
|
||||
isWorkspaceCreator = true,
|
||||
}: RenderHooksOptions = {},
|
||||
) => {
|
||||
jotaiStore.set(
|
||||
@@ -92,6 +94,7 @@ const renderHooks = (
|
||||
result.current.setCurrentUser({
|
||||
...mockedUserData,
|
||||
onboardingStatus,
|
||||
isWorkspaceCreator,
|
||||
userVars: {
|
||||
...mockedUserData.userVars,
|
||||
[ONBOARDING_BOOK_CALL_PENDING_USER_VAR_KEY]:
|
||||
@@ -375,6 +378,16 @@ describe('useSetNextOnboardingStatus', () => {
|
||||
expect(shouldOpenAiChatAfterOnboarding).toBe(true);
|
||||
});
|
||||
|
||||
it('should not open the ai chat after onboarding for an invitee', () => {
|
||||
const { isWelcomeAnimationVisible, shouldOpenAiChatAfterOnboarding } =
|
||||
renderHooks(OnboardingStatus.INVITE_TEAM, {
|
||||
isOnboardingAiChatEnabled: true,
|
||||
isWorkspaceCreator: false,
|
||||
});
|
||||
expect(isWelcomeAnimationVisible).toBe(true);
|
||||
expect(shouldOpenAiChatAfterOnboarding).toBe(false);
|
||||
});
|
||||
|
||||
it('should still show the welcome animation when the ai chat is disabled', () => {
|
||||
const { isWelcomeAnimationVisible, shouldOpenAiChatAfterOnboarding } =
|
||||
renderHooks(OnboardingStatus.INVITE_TEAM, {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const useSetNextOnboardingStatus = () => {
|
||||
store.set(isWelcomeAnimationVisibleState.atom, true);
|
||||
store.set(
|
||||
shouldOpenAiChatAfterOnboardingState.atom,
|
||||
isOnboardingAiChatEnabled,
|
||||
isOnboardingAiChatEnabled && currentUser?.isWorkspaceCreator === true,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -23,6 +23,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
canImpersonate
|
||||
supportUserHash
|
||||
onboardingStatus
|
||||
isWorkspaceCreator
|
||||
workspaceMember {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { AiChatMessageListPreambleContext } from '@/ai/contexts/AiChatMessageListPreambleContext';
|
||||
import { AiChatTab } from '@/ai/components/AiChatTab';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { WorkspaceSetupChatPreamble } from '@/onboarding/components/WorkspaceSetupChatPreamble';
|
||||
@@ -40,6 +41,7 @@ const StyledContent = styled.div`
|
||||
export const WorkspaceSetup = () => {
|
||||
const { t } = useLingui();
|
||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const isOnboardingAiChatEnabled = useAtomStateValue(
|
||||
isOnboardingAiChatEnabledState,
|
||||
);
|
||||
@@ -47,7 +49,7 @@ export const WorkspaceSetup = () => {
|
||||
shouldOpenAiChatAfterOnboardingState,
|
||||
);
|
||||
|
||||
if (!isOnboardingAiChatEnabled) {
|
||||
if (!isOnboardingAiChatEnabled || currentUser?.isWorkspaceCreator !== true) {
|
||||
return <Navigate to={defaultHomePagePath} replace />;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type ReactNode } from 'react';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { shouldContinueAiChatInSidePanelState } from '@/ai/states/shouldContinueAiChatInSidePanelState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { isOnboardingAiChatEnabledState } from '@/client-config/states/isOnboardingAiChatEnabledState';
|
||||
import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { messages } from '~/locales/generated/en';
|
||||
import { WorkspaceSetup } from '~/pages/onboarding/WorkspaceSetup';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
|
||||
i18n.load({ [SOURCE_LOCALE]: messages });
|
||||
i18n.activate(SOURCE_LOCALE);
|
||||
@@ -69,6 +71,13 @@ const setIsOnboardingAiChatEnabled = (value: boolean) => {
|
||||
jotaiStore.set(isOnboardingAiChatEnabledState.atom, value);
|
||||
};
|
||||
|
||||
const setIsWorkspaceCreator = (value: boolean) => {
|
||||
jotaiStore.set(currentUserState.atom, {
|
||||
...mockedUserData,
|
||||
isWorkspaceCreator: value,
|
||||
});
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
<I18nProvider i18n={i18n}>{children}</I18nProvider>
|
||||
@@ -80,6 +89,7 @@ describe('WorkspaceSetup', () => {
|
||||
sessionStorage.clear();
|
||||
resetJotaiStore();
|
||||
mockNavigate.mockClear();
|
||||
setIsWorkspaceCreator(true);
|
||||
});
|
||||
|
||||
it('should dress the chat for onboarding when the post-onboarding hint is set', () => {
|
||||
@@ -156,4 +166,14 @@ describe('WorkspaceSetup', () => {
|
||||
expect(queryByTestId('ai-chat-tab')).not.toBeInTheDocument();
|
||||
expect(mockNavigate).toHaveBeenCalledWith(defaultHomePagePath);
|
||||
});
|
||||
|
||||
it('should redirect home for an invitee', () => {
|
||||
setIsOnboardingAiChatEnabled(true);
|
||||
setIsWorkspaceCreator(false);
|
||||
|
||||
const { queryByTestId } = render(<WorkspaceSetup />, { wrapper: Wrapper });
|
||||
|
||||
expect(queryByTestId('ai-chat-tab')).not.toBeInTheDocument();
|
||||
expect(mockNavigate).toHaveBeenCalledWith(defaultHomePagePath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -580,6 +580,22 @@ export class UserResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean, {
|
||||
nullable: true,
|
||||
})
|
||||
async isWorkspaceCreator(
|
||||
@Parent() user: UserEntity,
|
||||
@AuthWorkspace({ allowUndefined: true })
|
||||
workspace: WorkspaceEntity | undefined,
|
||||
): Promise<boolean | null> {
|
||||
if (!workspace) return null;
|
||||
|
||||
return this.userWorkspaceService.isWorkspaceCreator({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => WorkspaceEntity, {
|
||||
nullable: true,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user