598 lines
22 KiB
Vue
598 lines
22 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>
|