diff options
author | vnugent <public@vaughnnugent.com> | 2023-12-16 02:40:03 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-12-16 02:40:03 -0500 |
commit | 0d25abab798c005266a1c0b4eeba957d232d4328 (patch) | |
tree | 427bd36e33fcd4960e3a2bc7d73b77dc7779b214 /front-end/src/views/Blog/components | |
parent | 4b8ae76132d2342f40cec703b3d5145ea075c451 (diff) |
move blog admin state to pinia store plugin
Diffstat (limited to 'front-end/src/views/Blog/components')
13 files changed, 191 insertions, 191 deletions
diff --git a/front-end/src/views/Blog/components/Channels.vue b/front-end/src/views/Blog/components/Channels.vue index 2a160b3..bf29067 100644 --- a/front-end/src/views/Blog/components/Channels.vue +++ b/front-end/src/views/Blog/components/Channels.vue @@ -2,14 +2,13 @@ <div id="channel-editor"> <EditorTable title="Manage channels" :show-edit="showEdit" :pagination="pagination" @open-new="openNew"> <template #table> - <ChannelTable - :channels="items" - @open-edit="openEdit" + <ChannelTable + :items="items" + @open-edit="openEdit" /> </template> <template #editor> <ChannelEdit - :blog="$props.blog" @close="closeEdit" @on-submit="onSubmit" @on-delete="onDelete" @@ -21,32 +20,25 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { BlogState } from '../blog-api'; import { isEmpty, filter as _filter } from 'lodash-es'; import { apiCall } from '@vnuge/vnlib.browser'; -import { BlogChannel, ChannelFeed, useFilteredPages } from '@vnuge/cmnext-admin'; +import { BlogChannel, ChannelFeed } from '@vnuge/cmnext-admin'; +import { useStore } from '../../../store'; import ChannelEdit from './Channels/ChannelEdit.vue'; import ChannelTable from './Channels/ChannelTable.vue'; import EditorTable from './EditorTable.vue'; const emit = defineEmits(['close', 'reload']) -const props = defineProps<{ - blog: BlogState, -}>() +const store = useStore() +const { items, pagination } = store.channels.createPages() -const { updateChannel, addChannel, deleteChannel, getQuery } = props.blog.channels; -const { channelEdit } = getQuery() +const showEdit = computed(() => !isEmpty(store.channels.editChannel)) -//Setup channel filter -const { items, pagination } = useFilteredPages(props.blog.channels, 15) - -const showEdit = computed(() => !isEmpty(channelEdit.value)) - -const openEdit = (channel: BlogChannel) => channelEdit.value = channel.id; +const openEdit = (channel: BlogChannel) => store.channels.editId = channel.id; const closeEdit = (update?:boolean) => { - channelEdit.value = '' + store.channels.editId = '' //reload channels if(update){ emit('reload') @@ -56,7 +48,7 @@ const closeEdit = (update?:boolean) => { } const openNew = () => { - channelEdit.value = 'new' + store.channels.editId = 'new' //Reset page to top window.scrollTo(0, 0) } @@ -64,18 +56,18 @@ const openNew = () => { const onSubmit = async ({channel, feed} : { channel:BlogChannel, feed? : ChannelFeed}) => { //Check for new channel, or updating old channel - if(channelEdit.value === 'new'){ + if(store.channels.editId === 'new'){ //Exec create call await apiCall(async () => { - await addChannel(channel, feed); + await store.channels.add(channel, feed); //Close the edit panel closeEdit(true); }) } - else if(!isEmpty(channelEdit.value)){ + else if(!isEmpty(store.channels.editId)){ //Exec update call await apiCall(async () => { - await updateChannel(channel, feed); + await store.channels.update(channel, feed); //Close the edit panel closeEdit(true); }) @@ -86,7 +78,7 @@ const onSubmit = async ({channel, feed} : { channel:BlogChannel, feed? : Channel const onDelete = async (channel : BlogChannel) => { //Exec delete call await apiCall(async () => { - await deleteChannel(channel); + await store.channels.delete(channel); //Close the edit panel closeEdit(true); }) diff --git a/front-end/src/views/Blog/components/Channels/ChannelEdit.vue b/front-end/src/views/Blog/components/Channels/ChannelEdit.vue index b84adf0..0363746 100644 --- a/front-end/src/views/Blog/components/Channels/ChannelEdit.vue +++ b/front-end/src/views/Blog/components/Channels/ChannelEdit.vue @@ -62,22 +62,19 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { BlogState } from '../../blog-api'; -import { forEach, isEmpty, cloneDeep, isNil } from 'lodash-es'; +import { forEach, isEmpty, cloneDeep, isNil, set } from 'lodash-es'; import { reactiveComputed } from '@vueuse/core'; import { useConfirm } from '@vnuge/vnlib.browser'; -import FeedFields from '../FeedFields.vue'; import { BlogChannel, ChannelFeed, useXmlProperties } from '@vnuge/cmnext-admin'; import { getChannelForm } from '../../form-helpers'; +import { useStore } from '../../../../store'; +import FeedFields from '../FeedFields.vue'; const emit = defineEmits(['close', 'onSubmit', 'onDelete']) - -const props = defineProps<{ - blog: BlogState -}>() +const store = useStore() //Disallow empty channels -const channel = computed(() => props.blog.channels.editChannel.value || {} as BlogChannel) +const channel = computed(() => store.channels.editChannel || {} as BlogChannel) const editMode = computed(() => !isNil(channel.value.id)) const { getChannelValidator, channelSchema, feedSchema, getFeedValidator } = getChannelForm(editMode); @@ -98,7 +95,7 @@ const feedEnabled = computed(() => !isEmpty(feedBuffer.url)) const disableFeed = () => { //Clear the feed - forEach(feedBuffer, (_value, key) => feedBuffer[key] = null) + forEach(feedBuffer, (_value, key) => set(feedBuffer, key, null)) //Reset the feed validator feedVal.reset(); } diff --git a/front-end/src/views/Blog/components/Channels/ChannelTable.vue b/front-end/src/views/Blog/components/Channels/ChannelTable.vue index cdf15e0..ff27371 100644 --- a/front-end/src/views/Blog/components/Channels/ChannelTable.vue +++ b/front-end/src/views/Blog/components/Channels/ChannelTable.vue @@ -9,7 +9,7 @@ </tr> </thead> <tbody> - <tr v-for="channel in channels" :key="channel.id" class="table-row"> + <tr v-for="channel in $props.items" :key="channel.id" class="table-row"> <td> {{ channel.name }} </td> @@ -33,14 +33,9 @@ <script setup lang="ts"> import { BlogChannel } from '@vnuge/cmnext-admin'; -import { toRefs } from 'vue'; -const emit = defineEmits(['open-edit']) - -const props = defineProps<{ - channels:BlogChannel[] -}>() -const { channels } = toRefs(props) +const emit = defineEmits(['open-edit']) +defineProps<{ items: BlogChannel[] }>() const feedEnabled = (channel: BlogChannel) => channel.feed ? 'Enabled' : 'Disabled' const openEdit = (channel: BlogChannel) => emit('open-edit', channel) diff --git a/front-end/src/views/Blog/components/Content.vue b/front-end/src/views/Blog/components/Content.vue index d8a9f24..888b595 100644 --- a/front-end/src/views/Blog/components/Content.vue +++ b/front-end/src/views/Blog/components/Content.vue @@ -2,11 +2,12 @@ <div id="content-editor" class=""> <EditorTable title="Manage content" :show-edit="showEdit" :pagination="pagination" @open-new="openNew"> <template #table> - <ContentTable - :content="items" + <ContentTable + :items="items" @open-edit="openEdit" @copy-link="copyLink" @delete="onDelete" + @download="onDownload" /> </template> <template #editor> @@ -16,7 +17,7 @@ <span role="progressbar" aria-labelledby="ProgressLabel" - :aria-valuenow="progress" + :aria-valuenow="uploadProgress" class="relative block bg-gray-200 rounded-full dark:bg-dark-500" > <span class="absolute inset-0 flex items-center justify-center text-[10px]/4"> @@ -27,58 +28,46 @@ </span> </div> <ContentEditor - :blog="$props.blog" @submit="onSubmit" @close="closeEdit" @delete="onDelete" /> </template> </EditorTable> + <a class="hidden" ref="downloadAnchor"></a> </div> </template> <script setup lang="ts"> -import { computed, toRefs } from 'vue'; -import { BlogState } from '../blog-api'; +import { computed, shallowRef } from 'vue'; import { isEmpty } from 'lodash-es'; import { apiCall, useConfirm } from '@vnuge/vnlib.browser'; -import { useClipboard } from '@vueuse/core'; -import { ContentMeta, useFilteredPages } from '@vnuge/cmnext-admin'; +import { get, useClipboard } from '@vueuse/core'; +import { ContentMeta } from '@vnuge/cmnext-admin'; +import { useStore } from '../../../store'; +import { storeToRefs } from 'pinia'; import EditorTable from './EditorTable.vue'; import ContentEditor from './Content/ContentEditor.vue'; import ContentTable from './Content/ContentTable.vue'; const emit = defineEmits(['reload']) -const props = defineProps<{ - blog: BlogState, - progress: number -}>() +const store = useStore() +const { uploadProgress } = storeToRefs(store) +const { items, pagination } = store.content.createPages() -const { progress } = toRefs(props) - -//Get the computed content -const { selectedId, - updateContent, - uploadContent, - deleteContent, - updateContentName, - getPublicUrl - } = props.blog.content; - - //Setup content filter - const { items, pagination } = useFilteredPages(props.blog.content, 15) const { reveal } = useConfirm() + const downloadAnchor = shallowRef<HTMLAnchorElement>() -const showEdit = computed(() => !isEmpty(selectedId.value)); -const loadingProgress = computed(() => `${progress?.value}%`); -const progressWidth = computed(() => ({ width: `${progress?.value}%` })); -const showProgress = computed(() => progress?.value > 0 && progress?.value < 100); +const showEdit = computed(() => !isEmpty(store.content.selectedId)); +const loadingProgress = computed(() => `${uploadProgress.value}%`); +const progressWidth = computed(() => ({ width: `${uploadProgress.value}%` })); +const showProgress = computed(() => uploadProgress.value > 0 && uploadProgress.value < 100); -const openEdit = async (item: ContentMeta) => selectedId.value = item.id +const openEdit = async (item: ContentMeta) => store.content.selectedId = item.id const closeEdit = (update?: boolean) => { - selectedId.value = '' + store.content.selectedId = '' //reload channels if (update) { emit('reload') @@ -88,7 +77,7 @@ const closeEdit = (update?: boolean) => { } const openNew = () => { - selectedId.value = 'new' + store.content.selectedId = 'new' //Reset page to top window.scrollTo(0, 0) } @@ -102,7 +91,7 @@ interface OnSubmitValue{ const { copy } = useClipboard() const copyLink = async (item : ContentMeta) =>{ apiCall(async ({toaster}) =>{ - const url = await getPublicUrl(item); + const url = await store.content.getPublicUrl(item); await copy(url); toaster.general.info({ title: 'Copied link to clipboard' }) }); @@ -111,7 +100,7 @@ const copyLink = async (item : ContentMeta) =>{ const onSubmit = async (value : OnSubmitValue) => { //Check for new channel, or updating old channel - if (selectedId.value === 'new') { + if (store.content.selectedId === 'new') { //Exec create call await apiCall(async () => { @@ -120,21 +109,21 @@ const onSubmit = async (value : OnSubmitValue) => { } //endpoint returns the content - await uploadContent(value.file, value.item.name!); + await store.content.uploadContent(value.file, value.item.name!); //Close the edit panel closeEdit(true); }) } - else if (!isEmpty(selectedId.value)) { + else if (!isEmpty(store.content.selectedId)) { //Exec update call await apiCall(async () => { //If no file was attached, just update the file name if(value.file?.name){ - await updateContent(value.item, value.file); + await store.content.updateContent(value.item, value.file); } else{ - await updateContentName(value.item, value.item.name!); + await store.content.updateContentName(value.item, value.item.name!); } //Close the edit panel closeEdit(true); @@ -159,13 +148,29 @@ const onDelete = async (item: ContentMeta) => { //Exec delete call await apiCall(async () => { - await deleteContent(item); + await store.content.delete(item); //Close the edit panel closeEdit(true); }) //Refresh content after delete - props.blog.content.refresh(); + store.content.refresh(); +} + +const onDownload = async (item: ContentMeta) => { + //Exec download call + await apiCall(async () => { + //Download the file blob from the server + const fileBlob = await store.content.downloadContent(item) + + //Create a url for the blob and open the save link + const url = window.URL.createObjectURL(fileBlob); + + const anchor = get(downloadAnchor)!; + anchor.href = url; + anchor.download = item.name!; + anchor.click(); + }) } diff --git a/front-end/src/views/Blog/components/Content/ContentEditor.vue b/front-end/src/views/Blog/components/Content/ContentEditor.vue index 608cd1b..392e706 100644 --- a/front-end/src/views/Blog/components/Content/ContentEditor.vue +++ b/front-end/src/views/Blog/components/Content/ContentEditor.vue @@ -105,35 +105,34 @@ import { computed, ref, watch } from 'vue'; import { reactiveComputed, useFileDialog, useDropZone } from '@vueuse/core'; import { ContentMeta } from '@vnuge/cmnext-admin'; import { useConfirm, useVuelidateWrapper, useFormToaster, useWait } from '@vnuge/vnlib.browser'; -import { defaultTo, first, isEmpty, round } from 'lodash-es'; +import { clone, defaultTo, first, isEmpty, round } from 'lodash-es'; import { required, helpers, maxLength } from '@vuelidate/validators' import { useVuelidate } from '@vuelidate/core'; -import { BlogState } from '../../blog-api'; +import { useStore } from '../../../../store'; const emit = defineEmits(['close', 'submit', 'delete']); -const props = defineProps<{ - blog: BlogState -}>(); const { reveal } = useConfirm(); const { waiting } = useWait(); -const { content, channels } = props.blog; +const { content, channels } = useStore() const newFileDropZone = ref<HTMLElement>(); -const selectedId = computed(() => content.selectedId.value); -const selectedContent = computed<ContentMeta>(() => defaultTo(content.selectedItem.value, {} as ContentMeta)); -const metaBuffer = reactiveComputed<ContentMeta>(() => ({ ...selectedContent.value})); -const isChannelSelected = computed(() => channels.selectedItem.value?.id?.length ?? 0 > 0); +const selectedId = computed(() => content.selectedId); +const selectedContent = computed<ContentMeta>(() => defaultTo(content.selected, {} as ContentMeta)); +const metaBuffer = reactiveComputed<Required<ContentMeta>>(() => clone(selectedContent.value) as Required<ContentMeta>); +const isChannelSelected = computed(() => channels.selected?.id?.length ?? 0 > 0); const isNewUpload = computed(() => selectedId.value === 'new'); -const v$ = useVuelidate({ - name: { +const validationArgs = { + name: { required, - maxLen:maxLength(50), + maxLen: maxLength(50), reg: helpers.withMessage('The file name contains invalid characters', helpers.regex(/^[a-zA-Z0-9 \-\.]*$/)) - }, -}, metaBuffer) + }, +} + +const v$ = useVuelidate(validationArgs, metaBuffer) const { validate } = useVuelidateWrapper(v$); diff --git a/front-end/src/views/Blog/components/Content/ContentTable.vue b/front-end/src/views/Blog/components/Content/ContentTable.vue index 3afe320..cc94c9f 100644 --- a/front-end/src/views/Blog/components/Content/ContentTable.vue +++ b/front-end/src/views/Blog/components/Content/ContentTable.vue @@ -5,14 +5,19 @@ <th>Id</th> <th>Date</th> <th>Content Type</th> - <th>Length</th> + <th>Size</th> <th></th> </tr> </thead> <tbody> - <tr v-for="item in content" :key="item.id" class="table-row"> + <tr v-for="item in $props.items" :key="item.id" class="table-row"> <td> - {{ getItemName(item) }} + <span class="mr-2"> + <fa-icon size="sm" :icon="getContentIconType(item)" /> + </span> + <span> + {{ getItemName(item) }} + </span> </td> <td> {{ getItemId(item) }} @@ -21,7 +26,7 @@ {{ getDateString(item.date) }} </td> <td> - {{ item.content_type }} + <span>{{ item.content_type }}</span> </td> <td> {{ getItemLength(item) }} @@ -37,7 +42,10 @@ <button class="btn xs no-border" @click="copy(item.id)"> <fa-icon icon="copy" /> </button> - <button class="btn xs no-border red" @click="deleteItem(item)"> + <button class="btn xs no-border" @click="download(item)"> + <fa-icon icon="file-download" /> + </button> + <button class="btn xs no-border red" @click="deleteItem(item)"> <fa-icon icon="trash" /> </button> </fieldset> @@ -47,19 +55,13 @@ </template> <script setup lang="ts"> -import { toRefs } from 'vue'; -import { filter as _filter, truncate } from 'lodash-es'; +import { filter as _filter, defaultTo, includes, truncate } from 'lodash-es'; import { useClipboard } from '@vueuse/core'; import { useWait } from '@vnuge/vnlib.browser'; import { ContentMeta } from '@vnuge/cmnext-admin'; -const emit = defineEmits(['open-edit', 'copy-link', 'delete']) - -const props = defineProps<{ - content: ContentMeta[] -}>() - -const { content } = toRefs(props) +const emit = defineEmits(['open-edit', 'copy-link', 'delete', 'download']) +defineProps<{ items: ContentMeta[] }>() const { waiting } = useWait() const { copy } = useClipboard() @@ -72,8 +74,20 @@ const getItemLength = (item: ContentMeta) : string =>{ const getItemId = (item: ContentMeta) => truncate(item.id || '', { length: 20 }) const getItemName = (item : ContentMeta) => truncate(item.name || '', { length: 30 }) +const getContentIconType = (item: ContentMeta) => { + const type = defaultTo(item.content_type, '') + if (includes(type, 'image')) return 'image' + if (includes(type, 'video')) return 'video' + if (includes(type, 'audio')) return 'headphones' + if (includes(type, 'html')) return 'code' + if (includes(type, 'zip')) return 'file-zipper' + return 'file' +} + const openEdit = async (item: ContentMeta) => emit('open-edit', item) const copyLink = (item : ContentMeta) => emit('copy-link', item) const deleteItem = (item : ContentMeta) => emit('delete', item) +const download = (item : ContentMeta) => emit('download', item) + </script>
\ No newline at end of file diff --git a/front-end/src/views/Blog/components/ContentSearch.vue b/front-end/src/views/Blog/components/ContentSearch.vue index 78cfa1a..7871196 100644 --- a/front-end/src/views/Blog/components/ContentSearch.vue +++ b/front-end/src/views/Blog/components/ContentSearch.vue @@ -40,19 +40,15 @@ import { apiCall, useWait } from '@vnuge/vnlib.browser'; import { computed, Ref, ref } from 'vue'; import { map, slice, truncate } from 'lodash-es'; import { ContentMeta } from '@vnuge/cmnext-admin'; -import { BlogState } from '../blog-api'; +import { useStore } from '../../../store'; const emit = defineEmits(['selected']) +const store = useStore() -const props = defineProps<{ - blog: BlogState, -}>() - -const { createReactiveSearch, getPublicUrl } = props.blog.content const { waiting } = useWait() const search = ref('') -const searcher = createReactiveSearch(search); +const searcher = store.content.createReactiveSearch(search); interface ContentResult extends ContentMeta { readonly shortId: string, @@ -67,7 +63,7 @@ const searchResults = computed<ContentResult[]>(() => { //Copies the link to the clipboard from the server to insert into the editor const copyLink = (result : ContentMeta, copy : (text: string) => Promise<void> ) => { apiCall(async () =>{ - const link = await getPublicUrl(result); + const link = await store.content.getPublicUrl(result); await copy(link); }) } diff --git a/front-end/src/views/Blog/components/FeedFields.vue b/front-end/src/views/Blog/components/FeedFields.vue index 0397c48..918f449 100644 --- a/front-end/src/views/Blog/components/FeedFields.vue +++ b/front-end/src/views/Blog/components/FeedFields.vue @@ -24,7 +24,7 @@ <div v-if="editMode" class="flex flex-col"> <div v-if="$props.blog" class="mb-2"> - <EpAdder :blog="$props.blog" @submit="onAddEnclosure" /> + <EpAdder @submit="onAddEnclosure" /> </div> <div class=""> @@ -38,13 +38,11 @@ <script setup lang="ts"> import { computed, defineAsyncComponent, ref } from 'vue'; import { FeedProperty, UseXmlProperties } from '@vnuge/cmnext-admin'; -import { BlogState } from '../blog-api'; -import EpAdder from './podcast-helpers/EpisodeAdder.vue'; +const EpAdder = defineAsyncComponent(() => import('./podcast-helpers/EpisodeAdder.vue')); const JsonEditorVue = defineAsyncComponent(() => import('json-editor-vue')) const props = defineProps<{ properties: UseXmlProperties, - blog?: BlogState }>() const { getXml, saveJson, getModel, addProperties } = props.properties diff --git a/front-end/src/views/Blog/components/Posts.vue b/front-end/src/views/Blog/components/Posts.vue index d52a0ac..801fa3c 100644 --- a/front-end/src/views/Blog/components/Posts.vue +++ b/front-end/src/views/Blog/components/Posts.vue @@ -3,14 +3,13 @@ <EditorTable title="Manage posts" :show-edit="showEdit" :pagination="pagination" @open-new="openNew"> <template #table> <PostTable - :posts="items" + :items="items" @open-edit="openEdit" @delete="onDelete" /> </template> <template #editor> <PostEditor - :blog="$props.blog" @submit="onSubmit" @close="closeEdit" @delete="onDelete" @@ -23,34 +22,26 @@ <script setup lang="ts"> import { computed, defineAsyncComponent } from 'vue'; import { isEmpty } from 'lodash-es'; -import { PostMeta, useFilteredPages } from '@vnuge/cmnext-admin'; +import { PostMeta } from '@vnuge/cmnext-admin'; import { apiCall, debugLog, useConfirm } from '@vnuge/vnlib.browser'; -import { BlogState } from '../blog-api'; +import { useStore } from '../../../store'; import EditorTable from './EditorTable.vue'; import PostTable from './Posts/PostTable.vue'; - const PostEditor = defineAsyncComponent(() => import('./Posts/PostEdit.vue')) const emit = defineEmits(['reload']) +const store = useStore() -const props = defineProps<{ - blog: BlogState -}>() - -const { selectedId, publishPost, updatePost, deletePost } = props.blog.posts; -const { updatePostContent } = props.blog.content; const { reveal } = useConfirm() -const showEdit = computed(() => !isEmpty(selectedId.value)); - -//Init paginated items for the table and use filtered items -const { pagination, items } = useFilteredPages(props.blog.posts, 15) +const showEdit = computed(() => !isEmpty(store.posts.selectedId)); +const { items, pagination } = store.posts.createPages(); //Open with the post id -const openEdit = async (post: PostMeta) => selectedId.value = post.id; +const openEdit = async (post: PostMeta) => store.posts.selectedId = post.id; const closeEdit = (update?: boolean) => { - selectedId.value = '' + store.posts.selectedId = '' //reload channels if (update) { emit('reload') @@ -61,40 +52,46 @@ const closeEdit = (update?: boolean) => { const openNew = () => { //Reset the edit post - selectedId.value = 'new' + store.posts.selectedId = 'new' //Reset page to top window.scrollTo(0, 0) } -const onSubmit = async ({post, content } : { post:PostMeta, content:string }) => { +const onSubmit = async ({post, content } : { post: PostMeta, content: string }) => { debugLog('submitting', post, content); //Check for new channel, or updating old channel - if (selectedId.value === 'new') { + if (store.posts.selectedId === 'new') { //Exec create call - await apiCall(async () => { + await apiCall(async ({toaster}) => { //endpoint returns the content - const newMeta = await publishPost(post); + const newMeta = await store.posts.add(post); //Publish the content - await updatePostContent(newMeta, content) + await store.content.updatePostContent(newMeta, content) - //Close the edit panel - closeEdit(true); + toaster.general.success({ + id: 'post-create-success', + title: 'Created', + text: `Post '${post.title}' created`, + }) }) } - else if (!isEmpty(selectedId.value)) { + else if (!isEmpty(store.posts.selectedId)) { //Exec update call - await apiCall(async () => { - await updatePost(post); + await apiCall(async ( {toaster} ) => { + await store.posts.update(post); //Publish the content - await updatePostContent(post, content) + await store.content.updatePostContent(post, content) - //Close the edit panel - closeEdit(true); + toaster.general.info({ + id: 'post-update-success', + title: 'Saved', + text: `Post '${post.title}' updated`, + }) }) } //Notify error state @@ -117,12 +114,12 @@ const onDelete = async (post: PostMeta) => { //Exec delete call await apiCall(async () => { - await deletePost(post); + await store.posts.delete(post); //Close the edit panel closeEdit(true); }) - props.blog.posts.refresh(); + store.posts.refresh(); } </script> diff --git a/front-end/src/views/Blog/components/Posts/PostEdit.vue b/front-end/src/views/Blog/components/Posts/PostEdit.vue index 4106256..4f7285b 100644 --- a/front-end/src/views/Blog/components/Posts/PostEdit.vue +++ b/front-end/src/views/Blog/components/Posts/PostEdit.vue @@ -4,7 +4,10 @@ <div class="button-group"> <!-- Submit the post form --> <button class="btn primary" form="post-edit-form">Save</button> - <button class="btn" @click="onClose">Cancel</button> + <button class="btn" @click="onClose">Back</button> + </div> + <div class="pl-3 text-xs text-color-background"> + ctrl + s </div> </div> <div class="mx-auto"> @@ -25,51 +28,47 @@ /> <div id="post-content-editor" class="px-6" :class="{'invalid':v$.content.$invalid}"> - <Editor :podcast-mode="podcastMode" :blog="$props.blog" @change="onContentChanged" @mode-change="onModeChange" @load="onEditorLoad" /> + <Editor :podcast-mode="podcastMode" @change="onContentChanged" @mode-change="onModeChange" @load="onEditorLoad" /> </div> - <FeedFields :properties="postProperties" :blog="$props.blog" /> + <FeedFields :properties="postProperties" /> <div class="mx-auto my-4"> <div class="button-group"> <!-- Submit the post form --> <button class="btn primary" form="post-edit-form">Save</button> - <button class="btn" @click="onClose">Cancel</button> + <button class="btn" @click="onClose">Back</button> <button v-if="!isNew" class="btn red" @click="onDelete">Delete Forever</button> </div> </div> </div> </template> <script setup lang="ts"> -import { computed, defineAsyncComponent, ref } from 'vue'; -import { BlogState } from '../../blog-api'; -import { reactiveComputed } from '@vueuse/core'; -import { isNil, isString, split } from 'lodash-es'; +import { computed, defineAsyncComponent, ref, toRef } from 'vue'; +import { reactiveComputed, useMagicKeys } from '@vueuse/core'; +import { isNil, isString, split, debounce } from 'lodash-es'; import { PostMeta, useXmlProperties } from '@vnuge/cmnext-admin'; import { apiCall, useUser } from '@vnuge/vnlib.browser'; import { getPostForm } from '../../form-helpers'; +import { useStore } from '../../../../store'; import FeedFields from '../FeedFields.vue'; - const Editor = defineAsyncComponent(() => import('../../ckeditor/Editor.vue')); const emit = defineEmits(['close', 'submit', 'delete']); -const props = defineProps<{ - blog: BlogState -}>() +const store = useStore() const { getProfile } = useUser(); const { schema, getValidator } = getPostForm(); -const { posts, content } = props.blog; const podcastMode = ref(false) -const isNew = computed(() => isNil(posts.selectedItem.value)); +const isNew = computed(() => isNil(store.posts.selected)); /* Post meta may load delayed from the api so it must be computed and reactive, it may also be empty when a new post is created */ const postBuffer = reactiveComputed<PostMeta>(() => { return { - ...posts.selectedItem.value, + ...store.posts.selected, content: '' } as PostMeta }); @@ -77,7 +76,7 @@ const postBuffer = reactiveComputed<PostMeta>(() => { const { v$, validate } = getValidator(postBuffer); //Wrap the post properties in an xml feed editor -const postProperties = useXmlProperties(posts.selectedItem); +const postProperties = useXmlProperties(toRef(store.posts.selected)); const onSubmit = async () =>{ if(!await validate()){ @@ -117,7 +116,7 @@ const onContentChanged = (content: string) => { v$.value.content.$model = content; } -const onDelete = () => emit('delete', posts.selectedItem.value) +const onDelete = () => emit('delete', store.posts.selected) const setMeAsAuthor = () => { apiCall(async () => { @@ -132,8 +131,12 @@ const onModeChange = (e: boolean) => { const onEditorLoad = async (editor : any) =>{ + if(isNil(store.posts.selected)){ + return; + } + //Get the initial content - const postContent = await content.getSelectedPostContent(); + const postContent = await store.content.getPostContent(store.posts.selected); //Set the initial content if(!isNil(postContent)){ @@ -148,6 +151,19 @@ const onEditorLoad = async (editor : any) =>{ } } +const throttleOnSubmit = debounce(onSubmit, 200); + +//Setup ctrl+s to submit the form(save) +useMagicKeys({ + passive: false, + onEventFired(e) { + if (e.ctrlKey && e.key === 's' && e.type === 'keydown'){ + e.preventDefault() + throttleOnSubmit() + } + }, +}) + </script> <style lang="scss"> diff --git a/front-end/src/views/Blog/components/Posts/PostTable.vue b/front-end/src/views/Blog/components/Posts/PostTable.vue index c1583a6..576501f 100644 --- a/front-end/src/views/Blog/components/Posts/PostTable.vue +++ b/front-end/src/views/Blog/components/Posts/PostTable.vue @@ -10,7 +10,7 @@ </tr> </thead> <tbody> - <tr v-for="post in posts" :key="post.id" class="table-row"> + <tr v-for="post in $props.items" :key="post.id" class="table-row"> <td class="truncate max-w-[16rem]"> {{ post.title }} </td> @@ -42,19 +42,14 @@ </template> <script setup lang="ts"> -import { toRefs, watch } from 'vue'; +import { watch } from 'vue'; import { filter as _filter, truncate } from 'lodash-es'; import { useClipboard } from '@vueuse/core'; import { PostMeta } from '@vnuge/cmnext-admin'; import { useGeneralToaster } from '@vnuge/vnlib.browser'; const emit = defineEmits(['reload', 'open-edit', 'delete']) - -const props = defineProps<{ - posts: PostMeta[], -}>() - -const { posts } = toRefs(props) +defineProps<{ items: PostMeta[] }>() const { copy, copied } = useClipboard() const { info } = useGeneralToaster() diff --git a/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue b/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue index 6d1e473..e8b9ddd 100644 --- a/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue +++ b/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue @@ -43,7 +43,7 @@ <div class=""> Search for content by its id or file name. </div> - <ContentSearch :blog="$props.blog" @selected="onContentSelected"/> + <ContentSearch @selected="onContentSelected"/> </div> </PopoverPanel> </Popover> @@ -72,7 +72,6 @@ <script setup lang="ts"> import { ref, reactive, computed } from 'vue'; -import { BlogState } from '../../blog-api'; import { PodcastEntity, getPodcastForm } from './podcast-form' import { Dialog, @@ -87,15 +86,12 @@ import { import ContentSearch from '../ContentSearch.vue' import { apiCall, debugLog } from '@vnuge/vnlib.browser'; import { ContentMeta } from '@vnuge/cmnext-admin'; +import { useStore } from '../../../../store'; const emit = defineEmits(['submit']) - -const props = defineProps<{ - blog: BlogState, -}>() +const store = useStore() const isOpen = ref(false) -const { getPublicUrl } = props.blog.content; const { schema, setEnclosureContent, getValidator, exportProperties } = getPodcastForm() const buffer = reactive<PodcastEntity>({} as PodcastEntity) @@ -127,7 +123,7 @@ const onCancel = () => setIsOpen(false) const onContentSelected = (content: ContentMeta) =>{ apiCall(async () =>{ //Get the content link from the server - const url = await getPublicUrl(content) + const url = await store.content.getPublicUrl(content) //set the form content setEnclosureContent(buffer, content, `/${url}`) diff --git a/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts b/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts index 56b898c..8e340b2 100644 --- a/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts +++ b/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts @@ -15,10 +15,10 @@ import { computed, Ref } from 'vue'; import { helpers, required, maxLength, alphaNum, numeric } from "@vuelidate/validators" -import useVuelidate from "@vuelidate/core" +import { useVuelidate } from "@vuelidate/core" import { MaybeRef } from '@vueuse/core'; import { useVuelidateWrapper } from '@vnuge/vnlib.browser'; -import { ContentMeta, FeedProperty } from '@vnuge/cmnext-admin'; +import type { ContentMeta, FeedProperty } from '@vnuge/cmnext-admin'; export interface EnclosureEntity{ fileId: string; |