Hello All,
I have a project where I need to have the AgendaTopics.vue display the Topics/Show.vue page with the AgendaTopic.vue component within a modal box. This is working as intended at the moment. My issue is that I need for the updateTopic method within the AgendaTopic.vue to reload within the modal box instead of reloading the entire page. I'll admit that I'm totally new to vue and inertia. I've attempted to use various A.I. to help solve the issue, but I'm getting "Uncaught (in promise) TypeError: this.resolveComponent is not a function" when I try to issue the Inertia.put portion of the script. I would greatly appreciate any help available.
TopicController update method:
public function update(Request $request, Topic $topic)
{
$data = $request->all();
// Update topic fields
$topic->update([
'topic' => $request->input('topic'),
'description' => $request->input('description'),
'private_description' => $request->input('private_description'),
'is_private' => $request->input('is_private'),
]);
// Clear existing subtopics and re-save
$topic->subtopic()->delete();
foreach ($request->input('subTopics') as $subTopicData) {
$subTopic = $topic->subtopic()->create([
'subtopic' => $subTopicData['subtopic'],
'topic_id' => $topic->id,
]);
foreach ($subTopicData['subSubTopics'] as $subSubTopicData) {
$subTopic->subSubTopics()->create([
'subSubTopic' => $subSubTopicData['subSubTopic'],
'sub_topic_id' => $subTopic->id,
]);
}
}
// Update links
$topic->link()->where('is_private', 0)->delete(); // Remove existing public links
$topic->link()->where('is_private', 1)->delete(); // Remove existing private links
foreach ($data['links'] as $link) {
$topic->link()->create([
'link' => $link['link'],
'is_private' => 0,
'topic_id' => $topic->id,
]);
}
foreach ($data['privlinks'] as $link) {
$topic->link()->create([
'link' => $link['link'],
'is_private' => 1,
'topic_id' => $topic->id,
]);
}
// Update attachments
$topic->attachment()->where('is_private', 0)->delete(); // Remove existing public attachments
$topic->attachment()->where('is_private', 1)->delete(); // Remove existing private attachments
foreach ($data['attachments'] as $attachment) {
$topic->attachment()->create([
'attachment' => $attachment['attachment'],
'is_private' => 0,
'topic_id' => $topic->id,
]);
}
foreach ($data['privAttachments'] as $attachment) {
$topic->attachment()->create([
'attachment' => $attachment['attachment'],
'is_private' => 1,
'topic_id' => $topic->id,
]);
}
// Return Inertia response
return Inertia::render('Topics/Show', [
'topic' => new TopicResource($topic->load(['subtopic.subSubTopics', 'agenda.meeting.team'])),
'written' => 2,
]);
}
AgendaTopics.vue:
<script lang="ts" setup>
import { ref } from 'vue';
import { Link, useForm } from "@inertiajs/vue3";
import { VueDraggable } from 'vue-draggable-plus';
import { SortableEvent } from "sortablejs";
import TopicsShow from '@/Pages/Topics/Show.vue';
import { Inertia } from "@inertiajs/inertia";
const props = defineProps({
topics: Array,
editor: Number,
agendaId: Number,
});
const textClasses = '';
const layoutClasses = 'p-6 mb-2 rounded-xl text-left align-center transition duration-300 ease-in-out border-2 border-transparent';
const lightClasses = 'bg-black/5 text-gray-500';
const darkClasses = 'dark:bg-white/5 dark:text-gray-400 hover:bg-white/10 dark:hover:border-white/20';
const list = ref([...props.topics]);
const showModal = ref(false);
const selectedTopic = ref(null);
const form = useForm({
topics: list.value,
});
const openModal = (topic) => {
selectedTopic.value = topic;
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
selectedTopic.value = null;
};
const handleUpdateSuccess = () => {
Inertia.reload({
only: ['topic'],
onSuccess: ({ props }) => {
console.log('Updated Topic:', props.topic);
selectedTopic.value = props.topic;
showModal.value = true;
}
});
};
function onUpdate(event: SortableEvent) {
list.value.forEach((topic, index) => {
topic.topic_order = index + 1;
});
console.log('Order Updated', list.value);
form.topics = list.value;
form.put(route('topics.updateOrder'), {
preserveScroll: true,
onSuccess: () => {
console.log('Topics order updated successfully');
},
});
}
</script>
<template>
<div class="mb-6">
<ol class="list-[upper-roman] list-outside">
<VueDraggable ref="el" v-model="list" @update="onUpdate" v-if="editor === 1">
<div v-for="topic in list" :key="topic.id" @click="openModal(topic)">
<div :class="textClasses + ' ' + layoutClasses + ' ' + lightClasses + ' ' + darkClasses">
<li class="ml-6 indent-2">
<div>
<h4>{{ topic.topic }}</h4>
<article class="ml-6">{{ topic.description }}</article>
</div>
</li>
</div>
</div>
</VueDraggable>
<template v-else>
<div v-for="topic in list" :key="topic.id" @click="openModal(topic)">
<div :class="textClasses + ' ' + layoutClasses + ' ' + lightClasses + ' ' + darkClasses">
<li class="ml-6 indent-2">
<div>
<h4>{{ topic.topic }}</h4>
<article class="ml-6">{{ topic.description }}</article>
</div>
</li>
</div>
</div>
</template>
</ol>
</div>
<transition name="fade">
<div v-if="showModal" class="modal-open grid h-screen place-items-center">
<div class="modal-box absolute top-8 dark:bg-gray-800 bg-white max-w-screen-2xl">
<TopicsShow :topic="selectedTopic" @close="closeModal" @update-success="handleUpdateSuccess" />
<button @click="closeModal" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded">Close</button>
</div>
</div>
</transition>
</template>
<style scoped>
/* Add your styles here */
</style>
Show.vue:
<script setup>
import AgendaTopic from "@/Components/AgendaTopic.vue";
import {defineProps, defineEmits, ref} from 'vue';
const props = defineProps({
topic: Object,
written: String,
});
const emit = defineEmits(['close', 'update-success']);
const topic = ref(props.topic);
const handleUpdateSuccess = () => {
emit('update-success');
};
</script>
<template>
<button @click="$emit('close')">Close</button>
<AgendaTopic :topic="topic" :written="written" @update-success="handleUpdateSuccess"/>
</template>
<style scoped>
/* Add your styles here */
</style>
AgendaTopic.vue:
<script setup>
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import LinkRepeater from "@/Components/LinkRepeater.vue";
import TextArea from "@/Components/TextArea.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import moment from "moment";
import {router, useForm} from "@inertiajs/vue3";
import AttachmentRepeater from "@/Components/AttachmentRepeater.vue";
import ResourceButton from "@/Components/ResourceButton.vue";
import { EyeIcon, EyeSlashIcon, PencilSquareIcon, DocumentCheckIcon } from '@heroicons/vue/16/solid'
import { useConfirmDialog } from "@vueuse/core"
import DangerButton from "@/Components/DangerButton.vue";
import Checkbox from "@/Components/Checkbox.vue";
import SecureCheckbox from "@/Components/SecureCheckbox.vue";
import SubTopicRepeater from "@/Components/SubTopicRepeater.vue";
import { ref, watch } from "vue";
import { Inertia } from "@inertiajs/inertia";
const {
isRevealed, reveal, confirm, cancel, onReveal, onConfirm, onCancel
} = useConfirmDialog();
const props = defineProps({
topic: Object,
written: String,
icon: Function,
});
const topic = ref(props.topic);
watch(() => props.topic, (newTopic) => {
topic.value = newTopic;
}, { immediate: true });
const emit = defineEmits(['update-success']);
const textClasses = '';
const layoutClasses = 'p-6 mb-6 rounded-xl flex-col text-left align-center w-full';
const lightClasses = 'bg-black/5 text-gray-500';
const darkClasses = 'dark:bg-white/5 dark:text-gray-400';
const topicForm = useForm({
topic_id: props.topic.id,
topic: props.topic.topic,
description: props.topic.description,
private_description: props.topic.private_description,
subTopics: props.topic.subtopic.map(subtopic => ({
subtopic: subtopic.subtopic,
is_private: subtopic.is_private,
subSubTopics: subtopic.subSubTopics.map(subSubTopic => ({
subSubTopic: subSubTopic.subSubTopic,
is_private: subSubTopic.is_private,
})) || [],
editable: false
})) || [],
links: props.topic.link.filter(link => link.is_private === 0).map(link => ({
link: link.link,
editable: false
})) || [],
privlinks: props.topic.link.filter(link => link.is_private === 1).map(link => ({
link: link.link,
editable: false
})) || [],
attachments: props.topic.attachment.filter(attachment => attachment.is_private === 0).map(attachment => ({
attachment: attachment.attachment,
editable: false
})) || [],
privAttachments: props.topic.attachment.filter(attachment => attachment.is_private === 1).map(attachment => ({
attachment: attachment.attachment,
editable: false
})) || [],
is_private: props.topic.is_private,
});
const cancelSubTopic = (index) => {
topicForm.subTopics.splice(index, 1);
};
const addSubTopic = () => {
topicForm.subTopics.push({ subtopic: '', subSubTopics: [], editable: true });
};
const removeSubTopic = (index) => {
reveal();
onConfirm(() => {
topicForm.subTopics.splice(index, 1);
});
};
const addSubSubTopic = (subIndex) => {
topicForm.subTopics[subIndex].subSubTopics.push({ subSubTopic: '', editable: true });
};
const removeSubSubTopic = (subIndex, subSubIndex) => {
topicForm.subTopics[subIndex].subSubTopics.splice(subSubIndex, 1);
};
const cancelSubSubTopic = (subIndex, subSubIndex) => {
topicForm.subTopics[subIndex].subSubTopics.splice(subSubIndex, 1);
};
const cancelLink = (index) => {
topicForm.links.splice(index, 1);
};
const addLink = () => {
topicForm.links.push({ link: '', editable: true });
};
const removeLink = (index) => {
reveal();
onConfirm(() => {
topicForm.links.splice(index, 1);
});
};
const addPrivLink = () => {
topicForm.privlinks.push({ link: '', editable: true });
};
const removePrivLink = (index) => {
reveal();
onConfirm(() => {
topicForm.privlinks.splice(index, 1);
});
};
const cancelPrivLink = (index) => {
topicForm.privlinks.splice(index, 1);
};
const addAttachment = () => {
topicForm.attachments.push({ attachment: '', editable: true });
};
const removeAttachment = (index) => {
topicForm.attachments.splice(index, 1);
};
const addPrivAttachment = () => {
topicForm.privAttachments.push({ attachment: '', editable: true });
};
const removePrivAttachment = (index) => {
topicForm.privAttachments.splice(index, 1);
};
const updateTopic = () => {
console.log('Update Topic Called');
const payload = {
topic_id: topicForm.topic_id,
topic: topicForm.topic,
description: topicForm.description,
private_description: topicForm.private_description,
subTopics: topicForm.subTopics.map(subtopic => ({
subtopic: subtopic.subtopic,
is_private: subtopic.is_private,
subSubTopics: subtopic.subSubTopics.map(subSubTopic => ({
subSubTopic: subSubTopic.subSubTopic,
is_private: subSubTopic.is_private,
})),
})),
links: topicForm.links.map(linkObj => ({ link: linkObj.link, is_private: 0 })),
privlinks: topicForm.privlinks.map(linkObj => ({ link: linkObj.link, is_private: 1 })),
attachments: topicForm.attachments.map(attachmentObj => ({
attachment: attachmentObj.attachment,
is_private: 0
})),
privAttachments: topicForm.privAttachments.map(attachmentObj => ({
attachment: attachmentObj.attachment,
is_private: 1
})),
is_private: topicForm.is_private,
};
console.log('Payload:', payload);
Inertia.put(route('topics.update', { topic: topicForm.topic_id }), payload, {
preserveState: true,
onStart: () => {
console.log('Inertia request started');
},
onSuccess: ({ props }) => {
console.log('Inertia request successful, props:', props);
topic.value = props.topic;
emit('update-success');
},
onError: (errors) => {
console.error('Inertia request errors:', errors);
},
});
};
const deleteTopic = () => {
reveal();
onConfirm(() => {
router.delete(route('topics.destroy', props.topic.id));
});
};
</script>
<template>
<div v-if="written === '1'" class="p-6 mb-6 rounded-xl dark:bg-green-300/5 dark:text-green-400">
<span>Your topic was successfully added!</span>
</div>
<div v-if="written === '2'" class="p-6 mb-6 rounded-xl dark:bg-green-300/5 dark:text-green-400">
<span>Your topic was updated successfully!</span>
</div>
<div :class="textClasses + ' ' + layoutClasses + ' ' + lightClasses + ' ' + darkClasses">
<h2 class="text-2xl">{{ topic.topic }}</h2>
<h3>Meeting: {{ topic.agenda.meeting.meeting }}</h3>
<h3>Team: {{ topic.agenda.meeting.team.name }}</h3>
<h4>Created on: {{ moment(topic.created_at).format("LLL") }}</h4>
<form @submit.prevent="updateTopic" id="updateTopic">
<div class="my-6 text-right">
<SecureCheckbox id="sensitive" value="String" class="size-6" v-model="topicForm.is_private"
:true-value="1" :false-value="0"/>
<label class="ml-2" for="sensitive"
v-text="topicForm.is_private ? 'This topic contains sensitive information.' : 'Mark topic as sensitive'"></label>
</div>
<div id="topic_form" class="mt-6">
<InputLabel for="topic" class="mb-3">Topic</InputLabel>
<TextInput id="topic" v-model="topicForm.topic" class="mb-6"/>
<InputLabel for="description" class="mb-3">Topic Details</InputLabel>
<TextArea id="description" v-model="topicForm.description" rows="24" class="mb-6"/>
<div id="private_description" v-show="topicForm.is_private"
class="dark:bg-red-500/20 py-6 px-6 mx-[-1.5rem] mb-6">
<InputLabel for="private_description" class="mb-3">Private Details</InputLabel>
<TextArea id="private_description" v-model="topicForm.private_description" rows="24"/>
</div>
<ol class="mb-2 items-center list-[upper-alpha] ml-6 mr-6 w-full">
<li v-for="(subtopic, index) in topicForm.subTopics" :key="index">
<SubTopicRepeater :id="'subTopic' + index" v-model="subtopic.subtopic"
:subSubTopics="subtopic.subSubTopics" :editable="subtopic.editable"
@addSubSubTopic="addSubSubTopic(index)" @remove="removeSubTopic(index)"
@cancel="cancelSubTopic(index)"/>
</li>
</ol>
<ResourceButton type="button" @click="addSubTopic" class="mt-2">Add a Sub-Topic</ResourceButton>
<div id="public_links" class="mb-6">
<h5>Public Links</h5>
<template v-for="(link, index) in topicForm.links" :key="index">
<div class="mb-2 flex items-center">
<LinkRepeater :id="'pubLink' + index" v-model="link.link" :editable="link.editable"
@remove="removeLink(index)" @cancelLink="cancelLink(index)"/>
</div>
</template>
<template v-if="topicForm.is_private">
<ResourceButton type="button" @click="addLink" class="mt-2" :icon="EyeIcon">Add a Public Link
</ResourceButton>
</template>
<template v-else>
<ResourceButton type="button" @click="addLink" class="mt-2">Add a Link</ResourceButton>
</template>
</div>
<div id="private_links" v-show="topicForm.is_private"
class="dark:bg-red-500/20 py-6 px-6 mx-[-1.5rem] mb-6">
<h5>Private Links</h5>
<template v-for="(link, index) in topicForm.privlinks" :key="index">
<div class="mb-2 flex items-center">
<LinkRepeater :id="'privLink' + index" v-model="link.link" :editable="link.editable"
@remove="removePrivLink(index)" @cancelLink="cancelPrivLink(index)"/>
</div>
</template>
<ResourceButton type="button" @click="addPrivLink" class="mt-2" :icon="EyeSlashIcon">Add Private
Link
</ResourceButton>
</div>
<div id="public_docs" class="mb-6">
<h5>Public Documents</h5>
<template v-for="(doc, index) in topicForm.attachments" :key="index">
<div class="mb-2 flex items-center">
<AttachmentRepeater :id="'pubLink' + index" v-model="doc.attachment"
:editable="doc.editable" @remove="removeAttachment(index)"/>
</div>
</template>
<template v-if="topicForm.is_private">
<ResourceButton type="button" @click="addAttachment" class="mt-2" :icon="EyeIcon">Add a Public
Document
</ResourceButton>
</template>
<template v-else>
<ResourceButton type="button" @click="addAttachment" class="mt-2">Add a Document
</ResourceButton>
</template>
</div>
<div id="private_docs" v-show="topicForm.is_private"
class="dark:bg-red-500/20 py-6 px-6 mx-[-1.5rem] mb-6">
<h5>Private Documents</h5>
<template v-for="(doc, index) in topicForm.privAttachments" :key="index">
<div class="mb-2 flex items-center">
<AttachmentRepeater :id="'pubLink' + index" v-model="doc.attachment"
:editable="doc.editable" @remove="removePrivAttachment(index)"/>
</div>
</template>
<ResourceButton type="button" @click="addPrivAttachment" class="mt-2" :icon="EyeSlashIcon">Add a
Private Document
</ResourceButton>
</div>
</div>
<!-- Prevent implicit submission of the form -->
<button type="submit" disabled style="display: none" aria-hidden="true"></button>
</form>
<form @submit.prevent="deleteTopic" id="deleteTopic"></form>
<div>
<!-- Actually submit -->
<PrimaryButton type="submit" form="updateTopic" class="mt-3">Save Changes to Topic</PrimaryButton>
<DangerButton form="deleteTopic" type="submit" class="ml-6">Delete Topic</DangerButton>
</div>
</div>
<div class="modal" :class="{'modal-open': isRevealed}">
<div class="modal-box fixed top-8 dark:bg-gray-800 bg-white">
<h1 class="text-2xl mb-6">Confirm Delete</h1>
<p>Are you sure you wish to remove this item?</p>
<div class="modal-action">
<ResourceButton @click="cancel">No</ResourceButton>
<DangerButton @click="confirm">Yes</DangerButton>
</div>
</div>
</div>
</template>
<style scoped>
/* Add your styles here */
</style>
app.js:
import './bootstrap';
import '../css/app.css';
import.meta.glob([
'../images/**'
]);
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
import VueTailwindDatepicker from 'vue-tailwind-datepicker';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.component('VueTailwindDatepicker', VueTailwindDatepicker)
.mount(el);
},
progress: {
color: '#4B5563',
},
});