feat: add wrapper to settings form

This commit is contained in:
Ben Furber
2024-07-05 13:17:30 +01:00
committed by benfurber
parent 2502d5f576
commit 634007f270
11 changed files with 457 additions and 482 deletions

View File

@@ -7,14 +7,16 @@ import { Flex } from 'theme-ui'
import { SettingsFormTab } from './SettingsFormTab'
import { SettingsFormTabList } from './SettingsFormTabList'
import type { ThemeUIStyleObject } from 'theme-ui'
import type { ITab } from './SettingsFormTab'
export interface IProps {
tabs: ITab[]
sx?: ThemeUIStyleObject | undefined
}
export const SettingsFormWrapper = (props: IProps) => {
const { tabs } = props
const { sx, tabs } = props
const [value, setValue] = useState<number>(0)
const handleChange = (
@@ -25,6 +27,7 @@ export const SettingsFormWrapper = (props: IProps) => {
}
return (
<Flex sx={sx}>
<Tabs value={value} onChange={handleChange}>
<Flex
sx={{
@@ -47,5 +50,6 @@ export const SettingsFormWrapper = (props: IProps) => {
</Flex>
</Flex>
</Tabs>
</Flex>
)
}

View File

@@ -1,377 +1,32 @@
import { useEffect, useState } from 'react'
import { Form } from 'react-final-form'
import { useParams } from 'react-router'
import { ARRAY_ERROR, FORM_ERROR } from 'final-form'
import arrayMutators from 'final-form-arrays'
import { toJS } from 'mobx'
import { Button, ExternalLink, Loader, TextNotification } from 'oa-components'
import { IModerationStatus } from 'oa-shared'
import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog'
import { Loader, SettingsFormWrapper } from 'oa-components'
import { useCommonStores } from 'src/common/hooks/useCommonStores'
import { isPreciousPlastic } from 'src/config/config'
import { logger } from 'src/logger'
import { isModuleSupported, MODULE } from 'src/modules'
import { ProfileType } from 'src/modules/profile/types'
import { Alert, Box, Card, Flex, Heading, Text } from 'theme-ui'
import { v4 as uuid } from 'uuid'
import { Flex, Text } from 'theme-ui'
import { AccountSettingsSection } from './content/formSections/AccountSettings.section'
import { CollectionSection } from './content/formSections/Collection.section'
import { EmailNotificationsSection } from './content/formSections/EmailNotifications.section'
import { ExpertiseSection } from './content/formSections/Expertise.section'
import { FocusSection } from './content/formSections/Focus.section'
import { ImpactSection } from './content/formSections/Impact/Impact.section'
import { PatreonIntegration } from './content/formSections/PatreonIntegration'
import { PublicContactSection } from './content/formSections/PublicContact.section'
import { SettingsErrors } from './content/formSections/SettingsErrors'
import { SettingsMapPinSection } from './content/formSections/SettingsMapPinSection'
import { UserInfosSection } from './content/formSections/UserInfos.section'
import { WorkspaceSection } from './content/formSections/Workspace.section'
import { ProfileGuidelines } from './content/PostingGuidelines'
import { buttons, headings } from './labels'
import INITIAL_VALUES from './Template'
import type { IMapPin } from 'src/models'
import type { IUserPP } from 'src/models/userPreciousPlastic.models'
interface IState {
formValues: IUserPP
showDeleteDialog?: boolean
showLocationDropdown: boolean
user?: IUserPP
userMapPin: IMapPin | null
}
type INotification = { message: string; icon: string; show: boolean }
const MapPinModerationComments = (props: { mapPin: IMapPin | null }) => {
const { mapPin } = props
return mapPin?.comments &&
mapPin.moderation == IModerationStatus.IMPROVEMENTS_NEEDED ? (
<Alert variant="info" sx={{ mt: 3, fontSize: 2, textAlign: 'left' }}>
<Box>
This map pin has been marked as requiring further changes. Specifically
the moderator comments are:
<br />
<em>{mapPin?.comments}</em>
</Box>
</Alert>
) : null
}
const WorkspaceMapPinRequiredStars = () => {
const { description } = headings.workspace
const { themeStore } = useCommonStores().stores
return (
<Alert sx={{ fontSize: 2, textAlign: 'left', my: 2 }} variant="failure">
<Box>
<ExternalLink
href={themeStore?.currentTheme.styles.communityProgramURL}
sx={{ textDecoration: 'underline', color: 'currentcolor' }}
>
{description}
</ExternalLink>
</Box>
</Alert>
)
}
import { UserProfile } from './content/formSections/UserProfile.section'
export const SettingsPage = () => {
const { mapsStore, userStore } = useCommonStores().stores
const [state, setState] = useState<IState>({} as any)
const [notification, setNotification] = useState<INotification>({
message: '',
icon: '',
show: false,
})
const [shouldUpdate, setShouldUpdate] = useState<boolean>(true)
const { id } = useParams()
const toggleLocationDropdown = () => {
setState((prevState) => ({
...prevState,
showLocationDropdown: !prevState.showLocationDropdown,
formValues: {
...prevState.formValues,
mapPinDescription: '',
location: null,
country: null,
},
}))
}
useEffect(() => {
let user = userStore.user as IUserPP
let userMapPin: IMapPin | null = null
const init = async () => {
if (!shouldUpdate) return
if (id) {
user = await userStore.getUserProfile(id)
}
if (isModuleSupported(MODULE.MAP)) {
userMapPin = (await mapsStore.getPin(user.userName)) || null
}
// ensure user form includes all user fields (merge any legacy user with correct format)
const baseValues: IUserPP = {
...INITIAL_VALUES,
// use toJS to avoid mobx monitoring of modified fields (e.g. out of bound arrays on link push)
...toJS(user),
}
const { coverImages, openingHours, links } = baseValues
// replace empty arrays with placeholders for filling forms
const formValues: IUserPP = {
...baseValues,
coverImages: new Array(4)
.fill(null)
.map((v, i) => (coverImages[i] ? coverImages[i] : v)),
links: (links.length > 0 ? links : [{} as any]).map((i) => ({
...i,
key: uuid(),
})),
openingHours: openingHours!.length > 0 ? openingHours : [{} as any],
}
// remove as updated by sub-form
if (formValues.impact) {
delete formValues.impact
}
setState({
formValues,
user,
showLocationDropdown: !user?.location?.latlng,
userMapPin,
})
setShouldUpdate(false)
}
init()
}, [shouldUpdate])
const saveProfile = async (values: IUserPP) => {
const vals = { ...values }
vals.coverImages = (vals.coverImages as any[]).filter((cover) =>
cover ? true : false,
)
// Remove undefined vals from obj before sending to firebase
Object.keys(vals).forEach((key) => {
if (vals[key] === undefined) {
delete vals[key]
}
})
try {
logger.debug({ profile: vals }, 'SettingsPage.saveProfile')
await userStore.updateUserProfile(vals, 'settings-save-profile', id)
setShouldUpdate(true)
return setNotification({
message: 'Profile Saved',
icon: 'check',
show: true,
})
} catch (error) {
logger.warn({ error, profile: vals }, 'SettingsPage.saveProfile.error')
setNotification({ message: 'Save Failed', icon: 'close', show: true })
return { [FORM_ERROR]: 'Save Failed' }
}
}
const validateForm = (v: IUserPP) => {
const errors: any = {}
// must have at least 1 cover (awkard react final form array format)
if (!v.coverImages[0]) {
errors.coverImages = []
errors.coverImages[ARRAY_ERROR] = 'Must have at least one cover image'
}
if (!v.links[0]) {
errors.links = []
errors.links[ARRAY_ERROR] = 'Must have at least one link'
}
return errors
}
const { formValues, user, userMapPin } = state
const formId = 'userProfileForm'
const { userStore } = useCommonStores().stores
const user = userStore.activeUser
return user ? (
<Form
id={formId}
onSubmit={saveProfile}
initialValues={formValues}
validate={validateForm}
mutators={{ ...arrayMutators }}
validateOnBlur
render={({
form,
submitFailed,
submitting,
values,
handleSubmit,
hasValidationErrors,
valid,
invalid,
errors,
}) => {
const { createProfile, editProfile } = headings
const heading = user.profileType ? editProfile : createProfile
const isMember = values.profileType === ProfileType.MEMBER
return (
<Flex mx={-2} bg={'inherit'} sx={{ flexWrap: 'wrap' }}>
<UnsavedChangesDialog
uploadComplete={userStore.updateStatus.Complete}
message={
'You are leaving this page without saving. Do you want to continue ?'
}
/>
<Flex
sx={{
width: ['100%', '100%', `${(2 / 3) * 100}%`],
my: 4,
bg: 'inherit',
px: 2,
}}
>
<Box sx={{ width: '100%' }}>
<form id="userProfileForm" onSubmit={handleSubmit}>
<Flex sx={{ flexDirection: 'column' }}>
<Card
sx={{
background: 'softblue',
}}
>
<Flex px={3} py={2}>
<Heading as="h1">{heading}</Heading>
<SettingsFormWrapper
sx={{ maxWidth: '850px', alignSelf: 'center', paddingTop: [1, 5] }}
tabs={[
{
title: 'Profile',
header: (
<Flex sx={{ gap: 2, flexDirection: 'column' }}>
<Text as="h3"> Complete your profile</Text>
<Text>
In order to post comments or create content, we'd like you to
share something about yourself.
</Text>
</Flex>
</Card>
<Box
sx={{
display: ['block', 'block', 'none'],
mt: 3,
}}
>
<ProfileGuidelines />
</Box>
{isModuleSupported(MODULE.MAP) && <FocusSection />}
{values.profileType === ProfileType.WORKSPACE && (
<WorkspaceSection />
)}
{values.profileType === ProfileType.COLLECTION_POINT && (
<CollectionSection
required={
values.collectedPlasticTypes
? values.collectedPlasticTypes.length === 0
: true
}
formValues={values}
/>
)}
{values.profileType === ProfileType.MACHINE_BUILDER && (
<ExpertiseSection
required={
values.machineBuilderXp
? values.machineBuilderXp.length === 0
: true
}
/>
)}
<UserInfosSection
formValues={values}
mutators={form.mutators}
showLocationDropdown={state.showLocationDropdown}
/>
{isModuleSupported(MODULE.MAP) && (
<SettingsMapPinSection
toggleLocationDropdown={toggleLocationDropdown}
>
<MapPinModerationComments mapPin={userMapPin} />
{!isMember && <WorkspaceMapPinRequiredStars />}
</SettingsMapPinSection>
)}
</Flex>
{!isMember && isPreciousPlastic() && <ImpactSection />}
<EmailNotificationsSection
notificationSettings={values.notification_settings}
/>
{!isMember && (
<PublicContactSection
isContactableByPublic={values.isContactableByPublic}
/>
)}
<PatreonIntegration user={user} />
</form>
<AccountSettingsSection />
</Box>
</Flex>
{/* desktop guidelines container */}
<Flex
sx={{
width: ['100%', '100%', `${100 / 3}%`],
flexDirection: 'column',
bg: 'inherit',
px: 2,
height: 'auto',
mt: [0, 0, 4],
}}
>
<Box
sx={{
position: ['relative', 'relative', 'sticky'],
top: 3,
maxWidth: ['100%', '100%', '400px'],
}}
>
{isModuleSupported(MODULE.MAP) && (
<Box mb={3} sx={{ display: ['none', 'none', 'block'] }}>
<ProfileGuidelines />
</Box>
)}
<Button
large
form={formId}
data-cy="save"
title={
invalid ? `Errors: ${Object.keys(errors || {})}` : 'Submit'
}
mb={3}
sx={{ width: '100%', justifyContent: 'center' }}
variant={'primary'}
type="submit"
// disable button when form invalid or during submit.
// ensure enabled after submit error
disabled={submitting || (submitFailed && hasValidationErrors)}
>
{buttons.save}
</Button>
<SettingsErrors
errors={errors}
isVisible={submitFailed && hasValidationErrors}
/>
{valid && notification.message !== '' && (
<TextNotification
isVisible={notification.show}
variant={valid ? 'success' : 'failure'}
>
<Text>{buttons.success}</Text>
</TextNotification>
)}
</Box>
</Flex>
</Flex>
)
}}
),
body: <UserProfile />,
glyph: 'thunderbolt',
},
]}
/>
) : (
<Loader />

View File

@@ -12,7 +12,7 @@ export const AccountSettingsSection = observer(() => {
const { description, title } = fields.deleteAccount
return (
<Card sx={{ padding: 4, marginTop: 4 }}>
<Card sx={{ padding: 4 }}>
<Flex
sx={{
justifyContent: 'space-between',

View File

@@ -6,7 +6,7 @@ import { SelectField } from 'src/common/Form/Select.field'
import { buttons, fields } from 'src/pages/UserSettings/labels'
import { formatLink } from 'src/utils/formatters'
import { required, validateEmail, validateUrl } from 'src/utils/validators'
import { Box, Flex, Grid } from 'theme-ui'
import { Flex } from 'theme-ui'
const COM_TYPE_MOCKS = [
{
@@ -84,7 +84,6 @@ export const ProfileLinkField = (props: IProps) => {
variant={'outline'}
showIconOnly={true}
onClick={() => toggleDeleteModal()}
ml={2}
{...props}
>
{text}
@@ -92,11 +91,12 @@ export const ProfileLinkField = (props: IProps) => {
)
return (
<Flex my={[2]} sx={{ flexDirection: ['column', 'column', 'row'] }}>
<Grid mb={[1, 1, 0]} gap={0} sx={{ width: ['100%', '100%', '210px'] }}>
<Box
<Flex
sx={{
mr: 2,
flexDirection: 'row',
alignItems: 'center',
gap: [1, 2],
marginBottom: 2,
}}
>
<Field
@@ -110,24 +110,8 @@ export const ProfileLinkField = (props: IProps) => {
placeholder={buttons.link.type}
validate={required}
validateFields={[]}
style={{ width: '100%', height: '40px' }}
style={{ flex: 3 }}
/>
</Box>
{isDeleteEnabled ? (
<DeleteButton
sx={{
display: ['block', 'block', 'none'],
}}
ml={'2px'}
/>
) : null}
</Grid>
<Grid
mb={[1, 1, 0]}
gap={0}
columns={['auto', 'auto', 'auto']}
sx={{ width: '100%' }}
>
<Field
data-cy={`input-link-${index}`}
name={`${name}.url`}
@@ -137,16 +121,9 @@ export const ProfileLinkField = (props: IProps) => {
placeholder={fields.links.placeholder}
format={(v) => formatLink(v, state.linkType)}
formatOnBlur={true}
style={{ width: '100%', height: '40px', marginBottom: '0px' }}
style={{ flex: 1 }}
/>
</Grid>
{isDeleteEnabled ? (
<DeleteButton
sx={{
display: ['none', 'none', 'block'],
}}
/>
) : null}
{isDeleteEnabled ? <DeleteButton /> : null}
{
<ConfirmModal
isOpen={!!state.showDeleteModal}

View File

@@ -1,6 +1,6 @@
import { Field } from 'react-final-form'
import { useTheme } from '@emotion/react'
import { Button, ExternalLink } from 'oa-components'
import { ExternalLink } from 'oa-components'
import { getSupportedProfileTypes } from 'src/modules/profile'
import { buttons, fields, headings } from 'src/pages/UserSettings/labels'
import { Box, Flex, Grid, Heading, Paragraph, Text } from 'theme-ui'
@@ -11,7 +11,7 @@ import { FlexSectionContainer } from './elements'
import type { ProfileTypeLabel } from 'src/modules/profile/types'
const ProfileTypes = () => {
const { description, error, title } = fields.activities
const { description, error } = fields.activities
const theme = useTheme()
const profileTypes = getSupportedProfileTypes().filter(({ label }) =>
Object.keys(theme.badges).includes(label),
@@ -31,8 +31,21 @@ const ProfileTypes = () => {
{headings.focus}
</Heading>
</Flex>
<Flex sx={{ alignItems: 'baseline', marginY: 2 }}>
<Paragraph my={4}>
{description}{' '}
<ExternalLink
href={theme.profileGuidelinesURL}
sx={{ textDecoration: 'underline', color: 'grey' }}
type="button"
>
{buttons.guidelines}
</ExternalLink>
</Paragraph>
</Flex>
<Box>
<Paragraph my={4}>{title}</Paragraph>
<Grid columns={['repeat(auto-fill, minmax(125px, 1fr))']} gap={2}>
{profileTypes.map((profile, index: number) => (
<Box key={index}>
@@ -52,14 +65,6 @@ const ProfileTypes = () => {
</Box>
))}
</Grid>
<Flex sx={{ flexWrap: 'wrap', alignItems: 'center' }} mt={4}>
<Text my={2}>{description}</Text>
<ExternalLink href={theme.profileGuidelinesURL}>
<Button ml={[1, 2, 2]} variant="outline" type="button">
{buttons.guidelines}
</Button>
</ExternalLink>
</Flex>
{props.meta.error && <Text color={theme.colors.red}>{error}</Text>}
</Box>
</FlexSectionContainer>

View File

@@ -186,7 +186,7 @@ export const UserInfosSection = (props: IProps) => {
/>
</Flex>
<Box data-cy="UserInfos: links">
<Flex sx={{ alignItems: 'center', width: '100%', wrap: 'nowrap' }}>
<Flex>
<Text mb={2} mt={7} sx={{ fontSize: 2 }}>
{`${fields.links.title} *`}
</Text>

View File

@@ -0,0 +1,353 @@
import React, { useEffect, useState } from 'react'
import { Form } from 'react-final-form'
import { useParams } from 'react-router'
import { ARRAY_ERROR, FORM_ERROR } from 'final-form'
import arrayMutators from 'final-form-arrays'
import { toJS } from 'mobx'
import { Button, ExternalLink, TextNotification } from 'oa-components'
import { IModerationStatus } from 'oa-shared'
import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog'
import { useCommonStores } from 'src/common/hooks/useCommonStores'
import { isPreciousPlastic } from 'src/config/config'
import { logger } from 'src/logger'
import { isModuleSupported, MODULE } from 'src/modules'
import { ProfileType } from 'src/modules/profile/types'
import { Alert, Box, Flex, Text } from 'theme-ui'
import { v4 as uuid } from 'uuid'
import { buttons, headings } from '../../labels'
import INITIAL_VALUES from '../../Template'
import { ImpactSection } from './Impact/Impact.section'
import { AccountSettingsSection } from './AccountSettings.section'
import { CollectionSection } from './Collection.section'
import { FlexSectionContainer } from './elements'
import { EmailNotificationsSection } from './EmailNotifications.section'
import { ExpertiseSection } from './Expertise.section'
import { FocusSection } from './Focus.section'
import { PatreonIntegration } from './PatreonIntegration'
import { PublicContactSection } from './PublicContact.section'
import { SettingsErrors } from './SettingsErrors'
import { SettingsMapPinSection } from './SettingsMapPinSection'
import { UserInfosSection } from './UserInfos.section'
import { WorkspaceSection } from './Workspace.section'
import type { IMapPin } from 'src/models'
import type { IUserPP } from 'src/models/userPreciousPlastic.models'
interface IState {
formValues: IUserPP
showDeleteDialog?: boolean
showLocationDropdown: boolean
user?: IUserPP
userMapPin: IMapPin | null
}
type INotification = {
message: string
icon: string
show: boolean
variant: 'success' | 'failure'
}
const MapPinModerationComments = (props: { mapPin: IMapPin | null }) => {
const { mapPin } = props
return mapPin?.comments &&
mapPin.moderation == IModerationStatus.IMPROVEMENTS_NEEDED ? (
<Alert variant="info" sx={{ mt: 3, fontSize: 2, textAlign: 'left' }}>
<Box>
This map pin has been marked as requiring further changes. Specifically
the moderator comments are:
<br />
<em>{mapPin?.comments}</em>
</Box>
</Alert>
) : null
}
const WorkspaceMapPinRequiredStars = () => {
const { description } = headings.workspace
const { themeStore } = useCommonStores().stores
return (
<Alert sx={{ fontSize: 2, textAlign: 'left', my: 2 }} variant="failure">
<Box>
<ExternalLink
href={themeStore?.currentTheme.styles.communityProgramURL}
sx={{ textDecoration: 'underline', color: 'currentcolor' }}
>
{description}
</ExternalLink>
</Box>
</Alert>
)
}
export const UserProfile = () => {
const { mapsStore, userStore } = useCommonStores().stores
const [state, setState] = useState<IState>({} as any)
const [notification, setNotification] = useState<INotification>({
message: '',
icon: '',
show: false,
variant: 'success',
})
const [shouldUpdate, setShouldUpdate] = useState<boolean>(true)
const { id } = useParams()
const toggleLocationDropdown = () => {
setState((prevState) => ({
...prevState,
showLocationDropdown: !prevState.showLocationDropdown,
formValues: {
...prevState.formValues,
mapPinDescription: '',
location: null,
country: null,
},
}))
}
useEffect(() => {
let user = userStore.user as IUserPP
let userMapPin: IMapPin | null = null
const init = async () => {
if (!shouldUpdate) return
if (id) {
user = await userStore.getUserProfile(id)
}
if (isModuleSupported(MODULE.MAP)) {
userMapPin = (await mapsStore.getPin(user.userName)) || null
}
// ensure user form includes all user fields (merge any legacy user with correct format)
const baseValues: IUserPP = {
...INITIAL_VALUES,
// use toJS to avoid mobx monitoring of modified fields (e.g. out of bound arrays on link push)
...toJS(user),
}
const { coverImages, openingHours, links } = baseValues
// replace empty arrays with placeholders for filling forms
const formValues: IUserPP = {
...baseValues,
coverImages: new Array(4)
.fill(null)
.map((v, i) => (coverImages[i] ? coverImages[i] : v)),
links: (links.length > 0 ? links : [{} as any]).map((i) => ({
...i,
key: uuid(),
})),
openingHours: openingHours!.length > 0 ? openingHours : [{} as any],
}
// remove as updated by sub-form
if (formValues.impact) {
delete formValues.impact
}
setState({
formValues,
user,
showLocationDropdown: !user?.location?.latlng,
userMapPin,
})
setShouldUpdate(false)
}
init()
}, [shouldUpdate])
const saveProfile = async (values: IUserPP) => {
window.scrollTo(0, 0)
const vals = { ...values }
vals.coverImages = (vals.coverImages as any[]).filter((cover) =>
cover ? true : false,
)
// Remove undefined vals from obj before sending to firebase
Object.keys(vals).forEach((key) => {
if (vals[key] === undefined) {
delete vals[key]
}
})
try {
logger.debug({ profile: vals }, 'SettingsPage.saveProfile')
await userStore.updateUserProfile(vals, 'settings-save-profile', id)
setShouldUpdate(true)
return setNotification({
message: 'Profile Saved',
icon: 'check',
show: true,
variant: 'success',
})
} catch (error) {
logger.warn({ error, profile: vals }, 'SettingsPage.saveProfile.error')
setNotification({
message: 'Save Failed',
icon: 'close',
show: true,
variant: 'failure',
})
return { [FORM_ERROR]: 'Save Failed' }
}
}
const validateForm = (v: IUserPP) => {
const errors: any = {}
// must have at least 1 cover (awkard react final form array format)
if (!v.coverImages[0]) {
errors.coverImages = []
errors.coverImages[ARRAY_ERROR] = 'Must have at least one cover image'
}
if (!v.links[0]) {
errors.links = []
errors.links[ARRAY_ERROR] = 'Must have at least one link'
}
return errors
}
const { formValues, user, userMapPin } = state
const formId = 'userProfileForm'
return (
user && (
<Form
id={formId}
onSubmit={saveProfile}
initialValues={formValues}
validate={validateForm}
mutators={{ ...arrayMutators }}
validateOnBlur
render={({
form,
submitFailed,
submitting,
touched,
values,
handleSubmit,
hasValidationErrors,
invalid,
errors,
pristine,
}) => {
const isMember = values.profileType === ProfileType.MEMBER
return (
<Flex
bg={'inherit'}
sx={{ flexDirection: 'column', padding: 4, gap: 4 }}
>
<UnsavedChangesDialog
uploadComplete={userStore.updateStatus.Complete}
message={
'You are leaving this page without saving. Do you want to continue ?'
}
/>
<>
{!!notification.show && pristine && !touched && (
<TextNotification
isVisible={notification.show}
variant={notification.variant}
>
<Text>{buttons.success}</Text>
</TextNotification>
)}
{(errors || submitFailed) && touched && !pristine && (
<SettingsErrors
errors={errors}
isVisible={!!(errors && Object.keys(errors).length > 0)}
/>
)}
</>
<form id="userProfileForm" onSubmit={handleSubmit}>
<Flex sx={{ flexDirection: 'column', gap: 4 }}>
{isModuleSupported(MODULE.MAP) && <FocusSection />}
{values.profileType === ProfileType.WORKSPACE && (
<WorkspaceSection />
)}
{values.profileType === ProfileType.COLLECTION_POINT && (
<CollectionSection
required={
values.collectedPlasticTypes
? values.collectedPlasticTypes.length === 0
: true
}
formValues={values}
/>
)}
{values.profileType === ProfileType.MACHINE_BUILDER && (
<ExpertiseSection
required={
values.machineBuilderXp
? values.machineBuilderXp.length === 0
: true
}
/>
)}
<UserInfosSection
formValues={values}
mutators={form.mutators}
showLocationDropdown={state.showLocationDropdown}
/>
{isModuleSupported(MODULE.MAP) && (
<SettingsMapPinSection
toggleLocationDropdown={toggleLocationDropdown}
>
<MapPinModerationComments mapPin={userMapPin} />
{!isMember && <WorkspaceMapPinRequiredStars />}
</SettingsMapPinSection>
)}
</Flex>
{!isMember && isPreciousPlastic() && (
<FlexSectionContainer>
<ImpactSection />
</FlexSectionContainer>
)}
<EmailNotificationsSection
notificationSettings={values.notification_settings}
/>
{!isMember && (
<FlexSectionContainer>
<PublicContactSection
isContactableByPublic={values.isContactableByPublic}
/>
</FlexSectionContainer>
)}
<PatreonIntegration user={user} />
</form>
<AccountSettingsSection />
<Button
large
form={formId}
data-cy="save"
title={
invalid ? `Errors: ${Object.keys(errors || {})}` : 'Submit'
}
variant={'primary'}
type="submit"
// disable button when form invalid or during submit.
// ensure enabled after submit error
disabled={submitting || (submitFailed && hasValidationErrors)}
sx={{ alignSelf: 'flex-start' }}
>
{buttons.save}
</Button>
</Flex>
)
}}
/>
)
)
}

View File

@@ -17,7 +17,7 @@ import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SettingsPage } from './SettingsPage'
import { UserProfile } from './UserProfile.section'
import type { IUserPPDB } from 'src/models'
@@ -69,21 +69,6 @@ describe('UserSettings', () => {
vi.resetAllMocks()
})
it('displays user settings', async () => {
mockUser = FactoryUser()
// Act
let wrapper
act(() => {
wrapper = Wrapper(mockUser)
})
// Assert
await waitFor(() => {
expect(wrapper.getByText('Edit profile'))
})
})
it('displays one photo for member', async () => {
mockUser = FactoryUser({ profileType: 'member' })
// Act
@@ -269,7 +254,7 @@ const Wrapper = (user: IUserPPDB, routerInitialEntry?: string) => {
}
const router = createMemoryRouter(
createRoutesFromElements(<Route index element={<SettingsPage />} />),
createRoutesFromElements(<Route index element={<UserProfile />} />),
{
initialEntries: [routerInitialEntry ? routerInitialEntry : ''],
},

View File

@@ -7,7 +7,7 @@ import Sheetpress from 'src/assets/images/workspace-focus/sheetpress.png'
import Shredder from 'src/assets/images/workspace-focus/shredder.png'
import { fields } from 'src/pages/UserSettings/labels'
import { required } from 'src/utils/validators'
import { Box, Flex, Heading, Text } from 'theme-ui'
import { Box, Flex, Grid, Heading, Text } from 'theme-ui'
import { CustomRadioField } from './Fields/CustomRadio.field'
import { FlexSectionContainer } from './elements'
@@ -66,7 +66,7 @@ export const WorkspaceSection = () => {
<Text mt={4} mb={4}>
{description}
</Text>
<Flex sx={{ flexWrap: ['wrap', 'wrap', 'nowrap'] }}>
<Grid columns={['repeat(auto-fill, minmax(125px, 1fr))']} gap={2}>
{WORKSPACE_TYPES.map((workspace, index: number) => (
<CustomRadioField
data-cy={workspace.label}
@@ -83,7 +83,7 @@ export const WorkspaceSection = () => {
subText={workspace.subText}
/>
))}
</Flex>
</Grid>
{meta.touched && meta.error && (
<Text
sx={{

View File

@@ -1,6 +1,6 @@
import { Field } from 'react-final-form'
import styled from '@emotion/styled'
import { Card, Flex } from 'theme-ui'
import { Flex } from 'theme-ui'
export const Label = (props) => (
<Flex
@@ -36,9 +36,5 @@ export const HiddenInput = styled(Field)`
`
export const FlexSectionContainer = (props) => (
<Card mt={4} style={{ overflow: 'visible' }}>
<Flex p={4} sx={{ flexWrap: 'nowrap', flexDirection: 'column' }}>
{props.children}
</Flex>
</Card>
<Flex sx={{ flexDirection: 'column', paddingY: 2 }}>{props.children}</Flex>
)

View File

@@ -33,7 +33,7 @@ export const buttons = {
export const fields: ILabels = {
activities: {
description: 'Not sure about your focus?',
description: 'Choose your main activity. Not sure?',
error: 'Please select a focus',
title: 'What is your main activity?',
},