<template> <div class="editor"> <div class="editor-header"> <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> </ul> <div class="template-add" @click="add" > <button class="button"> <Icon icon="file-plus2" /> <span class="button-label">New template</span> </button> </div> </div> <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" /> <div class="actions-save"> <input v-model="templateName" class="input" placeholder="Name" required > <button class="button" >Save</button> </div> </form> </div> </div> </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 defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved const emit = defineEmits(['event', 'changed']); const pageContext = inject('pageContext'); const cookies = Cookies.withConverter({ write: (value) => value, }); const props = defineProps({ release: { type: Object, default: null, }, }); const templates = ref(pageContext.assets.templates); const selectedTemplate = ref(Number(pageContext.urlParsed.search.t) || 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 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; emit('changed', true); try { summary.value = getSummary(); } catch (error) { hasError.value = true; } } async function save() { try { parse(template.value); emit('changed', 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> .editor { display: flex; flex-grow: 1; flex-direction: column; max-width: 100%; } .input { resize: none; background: var(--background); } .edit { flex-grow: 1; min-height: 10rem; resize: vertical; } .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; } } .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); } } } .actions-save { display: flex; width: 15rem; } .editor-header { display: flex; align-items: center; justify-content: space-between; padding: .5rem 0; } .templates { display: flex; align-items: center; overflow-x: auto; } .template-key { padding: .5rem .75rem; border-radius: .5rem; cursor: pointer; color: var(--glass-strong-20); font-size: .9rem; font-weight: bold; .icon { fill: var(--glass); } &.selected { background: var(--primary); color: var(--text-light); } &:hover:not(.selected) { color: var(--primary); .icon { fill: var(--primary); } } } .template-add { font-size: 0; /* prevent icon jump */ margin-left: .5rem; } @media(--small-30) { .dialog-actions { flex-direction: column; } .actions, .actions.save { justify-content: space-between; } } </style>