Added remaining elements to alert dialog.
This commit is contained in:
711
components/alerts/create.vue
Normal file
711
components/alerts/create.vue
Normal file
@@ -0,0 +1,711 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="New alert"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<form
|
||||
class="dialog-body"
|
||||
@submit.prevent="createAlert"
|
||||
>
|
||||
<div class="dialog-section">
|
||||
<div class="section-header">
|
||||
<h3 class="heading">IF</h3>
|
||||
</div>
|
||||
|
||||
<div class="field actors">
|
||||
<span
|
||||
v-tooltip="fieldsAnd ? 'The alert is triggered if all fields are matched.' : 'The alert is triggered if any of the fields are matched.'"
|
||||
class="field-logic fields-logic noselect"
|
||||
@click="fieldsAnd = !fieldsAnd"
|
||||
>
|
||||
<Icon
|
||||
v-show="fieldsAnd"
|
||||
icon="link3"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="!fieldsAnd"
|
||||
icon="unlink3"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<ul
|
||||
class="field-items nolist noselect"
|
||||
:class="{ and: actorAnd, or: !actorAnd }"
|
||||
>
|
||||
<li
|
||||
v-for="(actor, index) in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="field-item actor"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="field-logic item-logic"
|
||||
@click="actorAnd = !actorAnd"
|
||||
>{{ actorAnd ? 'AND' : 'OR' }}</div>
|
||||
|
||||
<div class="field-tile">
|
||||
<div class="field-label">{{ actor.name }}</div>
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="field-remove"
|
||||
@click="actors = actors.filter((selectedActor) => selectedActor.id !== actor.id)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown @show="focusActorInput">
|
||||
<button
|
||||
class="button"
|
||||
type="button"
|
||||
><Icon icon="plus3" />Add actor</button>
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="actorInput"
|
||||
v-model="actorQuery"
|
||||
class="input"
|
||||
@input="searchActors"
|
||||
>
|
||||
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="actor in actorResults"
|
||||
:key="`actor-result-${actor.id}`"
|
||||
v-close-popper
|
||||
class="result-item"
|
||||
@click="selectActor(actor)"
|
||||
>
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
class="field-avatar"
|
||||
:src="getPath(actor.avatar, 'lazy')"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="field-avatar"
|
||||
/>
|
||||
|
||||
<div class="result-label">
|
||||
{{ actor.name }}
|
||||
<template v-if="actor.ageFromBirth || actor.origin?.country">({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].join(', ') }})</template>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field tags">
|
||||
<span
|
||||
class="field-logic noselect"
|
||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||
|
||||
<ul
|
||||
class="field-items nolist noselect"
|
||||
:class="{ and: actorAnd, or: !actorAnd }"
|
||||
>
|
||||
<li
|
||||
v-for="(tag, index) in tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="field-item tag"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="field-logic item-logic"
|
||||
@click="tagAnd = !tagAnd"
|
||||
>{{ tagAnd ? 'AND' : 'OR' }}</div>
|
||||
|
||||
<div class="field-tile field-label">{{ tag.name }}</div>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="field-remove"
|
||||
@click="tags = tags.filter((selectedTag) => selectedTag.id !== tag.id)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
><Icon icon="plus3" />Add tag</button>
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="tagInput"
|
||||
v-model="tagQuery"
|
||||
class="input"
|
||||
@input="searchTags"
|
||||
>
|
||||
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="tag in tagResults"
|
||||
:key="`tag-result-${tag.id}`"
|
||||
v-close-popper
|
||||
class="result-item result-label"
|
||||
@click="selectTag(tag)"
|
||||
>{{ tag.name }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field entities">
|
||||
<span
|
||||
class="field-logic noselect"
|
||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||
|
||||
<ul class="field-items nolist noselect">
|
||||
<li
|
||||
v-for="(entity, index) in entities"
|
||||
:key="`entity-${entity.id}`"
|
||||
class="field-item entity"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
v-tooltip.click="{
|
||||
content: 'Scenes are only associated to one channel, \'AND\' would never match.',
|
||||
triggers: ['click'],
|
||||
autoHide: true,
|
||||
}"
|
||||
class="field-logic"
|
||||
>OR</div>
|
||||
|
||||
<div class="field-tile field-label">
|
||||
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
||||
{{ entity.name }}
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="field-remove"
|
||||
@click="entities = entities.filter((selectedEntity) => selectedEntity.id !== entity.id)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
><Icon icon="plus3" />Add channel</button>
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="entityInput"
|
||||
v-model="entityQuery"
|
||||
class="input"
|
||||
@input="searchEntities"
|
||||
>
|
||||
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="entity in entityResults"
|
||||
:key="`entity-result-${entity.id}`"
|
||||
v-close-popper
|
||||
class="result-item result-label"
|
||||
@click="selectEntity(entity)"
|
||||
>
|
||||
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
||||
|
||||
{{ entity.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field matches">
|
||||
<span
|
||||
class="field-logic noselect"
|
||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||
|
||||
<ul class="field-items nolist noselect">
|
||||
<li
|
||||
v-for="(match, index) in matches"
|
||||
:key="`match-${match.property}-${match.expression}`"
|
||||
class="field-item match"
|
||||
>
|
||||
<div
|
||||
v-if="index > 0"
|
||||
class="field-logic item-logic"
|
||||
@click="matchAnd = !matchAnd"
|
||||
>{{ matchAnd ? 'AND' : 'OR' }}</div>
|
||||
|
||||
<div class="field-tile field-label">
|
||||
<strong>{{ match.property }}:</strong> {{ match.expression }}
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="field-remove"
|
||||
@click="matches = matches.filter((selectedEntity, selectedIndex) => selectedIndex !== index)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
><Icon icon="plus3" />Add expression</button>
|
||||
|
||||
<template #popper>
|
||||
<form @submit.prevent="addMatch">
|
||||
<select
|
||||
v-model="matchProperty"
|
||||
class="input"
|
||||
>
|
||||
<option value="title">Title</option>
|
||||
<option value="description">Description</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-model="matchExpression"
|
||||
placeholder="Expression, // for regex"
|
||||
class="input"
|
||||
>
|
||||
</form>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section then">
|
||||
<h3 class="heading">THEN</h3>
|
||||
|
||||
<label class="field notify">
|
||||
<span>Notify me in traxxx</span>
|
||||
<Checkbox
|
||||
:checked="notify"
|
||||
@change="(checked) => notify = checked"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field email">
|
||||
<span>E-mail me</span>
|
||||
|
||||
<Checkbox
|
||||
:checked="email"
|
||||
@change="(checked) => email = checked"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="stash">
|
||||
<ul class="field-items nolist noselect">
|
||||
<li
|
||||
v-for="stash in stashes"
|
||||
:key="`stash-${stash.id}`"
|
||||
class="field-item tag"
|
||||
>
|
||||
<div class="field-tile field-label stash">
|
||||
<Icon
|
||||
v-if="stash.isPrimary"
|
||||
class="favorites"
|
||||
icon="heart7"
|
||||
/>{{ stash.name }}
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="field-remove"
|
||||
@click="stashes = stashes.filter((selectedStash) => selectedStash.id !== stash.id)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<button
|
||||
v-if="stashes.length === 0"
|
||||
type="button"
|
||||
class="button favorites"
|
||||
@click="selectStash(user.primaryStash)"
|
||||
><Icon icon="heart7" />Add to favorites</button>
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<button
|
||||
type="button"
|
||||
class="button field-add"
|
||||
><Icon icon="folder-heart" />Add to stash</button>
|
||||
|
||||
<template #popper>
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="stash in user.stashes"
|
||||
:key="`stash-result-${stash.id}`"
|
||||
v-close-popper
|
||||
class="result-item result-stash result-label"
|
||||
@click="selectStash(stash)"
|
||||
>
|
||||
{{ stash.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section dialog-actions">
|
||||
<button
|
||||
class="button button-submit"
|
||||
>Set alert</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
const { user } = inject('pageContext');
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const actors = ref([]);
|
||||
const actorResults = ref([]);
|
||||
const actorInput = ref(null);
|
||||
const actorQuery = ref('');
|
||||
|
||||
const entities = ref([]);
|
||||
const entityResults = ref([]);
|
||||
const entityInput = ref(null);
|
||||
const entityQuery = ref('');
|
||||
|
||||
const tags = ref([]);
|
||||
const tagResults = ref([]);
|
||||
const tagInput = ref(null);
|
||||
const tagQuery = ref('');
|
||||
|
||||
const matches = ref([]);
|
||||
const matchProperty = ref('title');
|
||||
const matchExpression = ref('');
|
||||
|
||||
const fieldsAnd = ref(true);
|
||||
const actorAnd = ref(true);
|
||||
const tagAnd = ref(true);
|
||||
const matchAnd = ref(true);
|
||||
|
||||
const notify = ref(true);
|
||||
const email = ref(false);
|
||||
|
||||
const stashes = ref([]);
|
||||
|
||||
async function createAlert() {
|
||||
console.log('creating alert');
|
||||
|
||||
await post('/alerts', {
|
||||
all: fieldsAnd.value,
|
||||
allActors: actorAnd.value,
|
||||
allTags: tagAnd.value,
|
||||
allMatches: matchAnd.value,
|
||||
actors: actors.value.map((actor) => actor.id),
|
||||
tags: tags.value.map((tag) => tag.id),
|
||||
matches: matches.value,
|
||||
entities: entities.value.map((entity) => entity.id),
|
||||
notify: notify.value,
|
||||
email: email.value,
|
||||
stashes: stashes.value.map((stash) => stash.id),
|
||||
}, { appendErrorMessage: true });
|
||||
|
||||
emit('close', true);
|
||||
}
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: actorQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
async function searchEntities() {
|
||||
const res = await get('/entities', {
|
||||
query: entityQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
entityResults.value = res;
|
||||
}
|
||||
|
||||
async function searchTags() {
|
||||
const res = await get('/tags', {
|
||||
query: tagQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
tagResults.value = res;
|
||||
}
|
||||
|
||||
function focusActorInput() {
|
||||
setTimeout(() => {
|
||||
console.log(actorInput.value);
|
||||
actorInput.value.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
actors.value.push(actor);
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
function selectEntity(entity) {
|
||||
entities.value.push(entity);
|
||||
entityQuery.value = '';
|
||||
entityResults.value = [];
|
||||
}
|
||||
|
||||
function selectTag(tag) {
|
||||
tags.value.push(tag);
|
||||
tagQuery.value = '';
|
||||
tagResults.value = [];
|
||||
}
|
||||
|
||||
function addMatch() {
|
||||
matches.value.push({
|
||||
property: matchProperty.value,
|
||||
expression: matchExpression.value,
|
||||
});
|
||||
|
||||
matchProperty.value = 'title';
|
||||
matchExpression.value = '';
|
||||
}
|
||||
|
||||
function selectStash(stash) {
|
||||
stashes.value.push(stash);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 30rem;
|
||||
max-width: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-section {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.heading {
|
||||
width: 100%;
|
||||
color: var(--primary);
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
.field-add .button {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.field-add:not(:first-child) {
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
.fields-logic:hover,
|
||||
.item-logic:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.field-logic {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 3.5rem;
|
||||
font-size: .9rem;
|
||||
color: var(--shadow);
|
||||
|
||||
&.item-logic {
|
||||
width: 2.75rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.field-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
gap: .5rem 0;
|
||||
padding: .5rem 0;
|
||||
border-bottom: solid 1px var(--shadow-weak-40);
|
||||
|
||||
&.or .field-item::before,
|
||||
&.and .field-item::before {
|
||||
color: var(--shadow);
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
&.or .field-item:not(:first-child)::before {
|
||||
content: 'OR';
|
||||
}
|
||||
|
||||
&.and .field-item:not(:first-child)::before {
|
||||
content: 'AND';
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.field-remove {
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
position: absolute;
|
||||
top: -.5rem;
|
||||
right: -.5rem;
|
||||
padding: .2rem;
|
||||
border-radius: 1rem;
|
||||
fill: var(--shadow);
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--error);
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.field-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
padding: .5rem .5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field-avatar {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 2rem;
|
||||
object-fit: cover;
|
||||
object-position: center 0;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.field-avatar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.result-label {
|
||||
padding: .25rem .5rem;
|
||||
}
|
||||
|
||||
.then {
|
||||
.field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1rem;
|
||||
border-bottom: solid 1px var(--shadow-weak-40);
|
||||
}
|
||||
|
||||
.field-items {
|
||||
padding: .5rem 1rem;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.field-add {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.result-stash {
|
||||
padding: .3rem .5rem .3rem .5rem;
|
||||
|
||||
&:first-child {
|
||||
padding-top: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.field-tile .icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.field-tile.stash {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field.notify,
|
||||
.field.email {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user