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

feat: add help and support page #1178

Merged
merged 8 commits into from
Dec 18, 2024
Merged
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exclude: "node_modules|.git"
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false

repos:
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const router = createBrowserRouter(
<Route path="create" lazy={() => import('./pages/settings/MessageActions/CreateMessageAction')} />
<Route path=":ID" lazy={() => import('./pages/settings/MessageActions/ViewMessageAction')} />
</Route>

<Route path="help" lazy={() => import('./pages/settings/HelpAndSupport')} />
</Route>
<Route path=":workspaceID" element={<MainPage />}>
<Route index element={<MobileTabsPage />} />
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/components/feature/settings/help/SocketIOHealth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ErrorCallout } from "@/components/common/Callouts/ErrorCallouts"
import { Stack } from "@/components/layout/Stack"
import { Badge, Flex, Heading, IconButton, Link, Text } from "@radix-ui/themes"
import clsx from "clsx"
import { FrappeConfig, FrappeContext, useFrappeEventListener } from "frappe-react-sdk"
import { useContext, useEffect, useState } from "react"
import { LuRefreshCcw } from "react-icons/lu"
import { TbReportAnalytics } from "react-icons/tb"

const SocketIOHealth = () => {

const { socket } = useContext(FrappeContext) as FrappeConfig

const [loading, setLoading] = useState<boolean>(true)
const [socketPingTest, setSocketPingTest] = useState('Fail')
const [socketTransportMode, setSocketTransportMode] = useState<string | undefined>('')

useFrappeEventListener('pong', () => {
setSocketPingTest('Pass')
setLoading(false)
setSocketTransportMode(socket?.io.engine.transport.name)
})

const onPingCheck = () => {
setLoading(true)
socket?.emit('ping')
setTimeout(() => {
setLoading(false)
setSocketTransportMode(s => {
if (!s) {
return ''
}
return s
})
}, 5000)
}

useEffect(() => {
setTimeout(onPingCheck, 5000);
}, []);

return (
<Stack>
<Heading as='h3' size='3' className="not-cal font-semibold">Realtime Connection Test</Heading>
<Text size='2' color='gray'>If messages on Raven do not appear in realtime, you can inspect your network connection here.</Text>
{!loading && socketPingTest === 'Fail' && <ErrorCallout
message="Realtime connections are not working on your site. Messages won't be refreshed in real-time."
/>}
<Flex gap="3" align="center" pt='2'>
<Text size="2" color="gray" as='span'>Real-time Ping Check:</Text>
<Flex align="center" gap="2">
<Badge color={loading ? 'gray' : socketPingTest === "Pass" ? 'green' : 'red'}>{loading ? 'Loading...' : socketPingTest}</Badge>
{!loading && <IconButton title="Send a ping" aria-label="send a ping" color="gray" size="1" variant="ghost" onClick={onPingCheck}>
<LuRefreshCcw className={clsx(loading ? "animate-spin" : null)} size={12} />
</IconButton>}
</Flex>
</Flex>

{socketTransportMode && <Flex gap="2" align="center">
<Text size="2" color="gray" as='span'>SocketIO Transport Mode:</Text>
<Badge color="orange">{socketTransportMode}</Badge>
</Flex>}
<div className="pt-2">
<Link underline="always" size='2' target="_blank"
title="System Health Report"
href="/app/system-health-report"><TbReportAnalytics size='16' className="-mb-0.5 pr-1" />View Full System Health Report
</Link>
</div>
</Stack>
)
}

export default SocketIOHealth
203 changes: 203 additions & 0 deletions frontend/src/components/feature/settings/help/SupportRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { ReactNode } from "react";
import { Box, Button, Dialog, Flex, Link, RadioCards, Text, TextArea, TextField } from "@radix-ui/themes"
import { Controller, useForm } from "react-hook-form"
import { MdOutlineMessage, MdOutlineQuestionMark } from "react-icons/md";
import clsx from "clsx";
import { useFrappePostCall } from "frappe-react-sdk";
import { toast } from "sonner";
import { BiBug } from "react-icons/bi";
import { ErrorText, Label } from "@/components/common/Form"
import { Loader } from "@/components/common/Loader"
import { HStack, Stack } from "@/components/layout/Stack"
import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog"
import { useUserData } from "@/hooks/useUserData";

type TicketType = "Feedback" | "Question" | "Bug"

const subTitles: Record<TicketType, { heading: string, subHeading: string, defaultTextAreaValue: string, footerHeading: ReactNode }> = {
"Feedback": {
heading: "Send feedback",
subHeading: "How can we improve Linear? If you have a feature request, can you also share how you would use it and why it's important to you?",
defaultTextAreaValue: "What if...",
footerHeading: <span>You can also email us at <Link href="mailto:[email protected]" underline="none" size='1' target="_blank">[email protected]</Link> We can't respond to every request but we read all of them.</span>
},
"Question": {
heading: "Ask a question",
subHeading: "How can we help? Please share any relevant information we may need to answer your question.",
defaultTextAreaValue: "How do I...",
footerHeading: <span>You can also email us at <Link href="mailto:[email protected]" underline="none" size='1' target="_blank">[email protected]</Link></span>
},
"Bug": {
heading: "Contact us",
subHeading: "What is the issue? If you're reporting a bug, what are the steps you took so we can reproduce the behaviour?",
defaultTextAreaValue: "Something seems wrong...",
footerHeading: <span>You can also email us at <Link href="mailto:[email protected]" underline="none" size='1' target="_blank">[email protected]</Link></span>
},
};

interface CreateSupportTicketDialogProps {
open: boolean
onClose: VoidFunction
}

const CreateSupportTicketDialog = ({ open, onClose }: CreateSupportTicketDialogProps) => {

return (
<Dialog.Root open={open} onOpenChange={onClose}>
<Dialog.Content maxWidth={'700px'} className={clsx(DIALOG_CONTENT_CLASS)}>
<SupportRequestForm onClose={onClose} />
</Dialog.Content>
</Dialog.Root>
)
}

interface SupportRequestFormFields {
ticket_type: TicketType
email: string
description: string
}
interface SupportRequestFormProps {
onClose: () => void
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const SupportRequestForm = ({ onClose }: SupportRequestFormProps) => {

const { name } = useUserData()

const {
control,
register,
formState: { errors },
handleSubmit,
watch
} = useForm<SupportRequestFormFields>({
defaultValues: {
ticket_type: "Feedback",
email: emailRegex.test(name ?? '') ? name : ""
}
})

const requestType = watch("ticket_type")

const { call, error, loading } = useFrappePostCall('raven.api.support_request.submit_support_request')

const onSubmit = (data: SupportRequestFormFields) => {
call({
email: data.email,
ticket_type: data.ticket_type,
subject: data.description.substring(0, 140),
description: data.description
})
.then(() => {
toast.success("Form submitted successfully!")
onClose()
})
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Dialog.Title>{subTitles[requestType].heading}</Dialog.Title>
<Dialog.Description size={'2'} className="min-h-10 select-none">{subTitles[requestType].subHeading}</Dialog.Description>
<Stack gap="2" pt="3">
<Controller
name="ticket_type"
control={control}
rules={{
required: 'Request type is required',
validate: (value) => Object.keys(subTitles).includes(value) || 'Invalid request type'
}}
render={({ field: { onChange, value } }) => (
<RadioCards.Root
value={value}
onValueChange={onChange}
>
<RadioCards.Item value="Feedback">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<MdOutlineMessage size="16" />
<Text weight="bold">Feedback</Text>
</Flex>
</Flex>
</RadioCards.Item>
<RadioCards.Item value="Question">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<MdOutlineQuestionMark size="16" />
<Text weight="bold">Question</Text>
</Flex>
</Flex>
</RadioCards.Item>
<RadioCards.Item value="Bug">
<Flex direction="column" width="100%" gap="1">
<Flex align="center" gap="3">
<BiBug size="16" />
<Text weight="bold">Bug</Text>
</Flex>
</Flex>
</RadioCards.Item>
</RadioCards.Root>
)}
/>

{errors.ticket_type && <ErrorText>{errors.ticket_type.message}</ErrorText>}

<Stack>
<Box>
<Label htmlFor='email' isRequired>Email</Label>
<TextField.Root
id='email'
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}
nikkothari22 marked this conversation as resolved.
Show resolved Hide resolved
type="email"
placeholder="[email protected]"
aria-invalid={errors.email ? 'true' : 'false'}
/>
</Box>
{errors.email && <ErrorText>{errors.email?.message}</ErrorText>}
</Stack>

<Stack>
<Box>
<Label htmlFor='description' isRequired>Description</Label>
<TextArea
id='description'
{...register('description', {
required: 'Description is required',
minLength: {
value: 10,
message: 'Description must be at least 10 characters'
}
})}
rows={5}
resize='vertical'
placeholder={subTitles[requestType].defaultTextAreaValue}
aria-invalid={errors.description ? 'true' : 'false'}
/>
</Box>
{errors.description && <ErrorText>{errors.description?.message}</ErrorText>}
</Stack>

<HStack justify="between" pt='4' gap="9">
<Text color="gray" size="1" className="select-none">{subTitles[requestType].footerHeading}</Text>
<Flex gap="2">
<Dialog.Close>
<Button color='gray' variant={'soft'} disabled={loading && !error}>Cancel</Button>
</Dialog.Close>
<Button type="submit" disabled={loading} >
{loading && !error ? <Loader className="text-white" /> : null}
Submit
</Button>
</Flex>
</HStack>
</Stack>
</form>
);
}

export default CreateSupportTicketDialog
15 changes: 9 additions & 6 deletions frontend/src/components/feature/userSettings/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { __ } from '@/utils/translations'
import { Box, Flex, Separator, Text } from '@radix-ui/themes'
import clsx from 'clsx'
import { PropsWithChildren, createElement } from 'react'
import { PropsWithChildren, createElement } from 'react';
import { IconType } from 'react-icons'
import { BiBot, BiBuildings } from 'react-icons/bi'
import { BiBot, BiBuildings, BiHelpCircle } from 'react-icons/bi'
import { BsBoxes } from 'react-icons/bs'
import { FiHelpCircle, FiLifeBuoy } from 'react-icons/fi';
import { LuCircleUserRound } from 'react-icons/lu'
import { NavLink } from 'react-router-dom'

export const SettingsSidebar = () => {

return (
<Box className="h-[calc(100vh-57px)] fixed w-64 border-r pt-2 border-gray-4 dark:border-gray-4">
<Flex direction="column" gap='2' className='px-4'>
Expand Down Expand Up @@ -41,6 +41,8 @@ export const SettingsSidebar = () => {
<SettingsSidebarItem title="Commands" to='commands' />
<SettingsSidebarItem title="OpenAI Settings" to='openai-settings' />
</SettingsGroup>
<SettingsSeparator />
<SettingsSidebarItem title="Help & Support" to='help' standalone icon={FiLifeBuoy} />
</Flex>
</Box>
)
Expand All @@ -65,7 +67,7 @@ const SettingsSidebarGroupHeader = ({ title, icon }: { title: string, icon: Icon
)
}

const SettingsSidebarItem = ({ title, to, end }: { title: string, to: string, end?: boolean }) => {
const SettingsSidebarItem = ({ title, to, end, standalone = false, icon }: { title: string, to: string, end?: boolean, standalone?: boolean, icon?: IconType }) => {

const activeClass = "bg-slate-3 dark:bg-slate-4 hover:bg-slate-3 hover:dark:bg-slate-4"

Expand All @@ -77,8 +79,9 @@ const SettingsSidebarItem = ({ title, to, end }: { title: string, to: string, en
>
{({ isActive }) => {
return (
<Box className='ml-4'>
<Flex className={clsx(`px-2 py-1 text-gray-12 rounded-md w-full`, isActive ? activeClass : "bg-transparent hover:bg-slate-2 hover:dark:bg-slate-3")}>
<Box className={!standalone ? 'ml-4' : ''}>
<Flex className={clsx(`px-2 py-1 text-gray-12 rounded-md w-full items-center`, isActive ? activeClass : "bg-transparent hover:bg-slate-2 hover:dark:bg-slate-3", standalone ? "gap-1.5" : '')}>
{icon ? createElement(icon, { size: 15 }) : null}
<Text className='text-[13px]' weight='medium'>{__(title)}</Text>
</Flex>
</Box>
Expand Down
Loading
Loading