Skip to content

Commit

Permalink
Terms of service dialog. Improve loading animations.
Browse files Browse the repository at this point in the history
  • Loading branch information
MrFlashAccount committed May 22, 2024
1 parent 58ae73d commit dabdf61
Show file tree
Hide file tree
Showing 22 changed files with 373 additions and 81 deletions.
2 changes: 2 additions & 0 deletions app/ide-desktop/.example.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ENSO_CLOUD_ENSO_HOST=https://enso.org
ENSO_CLOUD_REDIRECT=http://localhost:8080
ENSO_CLOUD_ENVIRONMENT=production
ENSO_CLOUD_API_URL=https://aaaaaaaaaa.execute-api.mars.amazonaws.com
ENSO_CLOUD_CHAT_URL=wss://chat.example.com
Expand Down
3 changes: 3 additions & 0 deletions app/ide-desktop/lib/common/src/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ export function getDefines() {
'process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH': stringify(
process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH
),
'process.env.ENSO_CLOUD_ENSO_HOST': stringify(
process.env.ENSO_CLOUD_ENSO_HOST ?? 'https://enso.org'
),
/* eslint-enable @typescript-eslint/naming-convention */
}
}
Expand Down
22 changes: 22 additions & 0 deletions app/ide-desktop/lib/dashboard/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ export async function login(
await locatePasswordInput(page).fill(password)
await locateLoginButton(page).click()
await locateToastCloseButton(page).click()
await passTermsAndConditionsDialog({ page })
}

// ================
Expand Down Expand Up @@ -787,6 +788,23 @@ async function mockDate({ page }: MockParams) {
}`)
}

/**
* Passes Terms and conditions dialog
*/
export async function passTermsAndConditionsDialog({ page }: MockParams) {
// wait for terms and conditions dialog to appear
// but don't fail if it doesn't appear
try {
// wait for terms and conditions dialog to appear
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
await page.waitForSelector('#terms-of-service-modal', { timeout: 500 })
await page.getByRole('checkbox').click()
await page.getByRole('button', { name: 'Accept' }).click()
} catch (error) {
// do nothing
}
}

// ========================
// === mockIDEContainer ===
// ========================
Expand Down Expand Up @@ -836,8 +854,12 @@ export async function mockAll({ page }: MockParams) {
export async function mockAllAndLogin({ page }: MockParams) {
const mocks = await mockAll({ page })
await login({ page })

await passTermsAndConditionsDialog({ page })

// This MUST run after login, otherwise the element's styles are reset when the browser
// is navigated to another page.
await mockIDEContainer({ page })

return mocks
}
4 changes: 4 additions & 0 deletions app/ide-desktop/lib/dashboard/e2e/signUpFlow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ test.test('sign up flow', async ({ page }) => {
await actions.locateEmailInput(page).fill(email)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateLoginButton(page).click()

await actions.passTermsAndConditionsDialog({ page })

await test.expect(actions.locateSetUsernamePanel(page)).toBeVisible()

// Logged in, but account disabled
await actions.locateUsernameInput(page).fill(name)
await actions.locateSetUsernameButton(page).click()

await test.expect(actions.locateUpgradeButton(page)).toBeVisible()
await test.expect(actions.locateDriveView(page)).not.toBeVisible()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ test.test('sign up with organization id', async ({ page }) => {
await actions.locateConfirmPasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateRegisterButton(page).click()

await actions.passTermsAndConditionsDialog({ page })

// Log in
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateLoginButton(page).click()

await actions.passTermsAndConditionsDialog({ page })

// Set username
await actions.locateUsernameInput(page).fill('arbitrary username')
await actions.locateSetUsernameButton(page).click()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ test.test('sign up without organization id', async ({ page }) => {
await actions.locateConfirmPasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateRegisterButton(page).click()

await actions.passTermsAndConditionsDialog({ page })

// Log in
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateLoginButton(page).click()

await actions.passTermsAndConditionsDialog({ page })

// Set username
await actions.locateUsernameInput(page).fill('arbitrary username')
await actions.locateSetUsernameButton(page).click()
Expand Down
94 changes: 45 additions & 49 deletions app/ide-desktop/lib/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import * as toastify from 'react-toastify'
import * as detect from 'enso-common/src/detect'

import * as appUtils from '#/appUtils'
import * as reactQueryClientModule from '#/reactQueryClient'

import * as inputBindingsModule from '#/configurations/inputBindings'

Expand All @@ -58,9 +57,7 @@ import SessionProvider from '#/providers/SessionProvider'
import SupportsLocalBackendProvider from '#/providers/SupportsLocalBackendProvider'

import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
import ErrorScreen from '#/pages/authentication/ErrorScreen'
import ForgotPassword from '#/pages/authentication/ForgotPassword'
import LoadingScreen from '#/pages/authentication/LoadingScreen'
import Login from '#/pages/authentication/Login'
import Registration from '#/pages/authentication/Registration'
import ResetPassword from '#/pages/authentication/ResetPassword'
Expand All @@ -76,6 +73,7 @@ import * as rootComponent from '#/components/Root'

import AboutModal from '#/modals/AboutModal'
import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'
import * as termsOfServiceModal from '#/modals/TermsOfServiceModal'

import type Backend from '#/services/Backend'
import LocalBackend from '#/services/LocalBackend'
Expand Down Expand Up @@ -159,34 +157,24 @@ export interface AppProps {
export default function App(props: AppProps) {
const { supportsLocalBackend } = props

const queryClient = React.useMemo(() => reactQueryClientModule.createReactQueryClient(), [])
const [rootDirectoryPath, setRootDirectoryPath] = React.useState<projectManager.Path | null>(null)
const [error, setError] = React.useState<unknown>(null)
const isLoading = supportsLocalBackend && rootDirectoryPath == null

React.useEffect(() => {
if (supportsLocalBackend) {
void (async () => {
try {
const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`)
const text = await response.text()
setRootDirectoryPath(projectManager.Path(text))
} catch (innerError) {
setError(innerError)
}
})()
}
}, [supportsLocalBackend])
const { data: rootDirectoryPath } = reactQuery.useSuspenseQuery({
queryKey: ['root-directory', supportsLocalBackend],
queryFn: async () => {
if (supportsLocalBackend) {
const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`)
const text = await response.text()
return projectManager.Path(text)
} else {
return null
}
},
})

// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
// will redirect the user between the login/register pages and the dashboard.
return error != null ? (
<ErrorScreen error={error} />
) : isLoading ? (
<LoadingScreen />
) : (
<reactQuery.QueryClientProvider client={queryClient}>
return (
<>
<toastify.ToastContainer
position="top-center"
theme="light"
Expand All @@ -196,7 +184,9 @@ export default function App(props: AppProps) {
transition={toastify.Zoom}
limit={3}
/>
<router.BrowserRouter basename={getMainPageUrl().pathname}>
{/* we want to use startTransition to enable concurrent rendering */}
{/* eslint-disable-next-line @typescript-eslint/naming-convention */}
<router.BrowserRouter basename={getMainPageUrl().pathname} future={{ v7_startTransition: true }}>
<LocalStorageProvider>
<ModalProvider>
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
Expand All @@ -205,7 +195,7 @@ export default function App(props: AppProps) {
</router.BrowserRouter>

<reactQueryDevtools.ReactQueryDevtools />
</reactQuery.QueryClientProvider>
</>
)
}

Expand Down Expand Up @@ -393,22 +383,24 @@ function AppRouter(props: AppRouterProps) {
{/* Protected pages are visible to authenticated users. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route element={<setOrganizationNameModal.SetOrganizationNameModal />}>
<router.Route
path={appUtils.DASHBOARD_PATH}
element={shouldShowDashboard && <Dashboard {...props} />}
/>

<router.Route
path={appUtils.SUBSCRIBE_PATH}
element={
<errorBoundary.ErrorBoundary>
<React.Suspense fallback={<loader.Loader />}>
<subscribe.Subscribe />
</React.Suspense>
</errorBoundary.ErrorBoundary>
}
/>
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
<router.Route element={<setOrganizationNameModal.SetOrganizationNameModal />}>
<router.Route
path={appUtils.DASHBOARD_PATH}
element={shouldShowDashboard && <Dashboard {...props} />}
/>

<router.Route
path={appUtils.SUBSCRIBE_PATH}
element={
<errorBoundary.ErrorBoundary>
<React.Suspense fallback={<loader.Loader />}>
<subscribe.Subscribe />
</React.Suspense>
</errorBoundary.ErrorBoundary>
}
/>
</router.Route>
</router.Route>

<router.Route
Expand All @@ -424,10 +416,12 @@ function AppRouter(props: AppRouterProps) {
</router.Route>
</router.Route>

{/* Semi-protected pages are visible to users currently registering. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.SemiProtectedLayout />}>
<router.Route path={appUtils.SET_USERNAME_PATH} element={<SetUsername />} />
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
{/* Semi-protected pages are visible to users currently registering. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.SemiProtectedLayout />}>
<router.Route path={appUtils.SET_USERNAME_PATH} element={<SetUsername />} />
</router.Route>
</router.Route>
</router.Route>

Expand All @@ -447,7 +441,9 @@ function AppRouter(props: AppRouterProps) {
<router.Route path="*" element={<router.Navigate to="/" replace />} />
</router.Routes>
)

let result = routes

result = (
<SupportsLocalBackendProvider supportsLocalBackend={supportsLocalBackend}>
{result}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface BaseButtonProps extends Omit<twv.VariantProps<typeof BUTTON_STY
* If the handler returns a promise, the button will be in a loading state until the promise resolves.
*/
readonly onPress?: (event: aria.PressEvent) => Promise<void> | void

readonly testId?: string
}

export const BUTTON_STYLES = twv.tv({
Expand Down Expand Up @@ -152,6 +154,7 @@ export const Button = React.forwardRef(function Button(
fullWidth,
rounded,
tooltip,
testId,
onPress = () => {},
...ariaProps
} = props
Expand All @@ -163,7 +166,7 @@ export const Button = React.forwardRef(function Button(

const Tag = isLink ? aria.Link : aria.Button

const goodDefaults = isLink ? { rel: 'noopener noreferrer' } : { type: 'button' }
const goodDefaults = isLink ? { rel: 'noopener noreferrer', 'data-testid': testId ?? 'link' } : { type: 'button', 'data-testid': testId ?? 'button' }
const isIconOnly = (children == null || children === '' || children === false) && icon != null
const shouldShowTooltip = isIconOnly && tooltip !== false
const tooltipElement = shouldShowTooltip ? tooltip ?? ariaProps['aria-label'] : null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function Dialog(props: types.DialogProps) {
className,
onOpenChange = () => {},
modalProps = {},
testId = 'dialog',
...ariaDialogProps
} = props
const dialogRef = React.useRef<HTMLDivElement>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface DialogProps extends aria.DialogProps {
readonly onOpenChange?: (isOpen: boolean) => void
readonly isKeyboardDismissDisabled?: boolean
readonly modalProps?: Pick<aria.ModalOverlayProps, 'className' | 'defaultOpen' | 'isOpen'>

readonly testId?: string
}

/** The props for the DialogTrigger component. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Form = React.forwardRef(function Form<
onSubmitSuccess = () => {},
onSubmitFailed = () => {},
id = formId,
testId,
schema,
...formProps
} = props
Expand All @@ -59,7 +60,7 @@ export const Form = React.forwardRef(function Form<
React.useImperativeHandle(formRef, () => innerForm, [innerForm])

const formMutation = reactQuery.useMutation({
mutationKey: ['FormSubmit', id],
mutationKey: ['FormSubmit', testId, id],
mutationFn: async (fieldValues: TFieldValues) => {
try {
await onSubmit(fieldValues, innerForm)
Expand All @@ -82,11 +83,16 @@ export const Form = React.forwardRef(function Form<
// There is no way to avoid type casting here
// eslint-disable-next-line @typescript-eslint/no-explicit-any,no-restricted-syntax,@typescript-eslint/no-unsafe-argument
const formOnSubmit = innerForm.handleSubmit(formMutation.mutateAsync as any)
const { formState, clearErrors, getValues, setValue, setError, register, unregister } = innerForm

const formStateRenderProps = {
formState: innerForm.formState,
register: innerForm.register,
unregister: innerForm.unregister,
const formStateRenderProps: types.FormStateRenderProps<TFieldValues> = {
formState,
register,
unregister,
setError,
clearErrors,
getValues,
setValue,
}

return (
Expand All @@ -97,6 +103,7 @@ export const Form = React.forwardRef(function Form<
className={typeof className === 'function' ? className(formStateRenderProps) : className}
style={typeof style === 'function' ? style(formStateRenderProps) : style}
noValidate
data-testid={testId}
{...formProps}
>
<reactHookForm.FormProvider {...innerForm}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function Reset(props: ResetProps): React.JSX.Element {
return (
<ariaComponents.Button
{...props}
type="reset"
variant={variant}
size={size}
isDisabled={formState.isSubmitting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export type SubmitProps = Omit<ariaComponents.ButtonProps, 'loading' | 'variant'
* Manages the form state and displays a loading spinner when the form is submitting.
*/
export function Submit(props: SubmitProps): React.JSX.Element {
const { form = reactHookForm.useFormContext(), variant = 'submit', size = 'medium' } = props
const {
form = reactHookForm.useFormContext(),
variant = 'submit',
size = 'medium',
testId = 'form-submit-button',
} = props
const { formState } = form

return (
Expand All @@ -48,6 +53,7 @@ export function Submit(props: SubmitProps): React.JSX.Element {
variant={variant}
size={size}
loading={formState.isSubmitting}
testId={testId}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function useForm<

return reactHookForm.useForm({
...options,
...(schema ? { resolver: zodResolver.zodResolver(schema) } : {}),
...(schema ? { resolver: zodResolver.zodResolver(schema, { async: true }) } : {}),
})
}
}
Expand Down
Loading

0 comments on commit dabdf61

Please sign in to comment.