Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Http/Controllers/Entities/EditController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Services\AttributeService;
use App\Services\Entity\AliasService;
use App\Services\Entity\EntitySaveService;
use App\Services\Entity\PreserveLastUpdatedService;
use App\Services\Entity\Relations\EntityRelationsServiceFactory;
use App\Services\Entity\Relations\LocationRelationsService;
use App\Services\MultiEditingService;
Expand All @@ -28,6 +29,7 @@ public function __construct(
protected AliasService $aliasService,
protected MultiEditingService $multiEditingService,
protected EntitySaveService $entitySaveService,
protected PreserveLastUpdatedService $preserveLastUpdatedService,
protected EntityRelationsServiceFactory $relationsFactory,
protected LocationRelationsService $locationRelationsService
) {}
Expand Down Expand Up @@ -85,6 +87,10 @@ public function save(Request $request, Campaign $campaign, Entity $entity)
return response()->json(['success' => true]);
}

$stealth = $request->boolean('stealth');
$request->request->remove('stealth');
$lastUpdated = $stealth ? $this->preserveLastUpdatedService->snapshot($entity) : null;

try {
// Sanitize the data
$sanitizerClassName = 'App\Sanitizers\\' . Str::studly($entity->entityType->code) . 'Sanitizer';
Expand Down Expand Up @@ -177,6 +183,10 @@ public function save(Request $request, Campaign $campaign, Entity $entity)
$error = str_replace(' ', '_', mb_strtolower(mb_rtrim($exception->getMessage(), '.')));

return redirect()->back()->withInput()->with('error', __('crud.errors.' . $error));
} finally {
if ($lastUpdated !== null) {
$this->preserveLastUpdatedService->restore($entity, $lastUpdated);
}
}
}

Expand Down
25 changes: 20 additions & 5 deletions app/Http/Controllers/Entity/EntryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
use App\Http\Requests\UpdateEntityEntry;
use App\Models\Campaign;
use App\Models\Entity;
use App\Services\Entity\PreserveLastUpdatedService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;

class EntryController extends Controller
{
public function __construct(
protected PreserveLastUpdatedService $preserveLastUpdatedService,
) {}

/**
* @return Application|Factory|View
*
Expand All @@ -39,12 +44,22 @@ public function update(UpdateEntityEntry $request, Campaign $campaign, Entity $e
return response()->json(['success' => true]);
}

$fields = $request->only('entry');
$entity->update($fields);
$lastUpdated = $request->boolean('stealth')
? $this->preserveLastUpdatedService->snapshot($entity)
: null;

try {
$fields = $request->only('entry');
$entity->update($fields);

if ($entity->wasChanged()) {
EntityLogger::entity($entity);
$entity->touch();
if ($entity->wasChanged()) {
EntityLogger::entity($entity);
$entity->touch();
}
} finally {
if ($lastUpdated !== null) {
$this->preserveLastUpdatedService->restore($entity, $lastUpdated);
}
}

$return = redirect()->to($entity->url());
Expand Down
97 changes: 60 additions & 37 deletions app/Http/Controllers/Entity/PostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Models\Entity;
use App\Models\Post;
use App\Models\PostLayout;
use App\Services\Entity\PreserveLastUpdatedService;
use App\Services\MultiEditingService;
use App\Services\Posts\Permissions\SavePermissionsService;
use App\Traits\CampaignAware;
Expand All @@ -22,6 +23,7 @@ class PostController extends Controller
public function __construct(
protected SavePermissionsService $savePermissionsService,
protected MultiEditingService $editingService,
protected PreserveLastUpdatedService $preserveLastUpdatedService,
) {}

public function index(Campaign $campaign, Entity $entity)
Expand Down Expand Up @@ -98,27 +100,38 @@ public function store(StorePost $request, Campaign $campaign, Entity $entity)

$data = $campaign->superboosted() ? $request->all() : $request->except(['layout_id']);
$data['entity_id'] = $entity->id;
$post = Post::create($data);
unset($data['stealth']);
$lastUpdated = $request->boolean('stealth')
? $this->preserveLastUpdatedService->snapshot($entity)
: null;

if (auth()->user()->can('permissions', $entity)) {
$this->savePermissionsService->post($post)->request($request)->save();
}
try {
$post = Post::create($data);

if ($request->has('submit-new')) {
$route = route('entities.posts.create', [$campaign, $entity]);
if (auth()->user()->can('permissions', $entity)) {
$this->savePermissionsService->post($post)->request($request)->save();
}

return response()->redirectTo($route);
} elseif ($request->has('submit-update')) {
$route = route('entities.posts.edit', [$campaign, $entity, $post]);
if ($request->has('submit-new')) {
$route = route('entities.posts.create', [$campaign, $entity]);

return response()->redirectTo($route);
}
return response()->redirectTo($route);
} elseif ($request->has('submit-update')) {
$route = route('entities.posts.edit', [$campaign, $entity, $post]);

return redirect()
->to($entity->url())
->with('success', __('entities/notes.create.success', [
'name' => $post->name, 'entity' => $entity->name,
]));
return response()->redirectTo($route);
}

return redirect()
->to($entity->url())
->with('success', __('entities/notes.create.success', [
'name' => $post->name, 'entity' => $entity->name,
]));
} finally {
if ($lastUpdated !== null) {
$this->preserveLastUpdatedService->restore($entity, $lastUpdated);
}
}
}

public function edit(Campaign $campaign, Entity $entity, Post $post)
Expand Down Expand Up @@ -164,32 +177,42 @@ public function update(StorePost $request, Campaign $campaign, Entity $entity, P
if ($request->isNotFilled('position')) {
unset($data['position']);
}
$post->update($data);
if (auth()->user()->can('permissions', $entity)) {
$this->savePermissionsService
->post($post)
->request($request)
->save();
}
$stealth = $request->boolean('stealth');
unset($data['stealth']);
$lastUpdated = $stealth ? $this->preserveLastUpdatedService->snapshot($entity) : null;

try {
$post->update($data);
if (auth()->user()->can('permissions', $entity)) {
$this->savePermissionsService
->post($post)
->request($request)
->save();
}

$this->editingService->model($post)
->user($request->user())
->finish();
$this->editingService->model($post)
->user($request->user())
->finish();

if ($request->has('submit-new')) {
$route = route('entities.posts.create', [$campaign, $entity]);
if ($request->has('submit-new')) {
$route = route('entities.posts.create', [$campaign, $entity]);

return response()->redirectTo($route);
} elseif ($request->has('submit-update')) {
$route = route('entities.posts.edit', [$campaign, $entity, $post]);
return response()->redirectTo($route);
} elseif ($request->has('submit-update')) {
$route = route('entities.posts.edit', [$campaign, $entity, $post]);

return response()->redirectTo($route);
}
return response()->redirectTo($route);
}

return redirect()->route('entities.show', [$campaign, $entity, '#post-' . $post->id])
->with('success', __('entities/notes.edit.success', [
'name' => $post->name, 'entity' => $entity->name,
]));
return redirect()->route('entities.show', [$campaign, $entity, '#post-' . $post->id])
->with('success', __('entities/notes.edit.success', [
'name' => $post->name, 'entity' => $entity->name,
]));
} finally {
if ($lastUpdated !== null) {
$this->preserveLastUpdatedService->restore($entity, $lastUpdated);
}
}
}

public function destroy(Campaign $campaign, Entity $entity, Post $post)
Expand Down
1 change: 1 addition & 0 deletions app/Http/Requests/UpdateEntityEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function rules()
{
return [
'entry' => 'required|string',
'stealth' => 'boolean',
];
}
}
84 changes: 84 additions & 0 deletions app/Services/Entity/PreserveLastUpdatedService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace App\Services\Entity;

use App\Models\Entity;
use Illuminate\Support\Facades\DB;

class PreserveLastUpdatedService
{
/**
* Capture the values shown as the entity's last update information.
*/
public function snapshot(Entity $entity): array
{
$snapshot = [
'updated_at' => $entity->getRawOriginal('updated_at'),
];

if (array_key_exists('updated_by', $entity->getAttributes())) {
$snapshot['updated_by'] = $entity->getRawOriginal('updated_by');
}

return $snapshot;
}

/**
* Restore the captured values without firing model events or changing logs.
*
* The current timestamp is part of the update condition so a concurrent
* update is not overwritten by a stealth edit.
*/
public function restore(Entity $entity, array $snapshot): void
{
if (! $entity->exists) {
return;
}

$table = $entity->getTable();
$key = $entity->getKeyName();
$current = DB::table($table)
->where($key, $entity->getKey())
->first(array_keys($snapshot));

if ($current === null || $this->matches($current, $snapshot)) {
return;
}

$query = DB::table($table)->where($key, $entity->getKey());
if ($current->updated_at === null) {
$query->whereNull('updated_at');
} else {
$query->where('updated_at', $current->updated_at);
}

if ($query->update($snapshot) === 0) {
return;
}

foreach ($snapshot as $attribute => $value) {
$entity->setAttribute($attribute, $value);
}
$entity->syncOriginalAttributes(array_keys($snapshot));
}

private function matches(object $current, array $snapshot): bool
{
foreach ($snapshot as $attribute => $value) {
if (! $this->sameValue($current->{$attribute}, $value)) {
return false;
}
}

return true;
}

private function sameValue(mixed $current, mixed $original): bool
{
if ($current === null || $original === null) {
return $current === $original;
}

return (string) $current === (string) $original;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
Expand All @@ -13,7 +12,6 @@ public function up(): void
Schema::dropIfExists('race_location');
}


public function down(): void
{
// The data was migrated to entity_locations and cannot be restored here.
Expand Down
3 changes: 3 additions & 0 deletions lang/en/crud.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
'parent' => 'Parent',
'position' => 'Position',
'replace_mentions' => 'Replace detail mentions in the description with those of the new entry',
'stealth_edit' => 'Silenty update',
'stealth_edit_helper' => 'If checked, saving won\'t update the entry\'s last updated time.',
'stealth_edit_post_helper' => 'If checked, saving this article won\'t update the entry\'s last updated time.',
'template' => 'Template',
'tooltip' => 'Tooltip',
'type' => 'Type',
Expand Down
5 changes: 3 additions & 2 deletions public/css/bootstrap-summernote.css

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions resources/js/utility/tippy.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,52 @@ const initTooltip = (e) => {
});
};

const initDropdownOptions = (instance, sourceDropdown) => {
const options = instance.popper?.querySelectorAll("[data-dropdown-option]") ?? [];
const sourceCheckboxes = sourceDropdown.querySelectorAll("[data-dropdown-option-checkbox]");

options.forEach((option) => {
const checkbox = option.querySelector("[data-dropdown-option-checkbox]");
const sourceCheckbox = Array.from(sourceCheckboxes).find(
(source) => source.name === checkbox?.name,
);
if (checkbox && sourceCheckbox) {
checkbox.checked = sourceCheckbox.checked;
if (!option.dataset.initialized) {
checkbox.addEventListener("change", () => {
sourceCheckbox.checked = checkbox.checked;
});
}
}

if (option.dataset.initialized) {
return;
}
option.dataset.initialized = "true";

const toggle = option.querySelector("[data-dropdown-option-help-toggle]");
const help = option.querySelector("[data-dropdown-option-help]");
if (!toggle || !help) {
return;
}

const toggleHelp = (event) => {
event.preventDefault();
event.stopPropagation();
const hidden = help.classList.toggle("hidden");
toggle.setAttribute("aria-expanded", String(!hidden));
requestAnimationFrame(() => instance.popperInstance?.forceUpdate());
};

toggle.addEventListener("click", toggleHelp);
toggle.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
toggleHelp(event);
}
});
});
};

const initDropdowns = () => {
const elements = document.querySelectorAll("[data-dropdown]");

Expand All @@ -99,6 +145,7 @@ const initDropdowns = () => {
interactive: true,
trigger: "click",
onShown(instance) {
initDropdownOptions(instance, dropdown);
window.triggerEvent();
},
});
Expand Down
Loading