Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ba 2266 report profile #249

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useNotification } from '@baseapp-frontend/utils'

import { UseMutationConfig, graphql, useMutation } from 'react-relay'

import { ReportCreateMutation } from '../../../../../__generated__/ReportCreateMutation.graphql'

export const ReportCreateMutationQuery = graphql`
mutation ReportCreateMutation($input: ReportCreateInput!) {
reportCreate(input: $input) {
target {
reports {
edges {
node {
pk
created
}
}
}
}
}
}
`

export const useReportCreateMutation = (): [
(config: UseMutationConfig<ReportCreateMutation>) => void,
boolean,
] => {
const [commitMutation, isMutationInFlight] =
useMutation<ReportCreateMutation>(ReportCreateMutationQuery)

const { sendToast } = useNotification()
const commit = (config: UseMutationConfig<ReportCreateMutation>) => {
commitMutation({
...config,
onCompleted: (response, errors) => {
errors?.forEach((error) => {
sendToast(error.message, { type: 'error' })
})
config?.onCompleted?.(response, errors)
},
onError: (error) => {
sendToast(error.message, { type: 'error' })
config?.onError?.(error)
},
})
}

return [commit, isMutationInFlight]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { graphql } from 'react-relay'

export const ReportTypeListQuery = graphql`
query ReportTypeListQuery($topLevelOnly: Boolean!, $targetObjectId: String) {
allReportTypes(topLevelOnly: $topLevelOnly, targetObjectId: $targetObjectId) {
edges {
node {
id
name
label
subTypes {
edges {
node {
id
name
label
parentType {
id
}
}
}
}
}
}
}
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/di
import { BlockIcon, UnblockIcon } from '@baseapp-frontend/design-system/components/web/icons'
import { useNotification } from '@baseapp-frontend/utils'

import { Button, CircularProgress, Typography } from '@mui/material'
import { Button, CircularProgress, MenuItem, Typography } from '@mui/material'
import { useFragment, useMutation } from 'react-relay'

import { BlockToggleMutation } from '../../../../../__generated__/BlockToggleMutation.graphql'
Expand Down Expand Up @@ -71,26 +71,44 @@ const BlockButtonWithDialog: FC<BlockButtonWithDialogProps> = ({

return (
<>
<Button
variant={isMenu ? 'text' : 'contained'}
onClick={handleOpen}
sx={{ justifyContent: isMenu ? 'start' : 'center' }}
size="medium"
>
<Typography variant="body2" color={isMenu ? 'error.main' : 'inherit'} noWrap>
{isBlockedByMe ? (
<>
<UnblockIcon sx={{ color: isMenu ? 'error.main' : 'inherit', marginRight: '5px' }} />
{isMenu ? 'Unblock profile' : 'Unblock'}
</>
) : (
<>
<BlockIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Block profile
</>
)}
</Typography>
</Button>
{isMenu ? (
<MenuItem onClick={handleOpen} disableRipple>
<Typography variant="body2" color="error.main" noWrap>
{isBlockedByMe ? (
<>
<UnblockIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Unblock profile
</>
) : (
<>
<BlockIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Block profile
</>
)}
</Typography>
</MenuItem>
) : (
<Button
variant="contained"
onClick={handleOpen}
sx={{ justifyContent: 'center' }}
size="medium"
>
<Typography variant="body2" color="inherit" noWrap>
{isBlockedByMe ? (
<>
<UnblockIcon sx={{ color: 'inherit', marginRight: '5px' }} />
Unblock
</>
) : (
<>
<BlockIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Block profile
</>
)}
</Typography>
</Button>
)}
<ConfirmDialog
open={open}
title={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
'use client'

import { FC, useMemo, useState } from 'react'

import { FlagIcon } from '@baseapp-frontend/design-system/components/common/icons'
import { Dialog } from '@baseapp-frontend/design-system/components/web/dialogs'
import { ChevronIcon } from '@baseapp-frontend/design-system/components/web/icons'

import { Box, Button, Divider, MenuItem, TextField, Typography } from '@mui/material'
import { useLazyLoadQuery } from 'react-relay'

import { ReportTypeListQuery as ReportTypeListQueryType } from '../../../../../__generated__/ReportTypeListQuery.graphql'
import { useReportCreateMutation } from '../../../common/graphql/mutations/ReportCreate'
import { ReportTypeListQuery } from '../../../common/graphql/queries/ReportTypeList'
import { TypeButton } from './styled'
import { ReportButtonWithDialogProps } from './types'

type AllReportTypes = NonNullable<ReportTypeListQueryType['response']['allReportTypes']>
type ReportType = NonNullable<AllReportTypes['edges'][number]>
type ReportTypeNode = NonNullable<ReportType['node']>
type ReportTypeSubType = NonNullable<ReportTypeNode['subTypes']['edges'][number]>
type ReportTypeSubTypeNode = ReportTypeSubType['node']

const ReportButtonWithDialog: FC<ReportButtonWithDialogProps> = ({ targetId, handleClose }) => {
const { allReportTypes } = useLazyLoadQuery<ReportTypeListQueryType>(ReportTypeListQuery, {
topLevelOnly: true,
})
const [isReportModalOpen, setIsReportModalOpen] = useState(false)
const [currentStep, setCurrentStep] = useState('report')
const [reportType, setReportType] = useState<ReportTypeNode>()
const [reportSubType, setReportSubType] = useState<ReportTypeSubTypeNode>()
const [reportText, setReportText] = useState('')

const [commitMutation, isMutationInFlight] = useReportCreateMutation()
const reportTypes = useMemo(
() => allReportTypes?.edges?.filter((edge) => edge?.node).map((edge) => edge?.node) || [],
[allReportTypes?.edges],
)
const subTypes = useMemo(
() => reportType?.subTypes?.edges?.filter((edge) => edge?.node).map((edge) => edge?.node) || [],
[reportType?.subTypes?.edges],
)

const onClose = () => {
handleClose()
setCurrentStep('report')
}

const handleReport = () => {
if (isMutationInFlight || !targetId) {
return
}
commitMutation({
variables: {
input: {
reportSubject: reportText,
reportTypeId: reportType?.id,
targetObjectId: targetId,
},
},
onCompleted: (_, errors) => {
if (!errors) {
setCurrentStep('confirmation')
return
}
onClose()
},
onError: () => {
onClose()
},
})
}
Comment on lines +44 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for report submission

The handleReport function doesn't validate if a report type is selected or if report text is provided.

  const handleReport = () => {
-   if (isMutationInFlight || !targetId) {
+   if (isMutationInFlight || !targetId || !reportType) {
      return
    }
    commitMutation({
      variables: {
        input: {
          reportSubject: reportText,
          reportTypeId: reportType?.id,
          targetObjectId: targetId,
        },
      },
      onCompleted: (_, errors) => {
        if (!errors) {
          setCurrentStep('confirmation')
          return
        }
        onClose()
      },
      onError: () => {
        onClose()
      },
    })
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onClose = () => {
handleClose()
setCurrentStep('report')
}
const handleReport = () => {
if (isMutationInFlight || !targetId) {
return
}
commitMutation({
variables: {
input: {
reportSubject: reportText,
reportTypeId: reportType?.id,
targetObjectId: targetId,
},
},
onCompleted: (_, errors) => {
if (!errors) {
setCurrentStep('confirmation')
return
}
onClose()
},
onError: () => {
onClose()
},
})
}
const onClose = () => {
handleClose()
setCurrentStep('report')
}
const handleReport = () => {
if (isMutationInFlight || !targetId || !reportType) {
return
}
commitMutation({
variables: {
input: {
reportSubject: reportText,
reportTypeId: reportType?.id,
targetObjectId: targetId,
},
},
onCompleted: (_, errors) => {
if (!errors) {
setCurrentStep('confirmation')
return
}
onClose()
},
onError: () => {
onClose()
},
})
}


const steps = [
{
name: 'report',
content: (
<>
<Box>
<Typography variant="h5">Why are you reporting this?</Typography>
</Box>
<Box>
<Typography variant="body2">
Your report is anonymous. If someone is in immediate danger, call the local emergency
services - don&apos;t wait.
</Typography>
</Box>
<Divider />
<Box display="flex" flexDirection="column">
{reportTypes?.map((type) => (
<TypeButton
key={type?.id}
onClick={() => {
if (type?.subTypes?.edges?.length) {
setReportType(type)
setCurrentStep('subTypes')
return
}
if (type) {
setReportType(type)
setCurrentStep('reportText')
}
}}
endIcon={<ChevronIcon position="right" />}
>
<Typography variant="body2">{type?.label}</Typography>
</TypeButton>
))}
</Box>
</>
),
},
{
name: 'subTypes',
content: (
<>
<Typography variant="h5">{reportType?.label}</Typography>
<Divider />
<Box display="flex" flexDirection="column">
{subTypes?.map((subType) => (
<TypeButton
key={subType?.name}
onClick={() => {
setReportSubType(subType)
setCurrentStep('reportText')
}}
endIcon={<ChevronIcon position="right" />}
>
<Typography variant="body2">{subType?.label}</Typography>
</TypeButton>
))}
</Box>
</>
),
},
{
name: 'reportText',
content: (
<>
<Typography variant="h5">How would you describe the problem?</Typography>
<Typography variant="body2">
Use the text field below to explain what the problem you are reporting is.
</Typography>
<Divider />
<TextField
fullWidth
multiline
rows={4}
value={reportText}
onChange={(e) => setReportText(e.target.value)}
placeholder="I find the post to be offensive..."
/>
<Button onClick={() => setCurrentStep('summary')}>Confirm</Button>
</>
),
},
{
name: 'summary',
content: (
<>
<Typography variant="h5">You&apos;re about to submit a report</Typography>
<Typography variant="body2">
Your report is anonymous. If someone is in immediate danger, call the local emergency
services - don&apos;t wait.
</Typography>
<Divider />
<Typography variant="body2">Why are you reporting this?</Typography>
<Typography variant="caption">{reportType?.label}</Typography>
{reportSubType && (
<>
<Typography variant="body2">What type of {reportType?.label}?</Typography>
<Typography variant="caption">{reportSubType?.label}</Typography>
</>
)}
<Typography variant="body2">About the problem</Typography>
<Typography variant="caption">{reportText}</Typography>
<Button disabled={isMutationInFlight} onClick={handleReport}>
Submit Report
</Button>
</>
),
},
{
name: 'confirmation',
content: (
<>
<Typography variant="h5">Thanks for reporting {reportType?.label}</Typography>
<Typography variant="body2">
Your report is anonymous. If someone is in immediate danger, call the local emergency
services - don&apos;t wait.
</Typography>
<Button onClick={onClose}>Close</Button>
</>
),
},
]

return (
<>
<MenuItem onClick={() => setIsReportModalOpen(true)} disableRipple>
<Typography variant="body2" color="error.main" noWrap>
<FlagIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Report profile
</Typography>
</MenuItem>
<Dialog open={isReportModalOpen} onClose={onClose}>
<Box display="flex" flexDirection="column" padding={4} gap={2}>
<Typography variant="subtitle1">
{currentStep === 'confirmation' ? 'Check Report' : 'Report'}
</Typography>
{steps?.find((step) => step.name === currentStep)?.content}
</Box>
</Dialog>
</>
)
}
Comment on lines +198 to +216
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add ARIA attributes for accessibility

The dialog is missing important accessibility attributes to ensure it's usable by all users, including those with screen readers.

      <Dialog 
        open={isReportModalOpen} 
        onClose={onClose}
+       aria-labelledby="report-dialog-title"
+       aria-describedby="report-dialog-description"
      >
        <Box display="flex" flexDirection="column" padding={4} gap={2}>
-         <Typography variant="subtitle1">
+         <Typography variant="subtitle1" id="report-dialog-title">
            {currentStep === 'confirmation' ? 'Check Report' : 'Report'}
          </Typography>
-         {steps?.find((step) => step.name === currentStep)?.content}
+         <div id="report-dialog-description">
+           {steps?.find((step) => step.name === currentStep)?.content}
+         </div>
        </Box>
      </Dialog>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<>
<MenuItem onClick={() => setIsReportModalOpen(true)} disableRipple>
<Typography variant="body2" color="error.main" noWrap>
<FlagIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Report profile
</Typography>
</MenuItem>
<Dialog open={isReportModalOpen} onClose={onClose}>
<Box display="flex" flexDirection="column" padding={4} gap={2}>
<Typography variant="subtitle1">
{currentStep === 'confirmation' ? 'Check Report' : 'Report'}
</Typography>
{steps?.find((step) => step.name === currentStep)?.content}
</Box>
</Dialog>
</>
)
}
return (
<>
<MenuItem onClick={() => setIsReportModalOpen(true)} disableRipple>
<Typography variant="body2" color="error.main" noWrap>
<FlagIcon sx={{ color: 'error.main', marginRight: '5px' }} />
Report profile
</Typography>
</MenuItem>
<Dialog
open={isReportModalOpen}
onClose={onClose}
aria-labelledby="report-dialog-title"
aria-describedby="report-dialog-description"
>
<Box display="flex" flexDirection="column" padding={4} gap={2}>
<Typography variant="subtitle1" id="report-dialog-title">
{currentStep === 'confirmation' ? 'Check Report' : 'Report'}
</Typography>
<div id="report-dialog-description">
{steps?.find((step) => step.name === currentStep)?.content}
</div>
</Box>
</Dialog>
</>
)
}


export default ReportButtonWithDialog
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Button } from '@mui/material'
import { styled } from '@mui/material/styles'

export const TypeButton = styled(Button)(({ theme }) => ({
variant: 'text',
borderRadius: 0,
justifyContent: 'space-between',
color: theme.palette.text.primary,
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: theme.palette.background.neutral,
boxShadow: 'none',
},
}))
Comment on lines +4 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect usage of variant property in styled component

The variant property is incorrectly placed within the styling object. In Material-UI's styled components, component props like variant should be set when using the component, not in the styling definition.

Fix the styled component by removing the variant from the styling object:

export const TypeButton = styled(Button)(({ theme }) => ({
-  variant: 'text',
  borderRadius: 0,
  justifyContent: 'space-between',
  color: theme.palette.text.primary,
  backgroundColor: 'transparent',
  '&:hover': {
    backgroundColor: theme.palette.background.neutral,
    boxShadow: 'none',
  },
}))

Then when using the component, set the variant as a prop:

<TypeButton variant="text">Button Text</TypeButton>

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ReportButtonWithDialogProps {
targetId?: string
handleClose: () => void
}
Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Interface looks good, but should targetId be optional?

The interface is clean and well-defined. However, I'm curious about the optionality of targetId. If a report always needs a target, it might be better to make this required. Otherwise, ensure the component has logic to handle the case when targetId is undefined.


🏁 Script executed:

#!/bin/bash
# Search for usages of ReportButtonWithDialog to see how targetId is handled
rg -A 5 "ReportButtonWithDialog" --type=tsx

Length of output: 71


🏁 Script executed:

#!/bin/bash
# Re-run the search for usages of ReportButtonWithDialog specifically in .tsx files 
rg -A 5 "ReportButtonWithDialog" -g "*.tsx"

Length of output: 3130


Action: Clarify the targetId Requirement

The interface is defined as follows:

export interface ReportButtonWithDialogProps {
  targetId?: string
  handleClose: () => void
}

After verifying the usage in packages/components/modules/profiles/web/ProfileComponent/index.tsx, we see that the component is rendered conditionally—only when profile exists—and targetId is passed as profile?.id. This indicates that when the component is rendered, a target ID is always provided. To avoid any accidental misuse of ReportButtonWithDialog where a report might lack a target, it would be more robust to make targetId a required prop.

Recommendations:

  • If the button is always meant to report on an entity: Adjust the interface so that targetId is required.

    export interface ReportButtonWithDialogProps {
      targetId: string
      handleClose: () => void
    }
  • If there’s a valid use case for rendering the component without an associated target: Ensure that the component has proper logic to handle the scenario when targetId is undefined.

Please review and confirm which approach best matches the intended behavior of the reporting feature.

Loading
Loading