Skip to content
Snippets Groups Projects
Commit 57e1e10f authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt :koala:
Browse files

refactor: re-implement note editor modal

closes #141
parent b4d84d1a
No related branches found
No related tags found
No related merge requests found
<template>
<ADialog
v-model="localIsOpen"
is-modal
title="Edit Note"
class="tw-w-screen lg:tw-w-[60vw] tw-max-w-[800px]"
>
<template #default>
<FormTable>
<FormGroup :label="t('noteEditor.title')" :errors="errors.title">
<template #default="attrs">
<input
v-model="noteData.title"
class="form-control"
:placeholder="t('noteEditor.titlePlaceholder')"
required
v-bind="attrs"
/>
<p v-if="noteData.slug" class="tw-text-xs tw-text-gray-400 tw-mt-1 tw-mb-0">
{{ t('slug') }}: {{ noteData.slug }}
</p>
</template>
</FormGroup>
<FormGroup :label="t('noteEditor.summary')" :errors="errors.summary">
<template #default="attrs">
<textarea
ref="summaryEl"
v-model="noteData.summary"
class="tw-resize-none tw-overflow-hidden"
:placeholder="t('noteEditor.summaryPlaceholder')"
v-bind="attrs"
/>
</template>
</FormGroup>
<FormGroup :label="t('noteEditor.content')" :errors="errors.content">
<template #default="attrs">
<textarea
ref="contentEl"
v-model="noteData.content"
class="tw-resize-none tw-overflow-hidden"
:placeholder="t('noteEditor.contentPlaceholder')"
required
v-bind="attrs"
/>
</template>
</FormGroup>
<FormGroup :label="t('noteEditor.image')" :errors="errors.image">
<ImagePicker v-model="noteData.image" class="tw-flex-none tw-w-min" />
</FormGroup>
</FormTable>
</template>
<template #footer>
<div class="tw-flex tw-items-center tw-gap-3">
<button type="button" class="btn btn-default" @click.stop="emit('show', false)">
{{ t('cancel') }}
</button>
<button type="button" class="btn btn-primary" @click.stop="save">
{{ t('noteEditor.save') }}
</button>
</div>
</template>
</ADialog>
</template>
<script lang="ts" setup>
import { computed, ref, toRefs, watchEffect } from 'vue'
import { useTextareaAutosize } from '@vueuse/core'
import { useAPIObject, useServerErrors } from '@/api'
import { useI18n } from '@/i18n'
import { useUpdatableState } from '@/util'
import { slugify } from '@/mixins/slugify'
import { newNote, NewNote, Note, useNoteStore } from '@/stores/notes'
import ADialog from '@/components/generic/ADialog.vue'
import FormGroup from '@/components/generic/FormGroup.vue'
import FormTable from '@/components/generic/FormTable.vue'
import ImagePicker from '@/components/images/ImagePicker.vue'
const props = defineProps<{
modelValue: null | number
timeslotId: number
isOpen: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: number | null): void
(e: 'show', value: boolean): void
}>()
const noteId = computed(() => props.modelValue)
const { t } = useI18n()
const noteStore = useNoteStore()
const { obj: storedNote } = useAPIObject(noteStore, noteId)
const noteData = ref<Note | NewNote>(newNote(props.timeslotId))
const error = ref<Error>()
const errors = useServerErrors(error)
watchEffect(() => {
if (storedNote.value) {
noteData.value = { ...storedNote.value }
}
})
watchEffect(() => {
noteData.value.slug = slugify(noteData.value.title)
})
const localIsOpen = useUpdatableState(
computed(() => props.isOpen),
(isOpen) => emit('show', isOpen),
)
const { textarea: summaryEl } = useTextareaAutosize({ watch: () => noteData.value.summary })
const { textarea: contentEl } = useTextareaAutosize({ watch: () => noteData.value.content })
async function save() {
const data = noteData.value
error.value = undefined
let note: Note
try {
if ('id' in data) {
note = await noteStore.update(data.id, data)
} else {
note = await noteStore.create(data)
}
} catch (e) {
if (e instanceof Error) {
error.value = e
}
return
}
emit('update:modelValue', note.id)
emit('show', false)
}
</script>
<script lang="ts">
export default {
compatConfig: {
MODE: 3,
},
}
</script>
<template>
<div>
<b-modal
ref="modalNote"
:title="$t('noteEditor.editNote')"
:cancel-title="$t('cancel')"
size="lg"
@ok="saveNote"
>
<b-container fluid>
<p>
<template v-if="timeslot">
{{ $t('noteEditor.intro.existing', { date: prettyDateTime(timeslot.start) }) }}
</template>
<template v-else>
{{ $t('noteEditor.intro.new') }}
</template>
</p>
<b-row>
<b-col cols="3"> {{ $t('noteEditor.title') }}: </b-col>
<b-col cols="9">
<b-form-input
v-model="title"
type="text"
:placeholder="$t('noteEditor.titlePlaceholder')"
/>
</b-col>
<b-col cols="3" />
<b-col cols="9">
<small class="slug">{{ $t('slug') }}: {{ slug }}</small>
</b-col>
</b-row>
<br />
<b-row>
<b-col cols="3"> {{ $t('noteEditor.summary') }}: </b-col>
<b-col cols="9">
<b-form-textarea
v-model="summary"
:rows="4"
:placeholder="$t('noteEditor.summaryPlaceholder')"
/>
</b-col>
</b-row>
<br />
<b-row>
<b-col cols="3"> {{ $t('noteEditor.content') }}: </b-col>
<b-col cols="9">
<b-form-textarea
v-model="content"
:rows="8"
:placeholder="$t('noteEditor.contentPlaceholder')"
/>
</b-col>
</b-row>
<br />
<b-row>
<b-col cols="3"> {{ $t('noteEditor.image') }}: </b-col>
<b-col cols="9">
<b-form-file
accept="image/*"
type="file"
class="mb-3"
:browse-text="$t('browse')"
:placeholder="$t('noteEditor.chooseImage')"
@change="setNoteImage"
/>
<img v-if="noteImage" class="tw-w-full tw-block" :src="noteImage" />
</b-col>
</b-row>
</b-container>
</b-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import prettyDate from '../../mixins/prettyDate'
import slugify from '../../mixins/slugify'
const SUBMIT_NEW = false
const SUBMIT_UPDATE = true
export default {
mixins: [prettyDate, slugify],
data() {
return {
note: {},
scheduleID: 0,
timeslotID: 0,
timeslot: null,
title: '',
summary: '',
content: '',
image: null,
backuptitle: '',
backupsummary: '',
backupcontent: '',
}
},
computed: {
slug() {
return this.slugify(this.title)
},
noteImage() {
if (!this.note.image) {
return ''
}
return this.note.image.includes('http')
? this.note.image
: import.meta.env.VUE_APP_BASEURI_STEERING + this.note.image
},
...mapGetters({
selectedShow: 'shows/selectedShow',
getTimeslotById: 'shows/getTimeslotById',
}),
},
methods: {
submit(event, updatemode) {
// prevent the modal from closing automatically on click
event.preventDefault()
// backup the note contents
const backupImage = this.note.image
this.backuptitle = this.note.title
this.backupsummary = this.note.summary
this.backupcontent = this.note.content
// now set the new contents
this.note.title = this.title
this.note.summary = this.summary
this.note.content = this.content
if (this.image) {
this.note.image = this.image
}
// for new notes we need to set some extras that are not in the UI yet
if (updatemode === SUBMIT_NEW) {
this.note.show = this.selectedShow.id
this.note.timeslot = this.timeslotID
this.note.slug = this.slug
this.note.cba_id = 0 // TODO: implement
this.note.audio_url = '' // TODO: implement
}
const modal = this.$refs.modalNote
this.$store.dispatch('shows/submitNote', {
update: updatemode,
id: this.selectedShow.id,
scheduleID: this.scheduleID,
timeslotID: this.timeslotID,
note: this.note,
callback: () => {
modal.hide()
},
callbackCancel: () => {
// as there was an error saving the show, we have to make sure
// to restore the initial values of the note object
this.note.title = this.backuptitle
this.note.summary = this.backupsummary
this.note.content = this.backupcontent
this.note.image = backupImage
// and we leave the modal open, so no call to its .hide function here
},
})
},
setNoteImage(event) {
this.image = event.target.files.item(0)
},
update(event) {
// only try to save if anything has changed
if (
this.title !== this.note.title ||
this.summary !== this.note.summary ||
this.content !== this.note.content ||
this.image
) {
this.submit(event, SUBMIT_UPDATE)
}
// if nothing was changed, just close the modal
else {
this.$refs.modalNote.hide()
}
},
new(event) {
// title and content are necessary
if (this.title.trim() === '' || this.content.trim() === '') {
event.preventDefault()
// TODO: make this nicer UI-wise (red text annotations next to input fields instead of simple alert)
alert('Please provide at least a title and some content!')
} else {
this.submit(event, SUBMIT_NEW)
}
},
saveNote(event) {
if (!this.note.id) {
this.new(event)
return
}
// If the note image is a string we haven't updated the field
// and thus delete it, so the API doesn't try to update it by mistake.
if (typeof this.note.image === 'string') {
delete this.note.image
}
this.update(event)
},
openModal(note, timeslotID, scheduleID) {
if (note === null) {
this.note = {}
this.title = ''
this.summary = ''
this.content = ''
} else {
this.note = { ...note }
this.title = this.note.title
this.summary = this.note.summary
this.content = this.note.content
}
this.timeslot = this.getTimeslotById(timeslotID)
this.timeslotID = timeslotID
this.scheduleID = scheduleID
this.$refs.modalNote.show()
},
},
}
</script>
<style scoped>
.slug {
color: gray;
}
</style>
......@@ -322,23 +322,14 @@ export default {
noteEditor: {
editNote: 'Sendungsbeschreibung bearbeiten',
intro: {
existing: 'Das ist die Sendungsbeschreibung für den Sendeplatz am %{date}',
new: 'Du erstellst eine neue Sendungsbeschreibung',
},
title: 'Titel',
titlePlaceholder: 'Gib einen Titel ein',
summary: 'Zusammenfassung',
summaryPlaceholder: 'Gib eine Zusammenfassung ein',
content: 'Inhalt',
contentPlaceholder: 'Beschreibe den Inhalt der Sendung',
image: 'Bild',
chooseImage: 'Bild auswählen',
save: 'Sendungsbeschreibung speichern',
},
error: {
......
......@@ -314,23 +314,14 @@ export default {
noteEditor: {
editNote: 'Edit note',
intro: {
existing: 'This is the note for the timeslot on %{date}',
new: 'You are creating a new note',
},
title: 'Title',
titlePlaceholder: 'Enter a title',
summary: 'Summary',
summaryPlaceholder: 'Enter a summary',
content: 'Content',
contentPlaceholder: 'Describe the content of the show',
image: 'Image',
chooseImage: 'Choose image',
save: 'Save Note',
},
error: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment