<template> <Dialog title="Edit summary template" :confirm-close="hasChanged" > <div class="dialog-body"> <ul class="templates nolist"> <li v-for="storedTemplate in templates" :key="`template-${storedTemplate.id}`" class="template-key" :class="{ selected: selectedTemplate === storedTemplate.id }" @click="selectTemplate(storedTemplate.id)" >{{ storedTemplate.name }}</li> <li class="template-key add" @click="add" > <Icon icon="plus3" /> </li> </ul> <textarea ref="input" v-model="template" height="3" class="input edit" @input="update" /> <textarea :value="summary" class="input summary" :class="{ error: hasError }" wrap="soft" @click="$event.target.select()" /> <div class="dialog-actions"> <div class="actions"> <button class="button" @click="copy" >Copy</button> <button class="button" @click="reset" >Default</button> </div> <form class="actions save" @submit.prevent="save" > <Icon v-if="selectedTemplate" icon="bin" class="remove" @click="remove" /> <input v-model="templateName" class="input" placeholder="Name" required > <button class="button" >Save</button> </form> </div> </div> </Dialog> </template> <script setup> import { ref, inject } from 'vue'; import { parse } from 'yaml'; import Cookies from 'js-cookie'; // import slugify from '#/utils/slugify.js'; import events from '#/src/events.js'; import { get, post, del } from '#/src/api.js'; import processSummaryTemplate from '#/utils/process-summary-template.js'; import Dialog from '#/components/dialog/dialog.vue'; import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved const emit = defineEmits(['event']); const pageContext = inject('pageContext'); const cookies = Cookies.withConverter({ write: (value) => value, }); const props = defineProps({ release: { type: Object, default: null, }, selected: { type: Number, default: null, }, }); const templates = ref(pageContext.assets.templates); const selectedTemplate = ref(props.selected || templates.value.at(0)?.id || null); const initialTemplate = templates.value.find((storedTemplate) => storedTemplate.id === selectedTemplate.value) || null; const template = ref(initialTemplate?.template || defaultTemplate); const hasError = ref(false); const hasChanged = ref(false); const input = ref(null); const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`); function getSummary() { return processSummaryTemplate(template.value, props.release); } const summary = ref(getSummary()); function selectTemplate(templateId) { selectedTemplate.value = templateId; const nextTemplate = templates.value.find((storedTemplate) => storedTemplate.id === templateId); template.value = nextTemplate.template; templateName.value = nextTemplate.name; summary.value = getSummary(); emit('event', { type: 'select', data: templateId, }); cookies.set('selectedTemplate', String(templateId)); } function update() { hasError.value = false; hasChanged.value = true; try { summary.value = getSummary(); } catch (error) { hasError.value = true; } } async function save() { try { parse(template.value); hasChanged.value = false; const createdTemplate = await post('/templates', { name: templateName.value, template: template.value, successFeedback: `Saved summary template '${templateName.value}'`, errorFeedback: `Failed to save summary template '${templateName.value}'`, }); templates.value = await get(`/users/${pageContext.user.id}/templates`); selectTemplate(createdTemplate.id); } catch (error) { events.emit('feedback', { type: 'error', message: `Failed to save summary template '${templateName.value}': ${error.message}`, }); } } function add() { selectedTemplate.value = null; template.value = ''; templateName.value = `custom_${Date.now()}`; summary.value = ''; input.value.focus(); } async function remove() { if (confirm(`Are you sure you want to delete summary template ${templateName.value}?`)) { // eslint-disable-line no-restricted-globals, no-alert await del(`/templates/${selectedTemplate.value}`, { undoFeedback: `Deleted summary template '${templateName.value}'`, errorFeedback: `Failed to remove summary template '${templateName.value}'`, }); templates.value = await get(`/users/${pageContext.user.id}/templates`); selectTemplate(templates.value.at(-1)?.id); } } function copy() { navigator.clipboard.writeText(summary.value); events.emit('feedback', { type: 'success', message: 'Summary copied to clipboard', }); } function reset() { if (confirm('Are you sure you want to reset the summary template to the default? Your custom template will be discarded.')) { // eslint-disable-line no-restricted-globals, no-alert template.value = defaultTemplate; update(); events.emit('feedback', { type: 'undo', message: 'Reset summary template', }); } } </script> <style scoped> :deep(.dialog-container .dialog) { background: red; } .dialog-body { display: flex; width: 50rem; max-width: 100%; height: 100vh; } .input { resize: none; } .edit { flex-grow: 1; } .summary { min-height: 4rem; flex-shrink: 0; line-height: 1.5; &.error { background: var(--background-error); } } .dialog-actions { display: flex; justify-content: space-between;; gap: 1rem; padding: 1rem; .input { flex-grow: 1; width: 0; max-width: 10rem; } } .actions { display: flex; .button:not(:last-child) { margin-right: 1rem; } &.save { flex-grow: 1; justify-content: flex-end; .input { margin-right: 1rem; } } .icon { height: 100%; padding: 0 1rem; cursor: pointer; fill: var(--glass); &.remove:hover { fill: var(--error); } } } .templates { display: flex; align-items: center; padding: .5rem; overflow-x: auto; } .template-key { padding: .25rem .5rem; border-radius: .25rem; cursor: pointer; .icon { fill: var(--glass); } &.selected { background: var(--primary); color: var(--text-light); } &:hover:not(.selected) { color: var(--primary); .icon { fill: var(--primary); } } } </style>