diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..e7f7c94 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + abstract class Controller { - // + use AuthorizesRequests; } diff --git a/app/Http/Controllers/IdeaController.php b/app/Http/Controllers/IdeaController.php index c47b40a..c60a214 100644 --- a/app/Http/Controllers/IdeaController.php +++ b/app/Http/Controllers/IdeaController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Requests\StoreIdeaRequest; +use App\Http\Requests\UpdateIdeaRequest; use App\Http\Resources\CommentResource; use App\Http\Resources\IdeaResource; use App\Models\Idea; @@ -34,4 +35,29 @@ public function store(StoreIdeaRequest $request): RedirectResponse return redirect()->route('dashboard')->with('status', 'Your feedback has been submitted!'); } + + public function edit(Idea $idea): Response + { + $this->authorize('update', $idea); + + return inertia('Ideas/Edit', [ + 'idea' => new IdeaResource($idea), + ]); + } + + public function update(UpdateIdeaRequest $request, Idea $idea): RedirectResponse + { + $idea->update($request->only(['title', 'description'])); + + return redirect()->route('feedback.show', $idea)->with('status', 'Your feedback has been updated!'); + } + + public function destroy(Idea $idea): RedirectResponse + { + $this->authorize('delete', $idea); + + $idea->delete(); + + return redirect()->route('dashboard')->with('status', 'Your feedback has been deleted!'); + } } diff --git a/app/Http/Requests/UpdateIdeaRequest.php b/app/Http/Requests/UpdateIdeaRequest.php new file mode 100644 index 0000000..7bc26c4 --- /dev/null +++ b/app/Http/Requests/UpdateIdeaRequest.php @@ -0,0 +1,24 @@ +user()->can('update', $this->route('idea')); + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string', 'max:5000'], + ]; + } +} diff --git a/app/Http/Resources/IdeaResource.php b/app/Http/Resources/IdeaResource.php index 9109848..48c4824 100644 --- a/app/Http/Resources/IdeaResource.php +++ b/app/Http/Resources/IdeaResource.php @@ -33,6 +33,10 @@ public function toArray(Request $request): array ])->values(), ), 'is_subscribed' => $user ? $this->subscribers->contains('id', $user->id) : false, + 'can' => [ + 'update' => $user ? $user->can('update', $this->resource) : false, + 'delete' => $user ? $user->can('delete', $this->resource) : false, + ], 'user' => new UserResource($this->whenLoaded('user')), 'comments_count' => $this->whenCounted('comments'), 'latest_status_update' => new IdeaStatusUpdateResource($this->whenLoaded('latestStatusUpdate')), diff --git a/app/Policies/IdeaPolicy.php b/app/Policies/IdeaPolicy.php new file mode 100644 index 0000000..d84a616 --- /dev/null +++ b/app/Policies/IdeaPolicy.php @@ -0,0 +1,19 @@ +id === $idea->user_id; + } + + public function delete(User $user, Idea $idea): bool + { + return $user->id === $idea->user_id; + } +} diff --git a/resources/js/Pages/Ideas/Edit.vue b/resources/js/Pages/Ideas/Edit.vue new file mode 100644 index 0000000..f4c5c5b --- /dev/null +++ b/resources/js/Pages/Ideas/Edit.vue @@ -0,0 +1,60 @@ + + + diff --git a/resources/js/Pages/Ideas/Show.vue b/resources/js/Pages/Ideas/Show.vue index ee0f5b1..a8bd1d1 100644 --- a/resources/js/Pages/Ideas/Show.vue +++ b/resources/js/Pages/Ideas/Show.vue @@ -10,6 +10,7 @@ import CommentForm from '@/Components/CommentForm.vue'; import { dashboard } from '@/routes'; import { store } from '@/actions/App/Http/Controllers/CommentController'; import { store as subscribe, destroy as unsubscribe } from '@/routes/account/notifications'; +import { edit, destroy } from '@/routes/feedback'; const props = defineProps({ idea: { @@ -26,6 +27,14 @@ const user = computed(() => usePage().props.auth?.user ?? null); const subscriptionForm = useForm({}); +const deleteForm = useForm({}); + +const deleteIdea = () => { + if (window.confirm('Are you sure you want to delete this feedback? This cannot be undone.')) { + deleteForm.delete(destroy.url(props.idea.id)); + } +}; + const toggleSubscription = () => { if (props.idea.is_subscribed) { subscriptionForm.delete(unsubscribe.url(props.idea.id), { preserveScroll: true }); @@ -62,6 +71,25 @@ const formatDate = (dateString) => { Submitted by {{ idea.user.name }} on {{ formatDate(idea.created_at) }}

+
+ + Edit + + +
+

{{ idea.latest_status_update.message }}

diff --git a/resources/js/actions/App/Http/Controllers/IdeaController.ts b/resources/js/actions/App/Http/Controllers/IdeaController.ts index 1a87918..0a23785 100644 --- a/resources/js/actions/App/Http/Controllers/IdeaController.ts +++ b/resources/js/actions/App/Http/Controllers/IdeaController.ts @@ -1,7 +1,7 @@ import { queryParams, type RouteQueryOptions, type RouteDefinition, applyUrlDefaults } from './../../../../wayfinder' /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -16,7 +16,7 @@ create.definition = { /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.url = (options?: RouteQueryOptions) => { @@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => { /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ @@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ @@ -60,7 +60,7 @@ store.definition = { /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ store.url = (options?: RouteQueryOptions) => { @@ -69,7 +69,7 @@ store.url = (options?: RouteQueryOptions) => { /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ @@ -77,9 +77,193 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ method: 'post', }) +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +export const edit = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: edit.url(args, options), + method: 'get', +}) + +edit.definition = { + methods: ["get","head"], + url: '/feedback/{idea}/edit', +} satisfies RouteDefinition<["get","head"]> + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return edit.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.get = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: edit.url(args, options), + method: 'get', +}) + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.head = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'head'> => ({ + url: edit.url(args, options), + method: 'head', +}) + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +export const update = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'put'> => ({ + url: update.url(args, options), + method: 'put', +}) + +update.definition = { + methods: ["put"], + url: '/feedback/{idea}', +} satisfies RouteDefinition<["put"]> + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +update.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return update.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +update.put = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'put'> => ({ + url: update.url(args, options), + method: 'put', +}) + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +export const destroy = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({ + url: destroy.url(args, options), + method: 'delete', +}) + +destroy.definition = { + methods: ["delete"], + url: '/feedback/{idea}', +} satisfies RouteDefinition<["delete"]> + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +destroy.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return destroy.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +destroy.delete = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({ + url: destroy.url(args, options), + method: 'delete', +}) + /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ export const show = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -94,7 +278,7 @@ show.definition = { /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { @@ -127,7 +311,7 @@ show.url = (args: { idea: number | { id: number } } | [idea: number | { id: numb /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.get = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -137,7 +321,7 @@ show.get = (args: { idea: number | { id: number } } | [idea: number | { id: numb /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.head = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'head'> => ({ @@ -145,6 +329,6 @@ show.head = (args: { idea: number | { id: number } } | [idea: number | { id: num method: 'head', }) -const IdeaController = { create, store, show } +const IdeaController = { create, store, edit, update, destroy, show } export default IdeaController \ No newline at end of file diff --git a/resources/js/routes/feedback/index.ts b/resources/js/routes/feedback/index.ts index 166d7cc..faa3c32 100644 --- a/resources/js/routes/feedback/index.ts +++ b/resources/js/routes/feedback/index.ts @@ -2,7 +2,7 @@ import { queryParams, type RouteQueryOptions, type RouteDefinition, applyUrlDefa import comments from './comments' /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -17,7 +17,7 @@ create.definition = { /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.url = (options?: RouteQueryOptions) => { @@ -26,7 +26,7 @@ create.url = (options?: RouteQueryOptions) => { /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -36,7 +36,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ /** * @see \App\Http\Controllers\IdeaController::create -* @see app/Http/Controllers/IdeaController.php:26 +* @see app/Http/Controllers/IdeaController.php:27 * @route '/feedback/create' */ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ @@ -46,7 +46,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ @@ -61,7 +61,7 @@ store.definition = { /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ store.url = (options?: RouteQueryOptions) => { @@ -70,7 +70,7 @@ store.url = (options?: RouteQueryOptions) => { /** * @see \App\Http\Controllers\IdeaController::store -* @see app/Http/Controllers/IdeaController.php:31 +* @see app/Http/Controllers/IdeaController.php:32 * @route '/feedback' */ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ @@ -78,6 +78,190 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ method: 'post', }) +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +export const edit = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: edit.url(args, options), + method: 'get', +}) + +edit.definition = { + methods: ["get","head"], + url: '/feedback/{idea}/edit', +} satisfies RouteDefinition<["get","head"]> + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return edit.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.get = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: edit.url(args, options), + method: 'get', +}) + +/** +* @see \App\Http\Controllers\IdeaController::edit +* @see app/Http/Controllers/IdeaController.php:39 +* @route '/feedback/{idea}/edit' +*/ +edit.head = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'head'> => ({ + url: edit.url(args, options), + method: 'head', +}) + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +export const update = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'put'> => ({ + url: update.url(args, options), + method: 'put', +}) + +update.definition = { + methods: ["put"], + url: '/feedback/{idea}', +} satisfies RouteDefinition<["put"]> + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +update.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return update.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::update +* @see app/Http/Controllers/IdeaController.php:48 +* @route '/feedback/{idea}' +*/ +update.put = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'put'> => ({ + url: update.url(args, options), + method: 'put', +}) + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +export const destroy = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({ + url: destroy.url(args, options), + method: 'delete', +}) + +destroy.definition = { + methods: ["delete"], + url: '/feedback/{idea}', +} satisfies RouteDefinition<["delete"]> + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +destroy.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { + if (typeof args === 'string' || typeof args === 'number') { + args = { idea: args } + } + + if (typeof args === 'object' && !Array.isArray(args) && 'id' in args) { + args = { idea: args.id } + } + + if (Array.isArray(args)) { + args = { + idea: args[0], + } + } + + args = applyUrlDefaults(args) + + const parsedArgs = { + idea: typeof args.idea === 'object' + ? args.idea.id + : args.idea, + } + + return destroy.definition.url + .replace('{idea}', parsedArgs.idea.toString()) + .replace(/\/+$/, '') + queryParams(options) +} + +/** +* @see \App\Http\Controllers\IdeaController::destroy +* @see app/Http/Controllers/IdeaController.php:55 +* @route '/feedback/{idea}' +*/ +destroy.delete = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({ + url: destroy.url(args, options), + method: 'delete', +}) + /** * @see \App\Http\Controllers\VoteController::__invoke * @see app/Http/Controllers/VoteController.php:11 @@ -196,7 +380,7 @@ react.post = (args: { idea: number | { id: number } } | [idea: number | { id: nu /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ export const show = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -211,7 +395,7 @@ show.definition = { /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.url = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions) => { @@ -244,7 +428,7 @@ show.url = (args: { idea: number | { id: number } } | [idea: number | { id: numb /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.get = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({ @@ -254,7 +438,7 @@ show.get = (args: { idea: number | { id: number } } | [idea: number | { id: numb /** * @see \App\Http\Controllers\IdeaController::show -* @see app/Http/Controllers/IdeaController.php:14 +* @see app/Http/Controllers/IdeaController.php:15 * @route '/feedback/{idea}' */ show.head = (args: { idea: number | { id: number } } | [idea: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'head'> => ({ @@ -330,6 +514,9 @@ unsubscribe.head = (args: { idea: number | { id: number }, user: number | { id: const feedback = { create: Object.assign(create, create), store: Object.assign(store, store), + edit: Object.assign(edit, edit), + update: Object.assign(update, update), + destroy: Object.assign(destroy, destroy), vote: Object.assign(vote, vote), react: Object.assign(react, react), comments: Object.assign(comments, comments), diff --git a/routes/web.php b/routes/web.php index 522d216..7bb0573 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,10 @@ Route::get('/feedback/create', [IdeaController::class, 'create'])->name('feedback.create'); Route::post('/feedback', [IdeaController::class, 'store'])->name('feedback.store'); + Route::get('/feedback/{idea}/edit', [IdeaController::class, 'edit'])->name('feedback.edit'); + Route::put('/feedback/{idea}', [IdeaController::class, 'update'])->name('feedback.update'); + Route::delete('/feedback/{idea}', [IdeaController::class, 'destroy'])->name('feedback.destroy'); + Route::post('/feedback/{idea}/vote', VoteController::class)->name('feedback.vote'); Route::post('/feedback/{idea}/reactions', ReactionController::class)->name('feedback.react'); Route::post('/feedback/{idea}/comments', [CommentController::class, 'store'])->name('feedback.comments.store'); diff --git a/tests/Feature/Ideas/DeleteIdeaTest.php b/tests/Feature/Ideas/DeleteIdeaTest.php new file mode 100644 index 0000000..c5f3e02 --- /dev/null +++ b/tests/Feature/Ideas/DeleteIdeaTest.php @@ -0,0 +1,36 @@ +create(); + + $this->delete(route('feedback.destroy', $idea)) + ->assertRedirect(route('login')); + + $this->assertDatabaseHas('ideas', ['id' => $idea->id]); +}); + +it('forbids a non-author from deleting', function () { + $idea = Idea::factory()->create(); + + $this->actingAs(User::factory()->create()) + ->delete(route('feedback.destroy', $idea)) + ->assertForbidden(); + + $this->assertDatabaseHas('ideas', ['id' => $idea->id]); +}); + +it('deletes the idea for the author', function () { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->delete(route('feedback.destroy', $idea)) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('status'); + + $this->assertDatabaseMissing('ideas', ['id' => $idea->id]); +}); diff --git a/tests/Feature/Ideas/UpdateIdeaTest.php b/tests/Feature/Ideas/UpdateIdeaTest.php new file mode 100644 index 0000000..245312f --- /dev/null +++ b/tests/Feature/Ideas/UpdateIdeaTest.php @@ -0,0 +1,108 @@ + [ + 'title' => 'Updated title', + 'description' => 'Updated description for the feedback.', +]; + +// --------------------------------------------------------------------------- +// Auth guard +// --------------------------------------------------------------------------- + +it('redirects guests to login', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->put(route('feedback.update', $idea), $validPayload()) + ->assertRedirect(route('login')); +}); + +it('shows the edit form to the author', function () { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->get(route('feedback.edit', $idea)) + ->assertOk() + ->assertInertia(fn ($page) => $page->component('Ideas/Edit')); +}); + +// --------------------------------------------------------------------------- +// Authorization +// --------------------------------------------------------------------------- + +it('forbids a non-author from viewing the edit form', function () { + $idea = Idea::factory()->create(); + + $this->actingAs(User::factory()->create()) + ->get(route('feedback.edit', $idea)) + ->assertForbidden(); +}); + +it('forbids a non-author from updating', function () use ($validPayload) { + $idea = Idea::factory()->create(['title' => 'Original title']); + + $this->actingAs(User::factory()->create()) + ->put(route('feedback.update', $idea), $validPayload()) + ->assertForbidden(); + + $this->assertDatabaseHas('ideas', ['id' => $idea->id, 'title' => 'Original title']); +}); + +// --------------------------------------------------------------------------- +// Successful update +// --------------------------------------------------------------------------- + +it('updates the idea for the author', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->put(route('feedback.update', $idea), $validPayload()) + ->assertRedirect(route('feedback.show', $idea)) + ->assertSessionHas('status'); + + $this->assertDatabaseHas('ideas', [ + 'id' => $idea->id, + 'title' => 'Updated title', + 'description' => 'Updated description for the feedback.', + ]); +}); + +// --------------------------------------------------------------------------- +// Validation +// --------------------------------------------------------------------------- + +it('requires a title', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->put(route('feedback.update', $idea), array_merge($validPayload(), ['title' => ''])) + ->assertSessionHasErrors('title'); +}); + +it('requires a description', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->put(route('feedback.update', $idea), array_merge($validPayload(), ['description' => ''])) + ->assertSessionHasErrors('description'); +}); + +it('rejects a title longer than 255 characters', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->put(route('feedback.update', $idea), array_merge($validPayload(), ['title' => str_repeat('a', 256)])) + ->assertSessionHasErrors('title'); +}); + +it('rejects a description longer than 5000 characters', function () use ($validPayload) { + $idea = Idea::factory()->create(); + + $this->actingAs($idea->user) + ->put(route('feedback.update', $idea), array_merge($validPayload(), ['description' => str_repeat('a', 5001)])) + ->assertSessionHasErrors('description'); +});