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

Rav 69 server scripts integrations #705

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
30 changes: 26 additions & 4 deletions raven-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FrappeProvider } from 'frappe-react-sdk'
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom'
import { Outlet, Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom'
import { MainPage } from './pages/MainPage'
import { ProtectedRoute } from './utils/auth/ProtectedRoute'
import { UserProvider } from './utils/auth/UserProvider'
Expand All @@ -10,6 +10,15 @@ import { Toaster } from './components/common/Toast/Toaster'
import { FullPageLoader } from './components/layout/Loaders'
import { useStickyState } from './hooks/useStickyState'
import { Settings } from './pages/settings/Settings'
import { DocTypeEvents } from './pages/settings/ServerScripts/DocTypeEvents'
import { CreateDocTypeEvent } from './pages/settings/ServerScripts/CreateDocTypeEvent'
import { ViewDocTypeEvent } from './pages/settings/ServerScripts/ViewDocTypeEvent'
import { APIEvents } from './pages/settings/ServerScripts/APIEvents/APIEvents'
import { CreateAPIEvent } from './pages/settings/ServerScripts/APIEvents/CreateAPIEvents'
import { ViewAPIEvent } from './pages/settings/ServerScripts/APIEvents/ViewAPIEvents'
import { TemporalEvents } from './pages/settings/ServerScripts/TemporalEvents/TemporalEvents'
import { ViewTemporalEvent } from './pages/settings/ServerScripts/TemporalEvents/ViewTemporalEvent'
import { CreateTemporalEvent } from './pages/settings/ServerScripts/TemporalEvents/CreateTemporalEvent'


const router = createBrowserRouter(
Expand All @@ -25,11 +34,24 @@ const router = createBrowserRouter(
</Route>
<Route path='settings' element={<Settings />}>
<Route path='integrations'>
<Route path='webhooks' element={<p>Webhooks</p>} />
<Route path='server-scripts' element={<p>SS</p>} />
<Route path='doctype-events' element={<Outlet />}>
<Route index element={<DocTypeEvents />} />
<Route path='create' element={<CreateDocTypeEvent />} />
<Route path=':eventID' element={<ViewDocTypeEvent />} />
</Route>
<Route path='scheduled-scripts' element={<Outlet />}>
<Route index element={<TemporalEvents />} />
<Route path='create' element={<CreateTemporalEvent />} />
<Route path=':scriptID' element={<ViewTemporalEvent />} />
</Route>
<Route path='api-events' element={<Outlet />}>
<Route index element={<APIEvents />} />
<Route path='create' element={<CreateAPIEvent />} />
<Route path=':apiID' element={<ViewAPIEvent />} />
</Route>
</Route>
</Route>
</Route>
</Route >
</>
), {
basename: `/${import.meta.env.VITE_BASE_NAME}` ?? '',
Expand Down
4 changes: 3 additions & 1 deletion raven-app/src/components/feature/settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const Integrations = (props: Props) => {
<SidebarGroup>
<SidebarGroupList>
<IntegrationsItem route='/settings/integrations/webhooks' label='Webhooks' />
<IntegrationsItem route='/settings/integrations/server-scripts' label='Server Scripts' />
<IntegrationsItem route='/settings/integrations/doctype-events' label='DocType Events' />
<IntegrationsItem route='/settings/integrations/scheduled-scripts' label='Temporal Events' />
<IntegrationsItem route='/settings/integrations/api-events' label='API Events' />
</SidebarGroupList>
</SidebarGroup>
</SidebarGroup>
Expand Down
105 changes: 105 additions & 0 deletions raven-app/src/components/feature/settings/api-events/APIEventsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Label, HelperText } from "@/components/common/Form"
import { Flex, Box, TextField, TextArea, Checkbox, Grid, Code } from "@radix-ui/themes"
import { Controller, useFormContext } from "react-hook-form"

export interface Props {
edit?: boolean
}

export const APIEventsForm = ({ edit = false }: Props) => {

const { register, watch, control } = useFormContext()

const enableRateLimit = watch('enable_rate_limit')

return (
<Flex direction="column" gap={'5'}>
{!edit && <Box>
<Label htmlFor="name" isRequired>Name</Label>
<TextField.Input
{...register('name', { required: "Name is required.", maxLength: { value: 140, message: "Name cannot be more than 140 characters." } })}
id="name"
placeholder="e.g. Sales Invoice - delete_note"
autoFocus
/>
</Box>}

<Box>
<Label htmlFor="api_method" isRequired>API Method</Label>
<TextField.Input
{...register('api_method', { required: "API Method is required." })}
id="api_method"
placeholder="e.g. delete_note"
autoFocus={edit}
/>
<HelperText>The API endpoint for which this event will be triggered (automatically prefixed with <Code>/api/method</Code>).</HelperText>
</Box>

<Box>
<Controller
control={control}
name="allow_guest"
render={({ field }) => (
<Flex align={'center'} gap={'2'}>
<Checkbox
checked={field.value ? true : false}
onClick={() => field.onChange(!field.value)} />
<Label htmlFor="allow_guest">Allow anyone to use without logging in</Label>
</Flex>
)}
/>
</Box>

<Box>
{/* TODO: Add a script editor here (maybe use Monaco Editor) */}
<Label htmlFor="script" isRequired>Script</Label>
<TextArea
{...register('script', { required: "Script is required." })}
rows={14}
placeholder={"Your script goes here"}
/>
<HelperText>Your custom script to be be called via this API event.</HelperText>
</Box>

<Box>
<Controller
control={control}
name="enable_rate_limit"
render={({ field }) => (
<Flex align={'center'} gap={'2'}>
<Checkbox
checked={field.value ? true : false}
onClick={() => field.onChange(!field.value)} />
<Label htmlFor="enable_rate_limit">Enable Rate Limit</Label>
</Flex>
)}
/>
</Box>

{enableRateLimit ? <Grid columns={"2"} gap={'5'}>
<Box>
<Label htmlFor="rate_limit_count">Rate Limit Count</Label>
<TextField.Input
{...register('rate_limit_count')}
id="rate_limit_count"
type="number"
placeholder="e.g. 5"
autoFocus={enableRateLimit}
/>
<HelperText>Number of requests allowed in the specified time.</HelperText>
</Box>

<Box>
<Label htmlFor="rate_limit_seconds">Rate Limit Seconds</Label>
<TextField.Input
{...register('rate_limit_seconds')}
id="rate_limit_seconds"
type="number"
placeholder="e.g. 86400"
/>
<HelperText>Time period in seconds.</HelperText>
</Box>
</Grid> : null}
</Flex>
)
}
109 changes: 109 additions & 0 deletions raven-app/src/components/feature/settings/common/DeleteAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Loader } from "@/components/common/Loader"
import { ErrorBanner } from "@/components/layout/AlertBanner"
import { useToast } from "@/hooks/useToast"
import { AlertDialog, Button, Callout, Checkbox, Flex, Text } from "@radix-ui/themes"
import { useFrappeDeleteDoc } from "frappe-react-sdk"
import { useState } from "react"
import { FiAlertTriangle } from "react-icons/fi"
import { useNavigate } from "react-router-dom"

export interface Props {
isOpen: boolean,
onClose: () => void
docname: string
path: string
}

export const DeleteAlert = ({ isOpen, onClose, docname, path }: Props) => {
return (
<AlertDialog.Root open={isOpen} onOpenChange={onClose}>
<AlertDialog.Content style={{ maxWidth: 450 }}>
{/* Hii */}
<AlertContent onClose={onClose} docname={docname} path={path} />
</AlertDialog.Content>
</AlertDialog.Root>
)
}


type DeleteDocModalProps = {
onClose: () => void,
docname: string
path: string
}

const AlertContent = ({ onClose, docname, path }: DeleteDocModalProps) => {

const { deleteDoc, error, loading: deletingDoc, reset } = useFrappeDeleteDoc()

const handleClose = () => {
onClose()
reset()
}

const { toast } = useToast()
const navigate = useNavigate()

const onSubmit = () => {
if (docname) {
deleteDoc('Server Script', docname)
.then(() => {
onClose()
navigate(path)
toast({
title: `${docname} deleted`,
variant: 'success',
})
})
}
}

const [allowDelete, setAllowDelete] = useState(false)

return (
<>
<AlertDialog.Title>
Delete {docname}?
</AlertDialog.Title>

<Flex direction='column' gap='4'>
<ErrorBanner error={error} />
<Callout.Root color="red" size='1'>
<Callout.Icon>
<FiAlertTriangle size='18' />
</Callout.Icon>
<Callout.Text>
This action is permanent and cannot be undone.
</Callout.Text>
</Callout.Root>
{/* <Text size='2'>When you delete a channel, all messages from this channel will be removed immediately.</Text> */}
{/* <Flex direction='column'>
<ul className={'list-inside'}>
<li><Text as='span' size='2'>All messages, including files and images will be removed</Text></li>
<li><Text as='span' size='2'>You can archive this channel instead to preserve your messages</Text></li>
</ul>
</Flex> */}
<Text size='2' as='label'>
<Flex gap="2" align={'center'}>
<Checkbox onClick={() => setAllowDelete(!allowDelete)} color='red' />
Yes, I understand, permanently delete this channel
</Flex>
</Text>
</Flex>

<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel>
<Button variant="soft" color="gray" onClick={handleClose}>
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={onSubmit} disabled={!allowDelete || deletingDoc}>
{deletingDoc && <Loader />}
{deletingDoc ? "Deleting" : "Delete"}
</Button>
</AlertDialog.Action>
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { HelperText, Label } from "@/components/common/Form"
import { Box, Flex, Grid, Link, Select, TextArea, TextField } from "@radix-ui/themes"
import { useFormContext, Controller } from "react-hook-form"

export interface Props {
edit?: boolean
}

export const DocTypeEventsForm = ({ edit = false }: Props) => {

const { register, control } = useFormContext()

return (
<Flex direction="column" gap={'5'}>
{!edit && <Box>
<Label htmlFor="name" isRequired>Name</Label>
<TextField.Input
{...register('name', { required: "Name is required.", maxLength: { value: 140, message: "Name cannot be more than 140 characters." } })}
id="name"
placeholder="e.g. Sales Invoice Before Save"
autoFocus
/>
</Box>}

<Grid columns={"2"} gap={'5'} p={'0'}>
<Box>
<Label htmlFor="reference_document_type" isRequired>Reference Document Type</Label>
<TextField.Input
{...register('reference_document_type', { required: "Reference Document Type is required." })}
id="reference_document_type"
placeholder="e.g. Sales Invoice"
autoFocus={edit}
/>
<HelperText>The event will be triggered for this DocType.</HelperText>
</Box>

<Box>
<Label htmlFor="document_event">DocType Event</Label>
<Controller
control={control}
name="document_event"
render={({ field }) => (
<Select.Root {...field} onValueChange={(value) => field.onChange(value)} defaultValue="Before Insert">
<Select.Trigger style={{ width: "100%" }} placeholder="Select Event" />
<Select.Content>
<Select.Group>
<Select.Label>DocType Event</Select.Label>
<Select.Item value='Before Insert'>Before Insert</Select.Item>
<Select.Item value='Before Validate'>Before Validate</Select.Item>
<Select.Item value='Before Save'>Before Save</Select.Item>
<Select.Item value='After Insert'>After Insert</Select.Item>
<Select.Item value='After Save'>After Save</Select.Item>
<Select.Item value='Before Submit'>Before Submit</Select.Item>
<Select.Item value='After Submit'>After Submit</Select.Item>
<Select.Item value='Before Cancel'>Before Cancel</Select.Item>
<Select.Item value='After Cancel'>After Cancel</Select.Item>
<Select.Item value='Before Save (Submitted Documents)'>Before Save (Submitted Documents)</Select.Item>
<Select.Item value='After Save (Submitted Documents)'>After Save (Submitted Documents)</Select.Item>
<Select.Item value='On Payment Authorization'>On Payment Authorization</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
)}
/>
<HelperText>The event on which you want to run this script. <Link href="https://frappeframework.com/docs/user/en/basics/doctypes/controllers" target="_blank">Learn more</Link></HelperText>
</Box>
</Grid>

<Box>
{/* TODO: Add a script editor here (maybe use Monaco Editor) */}
<Label htmlFor="script" isRequired>Script</Label>
<TextArea
{...register('script', { required: "Script is required." })}
rows={14}
placeholder={"Your script goes here"}
/>
<HelperText>Your custom script to be be called via this document event.</HelperText>
</Box>
</Flex>
)
}
Loading
Loading