Skip to content

Commit

Permalink
Merge pull request #2392 from aliraza556/admin-onboarding-flow
Browse files Browse the repository at this point in the history
[Admin]: Trigger `Onboarding Flow` for Open Graph without a Title
  • Loading branch information
Rassl authored Nov 1, 2024
2 parents 8b322e7 + 5028221 commit 94a818b
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/components/Auth/__tests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,37 @@ describe('Auth Component', () => {
})
})

test('should show onboarding modal if admin and no title is set', async () => {
const [setBudget, setIsAdmin, setPubKey, setIsAuthenticated] = [jest.fn(), jest.fn(), jest.fn(), jest.fn()]

useUserStoreMock.mockReturnValue({
isAdmin: true,
setBudget,
setIsAdmin,
setPubKey,
setIsAuthenticated,
})

getIsAdminMock.mockResolvedValue({ data: { isAdmin: true, title: null } })
getSignedMessageFromRelayMock.mockResolvedValue({ message: 'testMessage', signature: 'testSignature' })

render(
<MemoryRouter>
<ThemeProvider theme={appTheme}>
<StyleThemeProvider theme={appTheme}>
<AuthGuard>
<App />
</AuthGuard>
</StyleThemeProvider>
</ThemeProvider>
</MemoryRouter>,
)

waitFor(() => {
expect(screen.getByText('Welcome to SecondBrain')).toBeInTheDocument()
})
})

test.skip('the unauthorized state is correctly set when the user lacks proper credentials', async () => {
const [setBudget, setIsAdmin, setPubKey, setIsAuthenticated] = [jest.fn(), jest.fn(), jest.fn(), jest.fn()]

Expand Down
7 changes: 7 additions & 0 deletions src/components/Auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as sphinx from 'sphinx-bridge'
import styled from 'styled-components'
import { Flex } from '~/components/common/Flex'
import { Text } from '~/components/common/Text'
import { OnboardingModal } from '~/components/ModalsContainer/OnboardingFlow'
import { isDevelopment, isE2E } from '~/constants'
import { getIsAdmin } from '~/network/auth'
import { useDataStore } from '~/stores/useDataStore'
Expand All @@ -18,6 +19,7 @@ export const AuthGuard = ({ children }: PropsWithChildren) => {
const { setBudget, setIsAdmin, setPubKey, setIsAuthenticated, setSwarmUiUrl } = useUserStore((s) => s)
const { splashDataLoading } = useDataStore((s) => s)
const [renderMainPage, setRenderMainPage] = useState(false)
const [showOnboarding, setShowOnboarding] = useState(false)

const {
setTrendingTopicsFeatureFlag,
Expand Down Expand Up @@ -75,6 +77,10 @@ export const AuthGuard = ({ children }: PropsWithChildren) => {
setRealtimeGraphFeatureFlag(res.data.realtimeGraph || false)
setChatInterfaceFeatureFlag(res.data.chatInterface || false)
setFastFiltersFeatureFlag(res.data.fastFilters || false)

if (isAdmin && !res.data.title) {
setShowOnboarding(true)
}
}

setIsAuthenticated(true)
Expand Down Expand Up @@ -131,6 +137,7 @@ export const AuthGuard = ({ children }: PropsWithChildren) => {

return (
<>
{showOnboarding && <OnboardingModal />}
{splashDataLoading && <Splash />}
{renderMainPage && children}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Button } from '@mui/material'
import { FC, useEffect } from 'react'
import { useFormContext } from 'react-hook-form'
import { MdError } from 'react-icons/md'
import styled from 'styled-components'
import { noSpacePattern } from '~/components/AddItemModal/SourceTypeStep/constants'
import { Flex } from '~/components/common/Flex'
import { Text } from '~/components/common/Text'
import { TextInput } from '~/components/common/TextInput'
import { requiredRule } from '~/constants'
import { colors } from '~/utils'

type Props = {
onSubmit: () => void
error?: string
}

export const GraphDetailsStep: FC<Props> = ({ onSubmit, error }) => {
const {
formState: { isSubmitting },
watch,
} = useFormContext()

const title = watch('title')
const description = watch('description')

const isFormValid = !!title?.trim() && !!description?.trim()

useEffect(() => {
const titleInput = document.getElementById('graph-title') as HTMLInputElement

if (titleInput) {
titleInput.focus()
}
}, [])

return (
<Flex>
<Flex direction="column" justify="space-between">
<StyledText>Welcome to SecondBrain</StyledText>
<StyledSubText>Set a name and short description for your graph.</StyledSubText>
</Flex>

<StyledWrapper>
<Flex className="input__wrapper">
<TextInput
id="graph-title"
label="Title"
maxLength={50}
name="title"
placeholder="Type graph title here..."
rules={{
...requiredRule,
pattern: {
message: 'No leading whitespace allowed',
value: noSpacePattern,
},
}}
/>
<TextInput
id="graph-description"
label="Description"
maxLength={100}
name="description"
placeholder="Type graph description here..."
rules={{
...requiredRule,
pattern: {
message: 'No leading whitespace allowed',
value: noSpacePattern,
},
}}
/>
</Flex>
</StyledWrapper>

<Flex mt={10}>
<Button
color="secondary"
disabled={isSubmitting || !!error || !isFormValid}
onClick={onSubmit}
size="large"
variant="contained"
>
Confirm
</Button>
</Flex>
{error ? (
<StyledError>
<StyledErrorText>
<MdError className="errorIcon" />
<span>{error}</span>
</StyledErrorText>
</StyledError>
) : null}
</Flex>
)
}

const StyledText = styled(Text)`
font-size: 22px;
font-weight: 600;
font-family: 'Barlow';
margin-bottom: 10px;
`

const StyledSubText = styled(Text)`
font-size: 14px;
font-family: 'Barlow';
margin-bottom: 20px;
`

const StyledWrapper = styled(Flex)`
width: 100%;
display: flex;
justify-content: center;
gap: 10px;
margin: 0 0 15px 0;
.input__wrapper {
display: flex;
gap: 23px;
max-height: 225px;
overflow-y: auto;
padding-right: 20px;
width: calc(100% + 20px);
}
`

const StyledErrorText = styled(Flex)`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 2px;
.errorIcon {
display: block;
font-size: 13px;
min-height: 13px;
min-width: 13px;
}
span {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
letter-spacing: 0.2px;
padding-left: 4px;
font-size: 13px;
font-family: Barlow;
line-height: 18px;
}
`

const StyledError = styled(Flex)`
display: flex;
align-items: center;
color: ${colors.primaryRed};
position: relative;
margin-top: 20px;
`
120 changes: 120 additions & 0 deletions src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import '@testing-library/jest-dom'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import { postAboutData } from '~/network/fetchSourcesData'
import { useModal } from '~/stores/useModalStore'
import { OnboardingModal } from '../index'

jest.mock('~/network/fetchSourcesData', () => ({
postAboutData: jest.fn(),
}))

jest.mock('~/stores/useModalStore', () => ({
useModal: jest.fn(),
}))

const useModalMock = useModal as jest.MockedFunction<typeof useModal>
const postAboutDataMock = postAboutData as jest.MockedFunction<typeof postAboutData>

describe('OnboardingModal Component', () => {
beforeEach(() => {
jest.clearAllMocks()

useModalMock.mockReturnValue({
close: jest.fn(),
visible: true,
})
})

test('renders the onboarding modal', () => {
render(<OnboardingModal />)
expect(screen.getByText('Welcome to SecondBrain')).toBeInTheDocument()
expect(screen.getByText('Set a name and short description for your graph.')).toBeInTheDocument()
})

test('submits form successfully', async () => {
postAboutDataMock.mockResolvedValue({ status: 'success' })

render(<OnboardingModal />)

fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } })

fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), {
target: { value: 'Test Description' },
})

fireEvent.click(screen.getByText('Confirm'))

await waitFor(() => {
expect(postAboutDataMock).toHaveBeenCalledWith({
title: 'Test Title',
description: 'Test Description',
})
})
})

test('displays error on form submission failure', async () => {
postAboutDataMock.mockRejectedValue({ status: 400, json: async () => ({ errorCode: 'Error occurred' }) })

render(<OnboardingModal />)

fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } })

fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), {
target: { value: 'Test Description' },
})

fireEvent.click(screen.getByText('Confirm'))

await waitFor(() => {
expect(screen.getByText('Error occurred')).toBeInTheDocument()
})
})

test('closes modal on successful submission', async () => {
const closeMock = jest.fn()

useModalMock.mockReturnValue({
close: closeMock,
visible: true,
})

postAboutDataMock.mockResolvedValue({ status: 'success' })

render(<OnboardingModal />)

fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } })

fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), {
target: { value: 'Test Description' },
})

fireEvent.click(screen.getByText('Confirm'))

await waitFor(() => {
expect(closeMock).toHaveBeenCalled()
})
})

test('resets form and error on modal close', async () => {
const { rerender } = render(<OnboardingModal />)

fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } })

fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), {
target: { value: 'Test Description' },
})

useModalMock.mockReturnValue({
close: jest.fn(),
visible: false,
})

rerender(<OnboardingModal />)

waitFor(() => {
expect(screen.getByPlaceholderText('Type graph title here...')).toHaveValue('')
expect(screen.getByPlaceholderText('Type graph description here...')).toHaveValue('')
})
})
})
Loading

0 comments on commit 94a818b

Please sign in to comment.