traxxx-web/components/alerts/create.vue

714 lines
14 KiB
Vue

<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].filter(Boolean).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>&nbsp;{{ 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>