fixed content module ( well reverted)
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Content\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\View as ViewFacade;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Http\Requests\SaveContentDocumentRequest;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
use Modules\Content\Models\ContentType;
|
||||
|
||||
@@ -18,112 +16,131 @@ final class ContentDocumentController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$documents = ContentDocument::query()->with(['type','placements'])
|
||||
->when($request->filled('search'), fn ($q) => $q->where(fn ($q) => $q->where('title','like','%'.$request->string('search').'%')->orWhere('slug','like','%'.$request->string('search').'%')))
|
||||
->when($request->filled('type'), fn ($q) => $q->whereHas('type', fn ($q) => $q->where('key',$request->string('type'))))
|
||||
->when($request->filled('status'), fn ($q) => $q->where('status',$request->string('status')))
|
||||
->latest()->paginate(25)->withQueryString();
|
||||
return view('content::admin.index', ['documents'=>$documents, 'types'=>ContentType::orderBy('name')->get()]);
|
||||
$documents = ContentDocument::query()
|
||||
->with('type')
|
||||
->withCount('placements')
|
||||
->when($request->string('type')->isNotEmpty(), fn ($query) => $query->whereHas('type', fn ($type) => $type->where('key', $request->string('type')->toString())))
|
||||
->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString()))
|
||||
->when($request->string('search')->isNotEmpty(), fn ($query) => $query->where('title', 'like', '%'.$request->string('search')->toString().'%'))
|
||||
->orderBy('title')
|
||||
->paginate(30)
|
||||
->withQueryString();
|
||||
|
||||
return view('content::admin.index', [
|
||||
'documents' => $documents,
|
||||
'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$types = ContentType::orderBy('name')->get();
|
||||
$types = ContentType::query()->where('is_active', true)->orderBy('name')->get();
|
||||
$selectedType = $types->firstWhere('id', (int) $request->integer('type')) ?? $types->first();
|
||||
return view('content::admin.form', ['document'=>new ContentDocument(['status'=>'draft','content_type_id'=>$selectedType?->id]), 'types'=>$types, 'selectedType'=>$selectedType]);
|
||||
|
||||
return view('content::admin.form', [
|
||||
'document' => new ContentDocument(['status' => 'draft', 'data' => []]),
|
||||
'types' => $types,
|
||||
'selectedType' => $selectedType,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(SaveContentDocumentRequest $request): RedirectResponse
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$document = ContentDocument::create($request->payload());
|
||||
return redirect()->route('content.documents.edit',$document)->with('status','Content document created.');
|
||||
$validated = $this->validateDocument($request);
|
||||
$type = ContentType::query()->where('is_active', true)->findOrFail($validated['content_type_id']);
|
||||
|
||||
$document = ContentDocument::query()->create([
|
||||
'content_type_id' => $type->id,
|
||||
'title' => $validated['title'],
|
||||
'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title']),
|
||||
'status' => $validated['status'],
|
||||
'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? now()) : null,
|
||||
'expires_at' => $validated['expires_at'] ?? null,
|
||||
'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR),
|
||||
'created_by' => $request->user()?->id,
|
||||
'updated_by' => $request->user()?->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('content.documents.edit', $document)->with('status', 'Content document created.');
|
||||
}
|
||||
|
||||
public function edit(ContentDocument $document): View
|
||||
{
|
||||
$document->load('placements');
|
||||
return view('content::admin.form', ['document'=>$document, 'types'=>ContentType::orderBy('name')->get(), 'selectedType'=>$document->type()->first()]);
|
||||
}
|
||||
$document->load('type', 'placements');
|
||||
|
||||
public function update(SaveContentDocumentRequest $request, ContentDocument $document): RedirectResponse
|
||||
{
|
||||
$document->update($request->payload());
|
||||
return back()->with('status','Content document saved.');
|
||||
}
|
||||
|
||||
public function autosave(Request $request, ContentDocument $document): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'content_type_id' => ['required', 'integer', 'exists:content_types,id'],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:draft,published,archived'],
|
||||
'data_json' => ['required', 'json'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
'expires_at' => ['nullable', 'date', 'after:published_at'],
|
||||
return view('content::admin.form', [
|
||||
'document' => $document,
|
||||
'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(),
|
||||
'selectedType' => $document->type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, ContentDocument $document): RedirectResponse
|
||||
{
|
||||
$validated = $this->validateDocument($request, $document);
|
||||
|
||||
$document->update([
|
||||
'content_type_id' => $validated['content_type_id'],
|
||||
'title' => $validated['title'],
|
||||
'slug' => filled($validated['slug'] ?? null) ? $validated['slug'] : str($validated['title'])->slug(),
|
||||
'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title'], $document),
|
||||
'status' => $validated['status'],
|
||||
'data' => json_decode($validated['data_json'], true, flags: JSON_THROW_ON_ERROR),
|
||||
'published_at' => $validated['published_at'] ?? null,
|
||||
'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? $document->published_at ?? now()) : null,
|
||||
'expires_at' => $validated['expires_at'] ?? null,
|
||||
'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR),
|
||||
'updated_by' => $request->user()?->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'saved' => true,
|
||||
'saved_at' => now()->toIso8601String(),
|
||||
'updated_at' => $document->fresh()->updated_at?->toIso8601String(),
|
||||
]);
|
||||
return back()->with('status', 'Content document updated.');
|
||||
}
|
||||
|
||||
public function preview(Request $request): JsonResponse
|
||||
public function duplicate(Request $request, ContentDocument $document): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'content_type_id' => ['required', 'integer', 'exists:content_types,id'],
|
||||
'data_json' => ['required', 'json'],
|
||||
]);
|
||||
|
||||
$type = ContentType::query()->findOrFail($validated['content_type_id']);
|
||||
$view = 'stylist-theme-saasland::documents.'.$type->key;
|
||||
|
||||
if (! ViewFacade::exists($view)) {
|
||||
return response()->json([
|
||||
'html' => '<div style="padding:2rem;font-family:system-ui;color:#71717a">No Saasland renderer exists for this content type yet.</div>',
|
||||
'view' => $view,
|
||||
]);
|
||||
}
|
||||
|
||||
$data = json_decode($validated['data_json'], true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
return response()->json([
|
||||
'html' => ViewFacade::make($view, array_merge($data, [
|
||||
'data' => $data,
|
||||
'document' => new ContentDocument(['data' => $data]),
|
||||
'placement' => null,
|
||||
'preview' => true,
|
||||
]))->render(),
|
||||
'view' => $view,
|
||||
]);
|
||||
}
|
||||
|
||||
public function duplicate(ContentDocument $document): RedirectResponse
|
||||
{
|
||||
$copy = $document->replicate(['slug']);
|
||||
$copy = $document->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']);
|
||||
$copy->title = $document->title.' copy';
|
||||
$copy->slug = str($copy->title)->slug().'-'.str()->lower(str()->random(6));
|
||||
$copy->slug = $this->uniqueSlug($copy->title);
|
||||
$copy->status = 'draft';
|
||||
$copy->published_at = null;
|
||||
$copy->created_by = $request->user()?->id;
|
||||
$copy->updated_by = $request->user()?->id;
|
||||
$copy->save();
|
||||
return redirect()->route('content.documents.edit',$copy)->with('status','Independent copy created.');
|
||||
|
||||
return redirect()->route('content.documents.edit', $copy)->with('status', 'Independent draft copy created.');
|
||||
}
|
||||
|
||||
public function destroy(ContentDocument $document): RedirectResponse
|
||||
{
|
||||
abort_if($document->placements()->exists(), 422, 'A document used by a page cannot be deleted.');
|
||||
if ($document->placements()->exists()) {
|
||||
return back()->withErrors(['document' => 'Remove this document from every page before deleting it.']);
|
||||
}
|
||||
|
||||
$document->delete();
|
||||
return redirect()->route('content.index')->with('status','Content document deleted.');
|
||||
|
||||
return redirect()->route('content.index')->with('status', 'Content document deleted.');
|
||||
}
|
||||
|
||||
private function validateDocument(Request $request, ?ContentDocument $document = null): array
|
||||
{
|
||||
return $request->validate([
|
||||
'content_type_id' => ['required', 'integer', Rule::exists('content_types', 'id')->where('is_active', true)],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', Rule::in(['draft', 'published'])],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
'expires_at' => ['nullable', 'date', 'after:published_at'],
|
||||
'data_json' => ['required', 'json'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function uniqueSlug(string $value, ?ContentDocument $except = null): string
|
||||
{
|
||||
$base = Str::slug($value) ?: 'content-document';
|
||||
$slug = $base;
|
||||
$suffix = 2;
|
||||
|
||||
while (ContentDocument::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) {
|
||||
$slug = $base.'-'.$suffix++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Content\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
use Modules\Content\Models\PageContent;
|
||||
|
||||
final class PageComposerController extends Controller
|
||||
{
|
||||
public function edit(string $pageKey): View
|
||||
{
|
||||
return view('content::admin.pages.edit', [
|
||||
'pageKey'=>$pageKey,
|
||||
'placements'=>PageContent::with('document.type')->where('page_key',$pageKey)->orderBy('region')->orderBy('sort_order')->get()->groupBy('region'),
|
||||
'documents'=>ContentDocument::with('type')->orderBy('title')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, string $pageKey): RedirectResponse
|
||||
{
|
||||
$data=$request->validate(['content_document_id'=>['required','exists:content_documents,id'],'region'=>['required',Rule::in(['header','body','footer'])]]);
|
||||
$data['page_key']=$pageKey; $data['is_enabled']=true;
|
||||
$data['sort_order']=(int) PageContent::where('page_key',$pageKey)->where('region',$data['region'])->max('sort_order')+1;
|
||||
PageContent::create($data);
|
||||
return back()->with('status','Placement added.');
|
||||
}
|
||||
|
||||
public function update(Request $request, string $pageKey, PageContent $placement): RedirectResponse
|
||||
{
|
||||
abort_unless($placement->page_key===$pageKey,404);
|
||||
$placement->update($request->validate(['region'=>['sometimes',Rule::in(['header','body','footer'])],'is_enabled'=>['sometimes','boolean'],'published_at'=>['nullable','date'],'expires_at'=>['nullable','date','after:published_at']]));
|
||||
return back()->with('status','Placement updated.');
|
||||
}
|
||||
|
||||
public function destroy(string $pageKey, PageContent $placement): RedirectResponse
|
||||
{
|
||||
abort_unless($placement->page_key===$pageKey,404); $placement->delete();
|
||||
return back()->with('status','Placement removed.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request, string $pageKey): JsonResponse
|
||||
{
|
||||
$data=$request->validate(['region'=>['required',Rule::in(['header','body','footer'])],'ids'=>['required','array'],'ids.*'=>['integer']]);
|
||||
DB::transaction(function() use($data,$pageKey){ foreach($data['ids'] as $i=>$id){ PageContent::whereKey($id)->where('page_key',$pageKey)->where('region',$data['region'])->update(['sort_order'=>$i]); }});
|
||||
return response()->json(['saved'=>true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Content\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
use Modules\Content\Models\PageContent;
|
||||
|
||||
final class PageCompositionController extends Controller
|
||||
{
|
||||
private const REGIONS = ['header', 'body', 'footer'];
|
||||
|
||||
public function edit(string $pageKey): View
|
||||
{
|
||||
$placements = PageContent::query()
|
||||
->with('document.type')
|
||||
->where('page_key', $pageKey)
|
||||
->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->groupBy('region');
|
||||
|
||||
$documents = ContentDocument::query()
|
||||
->with('type')
|
||||
->withCount('placements')
|
||||
->orderBy('title')
|
||||
->get();
|
||||
|
||||
return view('content::admin.composition', compact('pageKey', 'placements', 'documents'));
|
||||
}
|
||||
|
||||
public function preview(string $pageKey): View
|
||||
{
|
||||
abort_unless($pageKey === 'home', 404, 'This phase currently provides the Saasland homepage preview.');
|
||||
|
||||
return view('stylist-theme-saasland::home', ['pageKey' => $pageKey]);
|
||||
}
|
||||
|
||||
public function store(Request $request, string $pageKey): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'content_document_id' => ['required', 'integer', Rule::exists('content_documents', 'id')->whereNull('deleted_at')],
|
||||
'region' => ['required', Rule::in(self::REGIONS)],
|
||||
]);
|
||||
|
||||
$document = ContentDocument::query()->with('type')->findOrFail($validated['content_document_id']);
|
||||
abort_unless(in_array($validated['region'], $document->type?->allowed_regions ?? [], true), 422, 'This content type is not allowed in the selected region.');
|
||||
|
||||
$nextOrder = ((int) PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->max('sort_order')) + 10;
|
||||
PageContent::query()->create([
|
||||
'page_key' => $pageKey,
|
||||
'content_document_id' => $document->id,
|
||||
'region' => $validated['region'],
|
||||
'sort_order' => $nextOrder,
|
||||
'is_enabled' => true,
|
||||
]);
|
||||
|
||||
return back()->with('status', 'Document added to the '.$validated['region'].' region.');
|
||||
}
|
||||
|
||||
public function update(Request $request, PageContent $placement): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'is_enabled' => ['nullable', 'boolean'],
|
||||
'starts_at' => ['nullable', 'date'],
|
||||
'ends_at' => ['nullable', 'date', 'after:starts_at'],
|
||||
]);
|
||||
|
||||
$placement->update([
|
||||
'is_enabled' => $request->boolean('is_enabled'),
|
||||
'starts_at' => $validated['starts_at'] ?? null,
|
||||
'ends_at' => $validated['ends_at'] ?? null,
|
||||
]);
|
||||
|
||||
return back()->with('status', 'Page placement updated.');
|
||||
}
|
||||
|
||||
public function destroy(PageContent $placement): RedirectResponse
|
||||
{
|
||||
$placement->delete();
|
||||
$this->normaliseRegion($placement->page_key, $placement->region);
|
||||
|
||||
return back()->with('status', 'Document removed from this page. The reusable document was not deleted.');
|
||||
}
|
||||
|
||||
public function duplicate(Request $request, PageContent $placement): RedirectResponse
|
||||
{
|
||||
$source = $placement->document()->firstOrFail();
|
||||
$copy = DB::transaction(function () use ($request, $placement, $source): ContentDocument {
|
||||
$copy = $source->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']);
|
||||
$copy->title = $source->title.' copy';
|
||||
$copy->slug = $source->slug.'-copy-'.strtolower(str()->random(6));
|
||||
$copy->status = 'draft';
|
||||
$copy->published_at = null;
|
||||
$copy->created_by = $request->user()?->id;
|
||||
$copy->updated_by = $request->user()?->id;
|
||||
$copy->save();
|
||||
|
||||
PageContent::query()->create([
|
||||
'page_id' => $placement->page_id,
|
||||
'page_key' => $placement->page_key,
|
||||
'content_document_id' => $copy->id,
|
||||
'region' => $placement->region,
|
||||
'sort_order' => $placement->sort_order + 5,
|
||||
'is_enabled' => false,
|
||||
'visibility_rules' => $placement->visibility_rules,
|
||||
'presentation' => $placement->presentation,
|
||||
]);
|
||||
|
||||
return $copy;
|
||||
});
|
||||
|
||||
$this->normaliseRegion($placement->page_key, $placement->region);
|
||||
|
||||
return redirect()->route('content.documents.edit', $copy)->with('status', 'Page-specific draft copy created and added after the original.');
|
||||
}
|
||||
|
||||
public function reorder(Request $request, string $pageKey): JsonResponse|RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'region' => ['required', Rule::in(self::REGIONS)],
|
||||
'placements' => ['required', 'array'],
|
||||
'placements.*' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$ids = array_values(array_unique(array_map('intval', $validated['placements'])));
|
||||
$owned = PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->whereIn('id', $ids)->pluck('id')->all();
|
||||
abort_unless(count($owned) === count($ids), 422, 'One or more placements do not belong to this page region.');
|
||||
|
||||
DB::transaction(function () use ($ids): void {
|
||||
PageContent::query()->whereIn('id', $ids)->increment('sort_order', 100000);
|
||||
foreach ($ids as $index => $id) {
|
||||
PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
|
||||
}
|
||||
});
|
||||
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => 'Order saved.'])
|
||||
: back()->with('status', 'Page order updated.');
|
||||
}
|
||||
|
||||
private function normaliseRegion(?string $pageKey, string $region): void
|
||||
{
|
||||
$ids = PageContent::query()->where('page_key', $pageKey)->where('region', $region)->orderBy('sort_order')->pluck('id');
|
||||
DB::transaction(function () use ($ids): void {
|
||||
foreach ($ids as $index => $id) {
|
||||
PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user