ytptube/ui/app/components/NotificationForm.vue

491 lines
20 KiB
Vue

<template>
<main class="columns mt-2 is-multiline">
<div class="column is-12">
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
<div class="card">
<div class="card-header">
<div class="card-header-title is-text-overflow is-block">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
<span>{{ reference ? 'Edit' : 'Add' }}</span>
</span>
</div>
<div class="card-header-icon" v-if="reference">
<button type="button" @click="showImport = !showImport">
<span class="icon"><i class="fa-solid" :class="{
'fa-arrow-down': !showImport,
'fa-arrow-up': showImport,
}" /></span>
<span>
<span v-if="showImport">Hide</span>
<span v-else>Show</span>
import
</span>
</button>
</div>
</div>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12" v-if="showImport || !reference">
<label class="label is-inline" for="import_string">
Import string
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
<span class="icon"><i class="fa-solid fa-add" /></span>
<span>Import</span>
</button>
</div>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>You can use this field to populate the data, using shared string.</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name">
Target name
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The notification target name, this is used to identify the target in the logs and
notifications.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="url">
Target URL
</label>
<div class="control has-icons-left">
<input type="url" class="input" id="url" v-model="form.request.url" :disabled="addInProgress"
required>
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The URL to send the notification to. It can be regular http/https endpoint. or <NuxtLink
target="blank" href="https://github.com/caronc/apprise?tab=readme-ov-file#readme">Apprise
</NuxtLink> URL.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="!isAppriseTarget">
<div class="field">
<label class="label is-inline" for="method">
Request method
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="method" class="is-fullwidth" v-model="form.request.method" :disabled="addInProgress">
<option v-for="rMethod, index in requestMethods" :key="`${index}-${rMethod}`" :value="rMethod">
{{ rMethod }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request method to use when sending the notification. This can be any of the standard HTTP
methods.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="!isAppriseTarget">
<div class="field">
<label class="label is-inline" for="type">
Request Type
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="type" class="is-fullwidth" v-model="form.request.type" :disabled="addInProgress">
<option v-for="rType, index in requestType" :key="`${index}-${rType}`" :value="rType">
{{ ucFirst(rType) }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request type to use when sending the notification. This can be <code>JSON</code> or
<code>FORM</code> request.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="on">
Select Events
<template v-if="form.on.length > 0">
- <NuxtLink @click="form.on = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.on" :disabled="addInProgress" multiple>
<option v-for="aEvent, index in allowedEvents" :key="`${index}-${aEvent}`" :value="aEvent">
{{ aEvent }}
</option>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will
be sent to the target URL. If no events are selected, the notification will be sent for all
events.
</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="on">
Select Presets
<template v-if="form.presets.length > 0">
- <NuxtLink @click="form.presets = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.presets" :disabled="addInProgress" multiple>
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
<option v-for="cPreset in filter_presets(false)" :key="cPreset.id" :value="cPreset.name">
{{ cPreset.name }}
</option>
</optgroup>
<optgroup label="Default presets">
<option v-for="dPreset in filter_presets(true)" :key="dPreset.id" :value="dPreset.name">
{{ dPreset.name }}
</option>
</optgroup>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-sliders" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Select the presets you want to listen for. If you select presets, only events that reference those
presets will trigger the notification. If no presets are selected, the notification will be sent
for all presets.
</span>
</span>
</div>
</div>
<div class="column" :class="isAppriseTarget ? 'is-12' : 'is-6-tablet is-12-mobile'">
<div class="field">
<label class="label is-inline" for="enabled">
<span class="icon"><i class="fa-solid fa-power-off" /></span>
Enabled
</label>
<div class="control is-unselectable">
<input id="enabled" type="checkbox" v-model="form.enabled" :disabled="addInProgress"
class="switch is-success" />
<label for="enabled" class="is-unselectable">
{{ form.enabled ? 'Yes' : 'No' }}
</label>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">Whether the notification target is enabled.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="!isAppriseTarget">
<div class="field">
<label class="label is-inline" for="data_key">
Data field
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="data_key" v-model="form.request.data_key"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The field name to use when sending the notification. This is used to identify the data in the
request. The default is <code>data</code>.
</span>
</span>
</div>
</div>
<div class="column is-12" v-if="!isAppriseTarget">
<div class="field">
<label class="label is-inline is-unselectable">
Optional Headers - <button type="button" class="has-text-link"
@click="form.request.headers.push({ key: '', value: '' });">Add Header
</button>
</label>
<div class="columns is-multiline is-mobile">
<template v-for="_, key in form.request.headers" :key="key">
<div class="column is-5" v-if="form.request.headers[key]">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].key"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header key to send with the notification.</span>
</span>
</div>
<div class="column is-6" v-if="form.request.headers[key]">
<div class="field">
<div class="control has-icons-left">
<input type="text" class="input" v-model="form.request.headers[key].value"
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
</div>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header value to send with the notification.</span>
</span>
</div>
<div class="column is-1">
<div class="control">
<button type="button" class="button is-danger" @click="form.request.headers.splice(key, 1)"
:disabled="addInProgress">
<span class="icon"><i class="fa-solid fa-trash" /></span>
</button>
</div>
</div>
</template>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
<span class="has-text-danger">
If header key or value is empty, the header will not be sent.
</span>
</span>
</div>
</div>
</div>
<div class="card-footer">
<p class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="taskForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</p>
<p class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</p>
</div>
</div>
</div>
</form>
</div>
</main>
</template>
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import type { notification } from '~/types/notification'
import { useConfirm } from '~/composables/useConfirm'
import { useNotification } from '~/composables/useNotification'
import type { ImportedItem } from '~/types'
const emitter = defineEmits(['cancel', 'submit'])
const toast = useNotification()
const box = useConfirm()
const config = useConfigStore()
const { isApprise } = useNotifications()
const props = defineProps({
reference: {
type: Number,
required: false,
default: null,
},
allowedEvents: {
type: Array as () => readonly string[],
required: true,
},
item: {
type: Object as () => notification,
required: true,
},
addInProgress: {
type: Boolean,
required: false,
default: false,
},
})
const form = reactive<notification>({ ...props.item })
const requestMethods = ['POST', 'PUT']
const requestType = ['json', 'form']
const showImport = useStorage('showImport', false)
const import_string = ref('')
onMounted(() => {
if (!form.request.data_key) {
form.request.data_key = 'data'
}
if (form.enabled === undefined) {
form.enabled = true
}
})
const checkInfo = async () => {
let required: string[]
if (!isAppriseTarget.value) {
required = ['name', 'request.url', 'request.method', 'request.type', 'request.data_key']
} else {
required = ['name', 'request.url']
}
for (const key of required) {
if (key.includes('.')) {
const [parent, child] = key.split('.') as [keyof typeof form, string]
const parentObj = form[parent] as Record<string, any> | undefined
if (!parentObj || !parentObj[child]) {
toast.error(`The field ${parent}.${child} is required.`)
return
}
} else {
const value = (form as Record<string, any>)[key]
if (!value) {
toast.error(`The field ${key} is required.`)
return
}
}
}
if (!isAppriseTarget.value) {
try {
new URL(form.request.url)
} catch {
toast.error('Invalid URL')
return
}
}
const headers = []
for (const header of form.request.headers) {
if (!header.key || !header.value) {
continue
}
headers.push({ key: String(header.key).trim(), value: String(header.value).trim() })
}
form.request.headers = headers
emitter('submit', { reference: toRaw(props.reference), item: toRaw(form) })
}
const importItem = async () => {
const val = import_string.value.trim()
if (!val) {
toast.error('The import string is required.')
return
}
try {
const item = decode(val) as notification & ImportedItem
if ('notification' !== item._type) {
toast.error(`Invalid import string. Expected type 'notification', got '${item._type}'.`)
import_string.value = ''
return
}
if (form.name || form.request?.url) {
if (false === (await box.confirm('Overwrite the current form fields?'))) {
return
}
}
if (item.name) {
form.name = item.name
}
if (!form.request) {
form.request = {} as any
}
if (item.request) {
form.request = item.request
}
if (item.request?.data_key) {
form.request.data_key = item.request.data_key
}
if (item.on) {
form.on = item.on
}
if (item.presets) {
item.presets.forEach(p => {
if (!config.presets.find(cp => cp.name === p)) {
return
}
if (!form.presets.includes(p)) {
form.presets.push(p)
}
})
}
if (item.enabled !== undefined) {
form.enabled = item.enabled
}
import_string.value = ''
} catch (e: any) {
console.error(e)
toast.error(`Failed to import task. ${e.message}`)
}
}
const isAppriseTarget = computed(() => form.request.url && isApprise(form.request.url))
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
</script>