mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-10 09:12:51 +01:00
feat: add reactions (#1074)
This commit is contained in:
36
ui/src/components/reaction/ActionPythonFormFields.vue
Normal file
36
ui/src/components/reaction/ActionPythonFormFields.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import GrowTextarea from '@/components/form/GrowTextarea.vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField name="actiondata.requirements" v-slot="{ componentField }">
|
||||
<FormItem>
|
||||
<FormLabel for="requirements" class="text-right">requirements.txt</FormLabel>
|
||||
<FormControl>
|
||||
<GrowTextarea id="requirements" class="col-span-3" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription> Specify the Python packages required to run the script. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField name="actiondata.script" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="script" class="text-right">Script</FormLabel>
|
||||
<FormControl>
|
||||
<GrowTextarea id="script" class="col-span-3" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Write a Python script to run when the reaction is triggered.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
40
ui/src/components/reaction/ActionWebhookFormFields.vue
Normal file
40
ui/src/components/reaction/ActionWebhookFormFields.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import GrowListTextarea from '@/components/form/ListInput.vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField name="actiondata.headers" v-slot="{ value, handleChange }">
|
||||
<FormItem>
|
||||
<FormLabel for="headers" class="text-right">Headers</FormLabel>
|
||||
<FormControl>
|
||||
<GrowListTextarea
|
||||
id="headers"
|
||||
:modelValue="value"
|
||||
@update:modelValue="handleChange"
|
||||
placeholder="Content-Type: application/json"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription> Specify the headers to include in the request. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField name="actiondata.url" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="url" class="text-right">URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="url" v-bind="componentField" placeholder="https://example.com/webhook" />
|
||||
</FormControl>
|
||||
<FormDescription> Specify the URL to send the request to. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
62
ui/src/components/reaction/ReactionDisplay.vue
Normal file
62
ui/src/components/reaction/ReactionDisplay.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import type { Reaction } from '@/lib/types'
|
||||
import { handleError } from '@/lib/utils'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
}>()
|
||||
|
||||
const {
|
||||
isPending,
|
||||
isError,
|
||||
data: reaction,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['reactions', props.id],
|
||||
queryFn: (): Promise<Reaction> => pb.collection('reactions').getOne(props.id)
|
||||
})
|
||||
|
||||
const updateReactionMutation = useMutation({
|
||||
mutationFn: (update: any) => pb.collection('reactions').update(props.id, update),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['reactions'] }),
|
||||
onError: handleError
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reaction">
|
||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
||||
<div class="flex items-center bg-background px-4 py-2">
|
||||
<div class="ml-auto">
|
||||
<DeleteDialog
|
||||
v-if="reaction"
|
||||
collection="reactions"
|
||||
:id="reaction.id"
|
||||
:name="reaction.name"
|
||||
:singular="'Reaction'"
|
||||
:to="{ name: 'reactions' }"
|
||||
:queryKey="['reactions']"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<ScrollArea v-if="reaction" class="flex-1">
|
||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
||||
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" hide-cancel />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TanView>
|
||||
</template>
|
||||
318
ui/src/components/reaction/ReactionForm.vue
Normal file
318
ui/src/components/reaction/ReactionForm.vue
Normal file
@@ -0,0 +1,318 @@
|
||||
<script setup lang="ts">
|
||||
import ActionPythonFormFields from '@/components/reaction/ActionPythonFormFields.vue'
|
||||
import ActionWebhookFormFields from '@/components/reaction/ActionWebhookFormFields.vue'
|
||||
import TriggerHookFormFields from '@/components/reaction/TriggerHookFormFields.vue'
|
||||
import TriggerWebhookFormFields from '@/components/reaction/TriggerWebhookFormFields.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
import { defineRule, useForm } from 'vee-validate'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { Reaction } from '@/lib/types'
|
||||
|
||||
const submitDisabledReason = ref<string>('')
|
||||
|
||||
const props = defineProps<{
|
||||
reaction?: Reaction
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
defineRule('required', (value: string) => {
|
||||
if (!value || !value.length) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('triggerdata.token', (value: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('triggerdata.path', (value: string) => {
|
||||
if (values.trigger !== 'webhook') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
const expression = /^[a-zA-Z0-9-_]+$/
|
||||
|
||||
if (!value.match(expression)) {
|
||||
return 'Invalid path, only letters, numbers, dashes, and underscores are allowed'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('triggerdata.collections', (value: string[]) => {
|
||||
if (values.trigger !== 'hook') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
if (value.length === 0) {
|
||||
return 'At least one collection is required'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('triggerdata.events', (value: string[]) => {
|
||||
if (values.trigger !== 'hook') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
if (value.length === 0) {
|
||||
return 'At least one event is required'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('actiondata.script', (value: string) => {
|
||||
if (values.action !== 'python') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('actiondata.url', (value: string) => {
|
||||
if (values.action !== 'webhook') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
|
||||
if (!(value.startsWith('http://') || value.startsWith('https://'))) {
|
||||
return 'Invalid URL, must start with http:// or https://'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const { handleSubmit, validate, values } = useForm({
|
||||
initialValues: props.reaction || {
|
||||
name: '',
|
||||
trigger: '',
|
||||
triggerdata: {},
|
||||
action: '',
|
||||
actiondata: {}
|
||||
},
|
||||
validationSchema: {
|
||||
name: 'required',
|
||||
trigger: 'required',
|
||||
'triggerdata.token': 'triggerdata.token',
|
||||
'triggerdata.path': 'triggerdata.path',
|
||||
'triggerdata.collections': 'triggerdata.collections',
|
||||
'triggerdata.events': 'triggerdata.events',
|
||||
'actiondata.script': 'actiondata.script',
|
||||
'actiondata.url': 'actiondata.url',
|
||||
action: 'required'
|
||||
}
|
||||
})
|
||||
|
||||
const equalReaction = (values: Reaction, reaction?: Reaction): boolean => {
|
||||
if (!reaction) return false
|
||||
|
||||
return (
|
||||
reaction.name === values.name &&
|
||||
reaction.trigger === values.trigger &&
|
||||
JSON.stringify(reaction.triggerdata) === JSON.stringify(values.triggerdata) &&
|
||||
reaction.action === values.action &&
|
||||
JSON.stringify(reaction.actiondata) === JSON.stringify(values.actiondata)
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.reaction,
|
||||
() => {
|
||||
if (equalReaction(values, props.reaction)) {
|
||||
submitDisabledReason.value = 'Make changes to save'
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
values,
|
||||
() => {
|
||||
if (equalReaction(values, props.reaction)) {
|
||||
submitDisabledReason.value = 'Make changes to save'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
validate({ mode: 'silent' }).then((res) => {
|
||||
if (res.valid) {
|
||||
submitDisabledReason.value = ''
|
||||
} else {
|
||||
submitDisabledReason.value = 'Please fix the errors'
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const onSubmit = handleSubmit((values) => emit('submit', values))
|
||||
|
||||
const curlExample = computed(() => {
|
||||
let cmd = `curl`
|
||||
|
||||
if (values.triggerdata.token) {
|
||||
cmd += ` -H "Auth: Bearer ${values.triggerdata.token}"`
|
||||
}
|
||||
|
||||
if (values.triggerdata.path) {
|
||||
cmd += ` https://${location.hostname}/reaction/${values.triggerdata.path}`
|
||||
}
|
||||
|
||||
return cmd
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit="onSubmit" class="flex flex-col items-start gap-4">
|
||||
<FormField name="name" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem class="w-full">
|
||||
<FormLabel for="name" class="text-right">Name</FormLabel>
|
||||
<Input id="name" class="col-span-3" v-bind="componentField" />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Trigger</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-4">
|
||||
<FormField name="trigger" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="trigger" class="text-right">Type</FormLabel>
|
||||
<FormControl>
|
||||
<Select id="trigger" class="col-span-3" v-bind="componentField">
|
||||
<SelectTrigger class="font-medium">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="webhook">HTTP / Webhook</SelectItem>
|
||||
<SelectItem value="hook">Collection Hook</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<p>HTTP / Webhook: Receive a HTTP request.</p>
|
||||
<p>Collection Hook: Triggered by a collection and event.</p>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<TriggerWebhookFormFields v-if="values.trigger === 'webhook'" />
|
||||
<TriggerHookFormFields v-else-if="values.trigger === 'hook'" />
|
||||
|
||||
<div v-if="values.trigger === 'webhook'">
|
||||
<Label for="url">Usage</Label>
|
||||
<Input id="url" readonly :modelValue="curlExample" class="bg-accent" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Action</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-4">
|
||||
<FormField name="action" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="action" class="text-right">Type</FormLabel>
|
||||
<FormControl>
|
||||
<Select id="action" class="col-span-3" v-bind="componentField">
|
||||
<SelectTrigger class="font-medium">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="webhook">HTTP / Webhook</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<p>Python: Execute a Python script.</p>
|
||||
<p>HTTP / Webhook: Send an HTTP request.</p>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<ActionPythonFormFields v-if="values.action === 'python'" />
|
||||
<ActionWebhookFormFields v-else-if="values.action === 'webhook'" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger class="cursor-default">
|
||||
<Button
|
||||
type="submit"
|
||||
:variant="submitDisabledReason !== '' ? 'secondary' : 'default'"
|
||||
:disabled="submitDisabledReason !== ''"
|
||||
:title="submitDisabledReason"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span v-if="submitDisabledReason !== ''">
|
||||
{{ submitDisabledReason }}
|
||||
</span>
|
||||
<span v-else> Save the reaction. </span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<slot name="cancel" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
81
ui/src/components/reaction/ReactionList.vue
Normal file
81
ui/src/components/reaction/ReactionList.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
||||
import ReactionNewDialog from '@/components/reaction/ReactionNewDialog.vue'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import type { Reaction } from '@/lib/types'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const {
|
||||
isPending,
|
||||
isError,
|
||||
data: reactions,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['reactions'],
|
||||
queryFn: (): Promise<Array<Reaction>> =>
|
||||
pb.collection('reactions').getFullList({
|
||||
sort: '-created'
|
||||
})
|
||||
})
|
||||
|
||||
const subtitle = (reaction: Reaction) =>
|
||||
triggerNiceName(reaction) + ' to ' + reactionNiceName(reaction)
|
||||
|
||||
const triggerNiceName = (reaction: Reaction) => {
|
||||
if (reaction.trigger === 'hook') {
|
||||
return 'Collection Hook'
|
||||
} else if (reaction.trigger === 'webhook') {
|
||||
return 'HTTP / Webhook'
|
||||
} else {
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const reactionNiceName = (reaction: Reaction) => {
|
||||
if (reaction.action === 'python') {
|
||||
return 'Python'
|
||||
} else if (reaction.action === 'webhook') {
|
||||
return 'HTTP / Webhook'
|
||||
} else {
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reactions">
|
||||
<div class="flex h-screen flex-col">
|
||||
<div class="flex items-center bg-background px-4 py-2">
|
||||
<h1 class="text-xl font-bold">Reactions</h1>
|
||||
<div class="ml-auto">
|
||||
<ReactionNewDialog />
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="mt-2 flex flex-1 flex-col gap-2 p-4 pt-0">
|
||||
<TransitionGroup name="list" appear>
|
||||
<ResourceListElement
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.id"
|
||||
:title="reaction.name"
|
||||
:created="reaction.created"
|
||||
:subtitle="subtitle(reaction)"
|
||||
description=""
|
||||
:active="route.params.id === reaction.id"
|
||||
:to="{ name: 'reactions', params: { id: reaction.id } }"
|
||||
:open="false"
|
||||
>
|
||||
{{ reaction.name }}
|
||||
</ResourceListElement>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</TanView>
|
||||
</template>
|
||||
63
ui/src/components/reaction/ReactionNewDialog.vue
Normal file
63
ui/src/components/reaction/ReactionNewDialog.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogScrollContent,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import type { Reaction, Ticket } from '@/lib/types'
|
||||
import { handleError } from '@/lib/utils'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const addReactionMutation = useMutation({
|
||||
mutationFn: (values: Reaction): Promise<Reaction> => pb.collection('reactions').create(values),
|
||||
onSuccess: (data: Ticket) => {
|
||||
router.push({ name: 'reactions', params: { id: data.id } })
|
||||
queryClient.invalidateQueries({ queryKey: ['reactions'] })
|
||||
isOpen.value = false
|
||||
},
|
||||
onError: handleError
|
||||
})
|
||||
|
||||
const cancel = () => (isOpen.value = false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="ghost">New Reaction</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Reaction</DialogTitle>
|
||||
<DialogDescription>Create a new reaction</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogScrollContent>
|
||||
<ReactionForm @submit="addReactionMutation.mutate">
|
||||
<template #cancel>
|
||||
<DialogClose as-child>
|
||||
<Button type="button" variant="secondary">Cancel</Button>
|
||||
</DialogClose>
|
||||
</template>
|
||||
</ReactionForm>
|
||||
</DialogScrollContent>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import MultiSelect from '@/components/form/MultiSelect.vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
const modelValue = defineModel<string[]>({
|
||||
default: []
|
||||
})
|
||||
|
||||
const items = ['Tickets', 'Tasks', 'Comments', 'Timeline', 'Links', 'Files']
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
tickets: 'Tickets',
|
||||
tasks: 'Tasks',
|
||||
comments: 'Comments',
|
||||
timeline: 'Timeline',
|
||||
links: 'Links',
|
||||
files: 'Files'
|
||||
}
|
||||
|
||||
const niceNames = computed(() => modelValue.value.map((collection) => mapping[collection]))
|
||||
|
||||
const updateModelValue = (values: string[]) => {
|
||||
modelValue.value = values.map(
|
||||
(value) => Object.keys(mapping).find((key) => mapping[key] === value)!
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MultiSelect
|
||||
:modelValue="niceNames"
|
||||
@update:modelValue="updateModelValue"
|
||||
:items="items"
|
||||
placeholder="Select collections..."
|
||||
/>
|
||||
</template>
|
||||
36
ui/src/components/reaction/TriggerHookFormFieldEvents.vue
Normal file
36
ui/src/components/reaction/TriggerHookFormFieldEvents.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import MultiSelect from '@/components/form/MultiSelect.vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
const modelValue = defineModel<string[]>({
|
||||
default: []
|
||||
})
|
||||
|
||||
const items = ['Create Events', 'Update Events', 'Delete Events']
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
create: 'Create Events',
|
||||
update: 'Update Events',
|
||||
delete: 'Delete Events'
|
||||
}
|
||||
|
||||
const niceNames = computed(() =>
|
||||
modelValue.value.map((collection) => mapping[collection] as string)
|
||||
)
|
||||
|
||||
const updateModelValue = (values: string[]) => {
|
||||
modelValue.value = values.map(
|
||||
(value) => Object.keys(mapping).find((key) => mapping[key] === value)!
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MultiSelect
|
||||
:modelValue="niceNames"
|
||||
@update:modelValue="updateModelValue"
|
||||
:items="items"
|
||||
placeholder="Select events..."
|
||||
/>
|
||||
</template>
|
||||
36
ui/src/components/reaction/TriggerHookFormFields.vue
Normal file
36
ui/src/components/reaction/TriggerHookFormFields.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import TriggerHookFormFieldCollections from '@/components/reaction/TriggerHookFormFieldCollections.vue'
|
||||
import TriggerHookFormFieldEvents from '@/components/reaction/TriggerHookFormFieldEvents.vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField name="triggerdata.collections" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="collections" class="text-right">Collections</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerHookFormFieldCollections id="collections" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription> Specify the collections to trigger the reaction. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField name="triggerdata.events" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="events" class="text-right">Events</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerHookFormFieldEvents id="events" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription> Specify the events to trigger the reaction. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
47
ui/src/components/reaction/TriggerWebhookFormFields.vue
Normal file
47
ui/src/components/reaction/TriggerWebhookFormFields.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField name="triggerdata.token" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="token" class="text-right">Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="token"
|
||||
class="col-span-3"
|
||||
v-bind="componentField"
|
||||
placeholder="Enter a token (e.g. 'xyz...')"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. Include an authorization token in the request headers.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField name="triggerdata.path" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="path" class="text-right">Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="path"
|
||||
class="col-span-3"
|
||||
v-bind="componentField"
|
||||
placeholder="Enter a path (e.g. 'action1')"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription> Specify the path to trigger the reaction. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
Reference in New Issue
Block a user