diff --git a/src/components/shows/NoteEditorModal.vue b/src/components/shows/NoteEditorModal.vue index a8895a943648586e629c2b63cf5eb6063f372e2e..80dcd9c56b50823450941484cfb9b31d82c1572a 100644 --- a/src/components/shows/NoteEditorModal.vue +++ b/src/components/shows/NoteEditorModal.vue @@ -47,6 +47,34 @@ </template> </FormGroup> + <FormGroup :label="t('noteEditor.contributors')" :errors="contributorErrors"> + <ComboBox + v-model="contributors" + :choices="relevantHosts" + :close-on-select="false" + multiple + input-container-class="tw-flex tw-flex-wrap tw-gap-2 tw-w-full form-control tw-h-auto tw-min-h-[46px]" + input-class="tw-border-none tw-min-w-[150px] focus:tw-outline-none" + @search="hostSearch = $event" + > + <template #default="{ choice, ...attrs }"> + <li v-bind="attrs"> + {{ choice.name }} + </li> + </template> + + <template #selected="{ items, deselect }"> + <Tag + v-for="(host, index) in items" + :key="index" + :label="host.name" + removable + @remove="deselect(host)" + /> + </template> + </ComboBox> + </FormGroup> + <FormGroup :label="t('noteEditor.tags')" :errors="tagsErrors"> <TagInput v-model="tags" /> </FormGroup> @@ -74,38 +102,64 @@ <script lang="ts" setup> import { computed, ref, watchEffect } from 'vue' +import { useStore } from 'vuex' import { useTextareaAutosize } from '@vueuse/core' + import { useAPIObject, useServerFieldErrors } from '@/api' import { useI18n } from '@/i18n' -import { useUpdatableState } from '@/util' -import { slugify } from '@/mixins/slugify' +import { Host, Show, TimeSlot } from '@/types' +import { useHostStore } from '@/stores/hosts' import { newNote, NewNote, Note, useNoteStore } from '@/stores/notes' +import { slugify } from '@/mixins/slugify' +import { asyncWritableComputed, useUpdatableState } from '@/util' 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' import ServerErrors from '@/components/ServerErrors.vue' import TagInput from '@/components/generic/TagInput.vue' +import ComboBox from '@/components/ComboBox.vue' +import Tag from '@/components/generic/Tag.vue' defineOptions({ compatConfig: { MODE: 3 } }) const props = defineProps<{ modelValue: null | number - timeslotId: number + timeslot: TimeSlot 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 store = useStore() +const shows = computed<Show[]>(() => store.state.shows.shows) +const show = computed(() => shows.value.find((show) => show.id === props.timeslot.showId)) +const noteId = computed(() => props.modelValue) const noteStore = useNoteStore() const { obj: storedNote } = useAPIObject(noteStore, noteId) -const noteData = ref<Note | NewNote>(newNote(props.timeslotId)) +const noteData = ref<Note | NewNote>(newNote(props.timeslot.id, show.value)) +const { items: hosts, retrieve: retrieveHost } = useHostStore() +const hostSearch = ref('') +const relevantHosts = computed(() => { + const sanitize = (s: string) => s.replace(/\s+/g, '').toLowerCase() + const searchString = sanitize(hostSearch.value) + return hosts.filter( + ({ id, name }) => + !noteData.value.contributorIds.includes(id) && sanitize(name).includes(searchString), + ) +}) const error = ref<Error>() -const [titleErrors, summaryErrors, contentErrors, imageErrors, tagsErrors, remainingErrors] = - useServerFieldErrors(error, 'title', 'summary', 'content', 'tags', 'imageId') +const [ + titleErrors, + summaryErrors, + contentErrors, + imageErrors, + tagsErrors, + contributorErrors, + remainingErrors, +] = useServerFieldErrors(error, 'title', 'summary', 'content', 'tags', 'contributorIds', 'imageId') const tags = computed({ get() { return noteData.value.tags @@ -117,6 +171,17 @@ const tags = computed({ noteData.value.tags = value.join(', ') }, }) +const contributors = asyncWritableComputed<Host[]>([], { + async get() { + const data = await Promise.all( + noteData.value.contributorIds.map((id) => retrieveHost(id, undefined, { useCached: true })), + ) + return data.filter((obj) => obj !== null) as Host[] + }, + set(value: Host[]) { + noteData.value.contributorIds = value.map(({ id }) => id) + }, +}) watchEffect(() => { if (storedNote.value) { diff --git a/src/components/shows/TimeSlotRow.vue b/src/components/shows/TimeSlotRow.vue index dad1b461673ee31e867a9fdc97ccb64098d2b97c..217cadf13ed7e595b31cb3b51558029587d88f13 100644 --- a/src/components/shows/TimeSlotRow.vue +++ b/src/components/shows/TimeSlotRow.vue @@ -69,7 +69,7 @@ <NoteEditorModal v-model="localNoteId" :is-open="showNoteEditor" - :timeslot-id="timeslot.id" + :timeslot="timeslot" @show="showNoteEditor = $event" /> </Teleport> diff --git a/src/i18n/de.js b/src/i18n/de.js index 528e59181b76b8064cf57cd6e0f21d00ed5086a8..1a98eedbdfdab42ea699797352e0a6190f77fe7e 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -337,6 +337,7 @@ export default { contentPlaceholder: 'Beschreibe den Inhalt der Sendung', image: 'Bild', tags: 'Schlagwörter', + contributors: 'Mitwirkende', save: 'Sendungsbeschreibung speichern', }, diff --git a/src/i18n/en.js b/src/i18n/en.js index 9c9f216dc9903f9beda0b7a12681b698783221fb..a4cb607ba837cdf5c43dbc39a72c0dbbe3de0a8f 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -329,6 +329,7 @@ export default { contentPlaceholder: 'Describe the content of the show', image: 'Image', tags: 'Tags', + contributors: 'Contributors', save: 'Save Note', }, diff --git a/src/stores/notes.ts b/src/stores/notes.ts index d39a7cb874c5adb3e7995b2737025340d070fd03..c8f6b3ce93cbdd44ef8292c0f22b16bec1e7b138 100644 --- a/src/stores/notes.ts +++ b/src/stores/notes.ts @@ -10,6 +10,7 @@ import { createExtendableAPI, createSteeringURL, } from '@/api' +import { Show } from '@/types' export type Note = { id: number @@ -35,7 +36,7 @@ export type NewNote = Omit<Note, ReadonlyAttrs> type NoteCreateData = Omit<Note, ReadonlyAttrs> type NoteUpdateData = Partial<Omit<Note, ReadonlyAttrs>> -export function newNote(timeslotId: number): NewNote { +export function newNote(timeslotId: number, show?: Show): NewNote { return { title: '', slug: '', @@ -43,7 +44,7 @@ export function newNote(timeslotId: number): NewNote { content: '', imageId: null, cbaId: null, - contributorIds: [], + contributorIds: show?.hostIds ?? [], links: [], timeslotId, tags: '',