718 lines
15 KiB
Vue
718 lines
15 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> {{ 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>
|
|
|
|
<template v-if="stashes.length < user.stashes.length">
|
|
<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.filter((stash) => !stashes.some((selectedStash) => selectedStash.id === stash.id))"
|
|
: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>
|
|
</template>
|
|
</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}*`, // return partial matches
|
|
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(selectedStash) {
|
|
if (!stashes.value.some((stash) => stash.id === selectedStash.id)) {
|
|
stashes.value.push(selectedStash);
|
|
}
|
|
}
|
|
</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(--glass);
|
|
|
|
&.item-logic {
|
|
width: 2.75rem;
|
|
}
|
|
|
|
.icon {
|
|
fill: var(--glass);
|
|
}
|
|
}
|
|
|
|
.field-items {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
flex-grow: 1;
|
|
gap: .5rem 0;
|
|
padding: .5rem 0;
|
|
border-bottom: solid 1px var(--glass-weak-40);
|
|
|
|
&.or .field-item::before,
|
|
&.and .field-item::before {
|
|
color: var(--glass);
|
|
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(--glass);
|
|
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(--glass-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>
|