<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 < assets.stashes.length"> <li class="field-add"> <button v-if="stashes.length === 0" type="button" class="button favorites" @click="selectStash(assets.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 assets.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 { assets } = 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() { 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>