feat(episode): add form to allow editing episode's publication date to a past date

This allows podcasters to reorganize their published episodes as they see fit

closes #97
This commit is contained in:
Yassine Doghri 2022-10-14 14:37:03 +00:00
parent 94c0b7c159
commit d783d16eb7
10 changed files with 196 additions and 31 deletions

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0H24V24H0z"/>
<path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12h2c0 4.418 3.582 8 8 8s8-3.582 8-8-3.582-8-8-8C9.536 4 7.332 5.114 5.865 6.865L8 9H2V3l2.447 2.446C6.28 3.336 8.984 2 12 2zm1 5v4.585l3.243 3.243-1.415 1.415L11 12.413V7h2z"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@ -4,27 +4,28 @@ declare(strict_types=1);
namespace App\Views\Components; namespace App\Views\Components;
use ViewComponents\Component; class IconButton extends Button
class IconButton extends Component
{ {
public string $glyph = ''; public string $glyph = '';
public function render(): string public function __construct(array $attributes)
{ {
$attributes = [ $iconButtonAttributes = [
'isSquared' => 'true', 'isSquared' => 'true',
'title' => $this->slot, 'title' => $attributes['slot'],
'data-tooltip' => 'bottom', 'data-tooltip' => 'bottom',
]; ];
$attributes = array_merge($attributes, $this->attributes); $glyphSize = [
'small' => 'text-sm',
'base' => 'text-lg',
'large' => 'text-2xl',
];
$attributes['slot'] = icon($this->glyph); $allAttributes = array_merge($attributes, $iconButtonAttributes);
unset($attributes['glyph']); parent::__construct($allAttributes);
$iconButton = new Button($attributes); $this->slot = icon($this->glyph, $glyphSize[$this->size]);
return $iconButton->render();
} }
} }

View File

@ -327,6 +327,23 @@ $routes->group(
'permission:podcast-manage_publications', 'permission:podcast-manage_publications',
], ],
); );
$routes->get(
'publish-date-edit',
'EpisodeController::publishDateEdit/$1/$2',
[
'as' => 'episode-publish_date_edit',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish-date-edit',
'EpisodeController::attemptPublishDateEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get( $routes->get(
'unpublish', 'unpublish',
'EpisodeController::unpublish/$1/$2', 'EpisodeController::unpublish/$1/$2',

View File

@ -683,29 +683,104 @@ class EpisodeController extends BaseController
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]); return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
} }
public function unpublish(): string | RedirectResponse public function publishDateEdit(): string|RedirectResponse
{ {
if ($this->episode->publication_status === 'published') { // only accessible if episode is already published
helper(['form']); if ($this->episode->publication_status !== 'published') {
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_date_edit_error')
);
}
$data = [ helper('form');
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([ $data = [
0 => $this->podcast->title, 'podcast' => $this->podcast,
1 => $this->episode->title, 'episode' => $this->episode,
]); ];
return view('episode/unpublish', $data);
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('episode/publish_date_edit', $data);
}
/**
* Allows to set an episode's publication date to a past date
*
* Prevents setting a future date as it does not make sense to set a future published date to an already published
* episode. This also prevents any side-effects from occurring.
*/
public function attemptPublishDateEdit(): RedirectResponse
{
$rules = [
'new_publication_date' => 'valid_date[Y-m-d H:i]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$newPublicationDate = $this->request->getPost('new_publication_date');
$newPublicationDate = Time::createFromFormat(
'Y-m-d H:i',
$newPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
if ($newPublicationDate->isAfter(Time::now())) {
return redirect()
->back()
->withInput()
->with('error', lang('Episode.publish_date_edit_future_error'));
}
$this->episode->published_at = $newPublicationDate;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
} }
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with( return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error', 'message',
lang('Episode.unpublish_error') lang('Episode.publish_date_edit_success')
); );
} }
public function unpublish(): string | RedirectResponse
{
if ($this->episode->publication_status !== 'published') {
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.unpublish_error')
);
}
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('episode/unpublish', $data);
}
public function attemptUnpublish(): RedirectResponse public function attemptUnpublish(): RedirectResponse
{ {
$rules = [ $rules = [

View File

@ -25,6 +25,7 @@ return [
'persons' => 'persons', 'persons' => 'persons',
'publish' => 'publish', 'publish' => 'publish',
'publish-edit' => 'edit publication', 'publish-edit' => 'edit publication',
'publish-date-edit' => 'edit publication date',
'unpublish' => 'unpublish', 'unpublish' => 'unpublish',
'delete' => 'delete', 'delete' => 'delete',
'fediverse' => 'fediverse', 'fediverse' => 'fediverse',

View File

@ -24,10 +24,14 @@ return [
'edit' => 'Edit', 'edit' => 'Edit',
'publish' => 'Publish', 'publish' => 'Publish',
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
'publish_date_edit' => 'Edit publication date',
'unpublish' => 'Unpublish', 'unpublish' => 'Unpublish',
'publish_error' => 'Episode is already published.', 'publish_error' => 'Episode is already published.',
'publish_edit_error' => 'Episode is already published.', 'publish_edit_error' => 'Episode is already published.',
'publish_cancel_error' => 'Episode is already published.', 'publish_cancel_error' => 'Episode is already published.',
'publish_date_edit_error' => 'Episode has not been published yet, you cannot edit its publication date.',
'publish_date_edit_future_error' => 'Episode\'s publication date can only be set to a past date! If you would like to reschedule it, unpublish it first.',
'publish_date_edit_success' => 'Episode\'s publication date has been updated successfully!',
'unpublish_error' => 'Episode is not published.', 'unpublish_error' => 'Episode is not published.',
'delete' => 'Delete', 'delete' => 'Delete',
'go_to_page' => 'Go to page', 'go_to_page' => 'Go to page',
@ -178,6 +182,11 @@ return [
'message_warning_hint' => 'Having a message increases social engagement, resulting in a better visibility for your episode.', 'message_warning_hint' => 'Having a message increases social engagement, resulting in a better visibility for your episode.',
'message_warning_submit' => 'Publish anyways', 'message_warning_submit' => 'Publish anyways',
], ],
'publish_date_edit_form' => [
'new_publication_date' => 'New publication date',
'new_publication_date_hint' => 'Must be set to a past date.',
'submit' => 'Edit publication date',
],
'unpublish_form' => [ 'unpublish_form' => [
'disclaimer' => 'disclaimer' =>
"Unpublishing the episode will delete all the comments and posts associated with it and remove it from the podcast's RSS feed.", "Unpublishing the episode will delete all the comments and posts associated with it and remove it from the podcast's RSS feed.",

View File

@ -1,3 +1,9 @@
<?php declare(strict_types=1);
$isPodcastArea = isset($podcast) && ! isset($episode);
$isEpisodeArea = isset($podcast) && isset($episode);
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<?= service('request') <html lang="<?= service('request')
->getLocale() ?>"> ->getLocale() ?>">
@ -32,9 +38,9 @@
<?= render_breadcrumb('text-xs items-center flex') ?> <?= render_breadcrumb('text-xs items-center flex') ?>
<div class="flex justify-between py-1"> <div class="flex justify-between py-1">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<?php if ((isset($episode) && $episode->is_premium) || (isset($podcast) && $podcast->is_premium)): ?> <?php if (($isEpisodeArea && $episode->is_premium) || ($isPodcastArea && $podcast->is_premium)): ?>
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar" variant="secondary" class="p-0 mr-2 text-4xl border-0"><?= isset($episode) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></IconButton> <IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></IconButton>
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading> <Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
</div> </div>
<?php else: ?> <?php else: ?>
@ -42,11 +48,11 @@
<?php endif; ?> <?php endif; ?>
<?= $this->renderSection('headerLeft') ?> <?= $this->renderSection('headerLeft') ?>
</div> </div>
<div class="flex flex-shrink-0 gap-x-2"><?= $this->renderSection('headerRight') ?></div> <div class="flex items-center flex-shrink-0 gap-x-2"><?= $this->renderSection('headerRight') ?></div>
</div> </div>
</div> </div>
</header> </header>
<?php if (isset($podcast) && $podcast->publication_status !== 'published'): ?> <?php if ($isPodcastArea && $podcast->publication_status !== 'published'): ?>
<?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?> <?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?>
<?php endif ?> <?php endif ?>
<div class="px-2 py-8 mx-auto md:px-12"> <div class="px-2 py-8 mx-auto md:px-12">

View File

@ -1,8 +1,14 @@
<?php declare(strict_types=1);
$isPodcastArea = isset($podcast) && ! isset($episode);
$isEpisodeArea = isset($podcast) && isset($episode);
?>
<div data-sidebar-toggler="backdrop" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>" class="fixed z-50 hidden w-full h-full bg-gray-800/75 md:hidden"></div> <div data-sidebar-toggler="backdrop" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>" class="fixed z-50 hidden w-full h-full bg-gray-800/75 md:hidden"></div>
<aside data-sidebar-toggler="sidebar" data-toggle-class="-translate-x-full" data-hide-class="-translate-x-full" class="h-full max-h-[calc(100vh-40px)] sticky z-50 flex flex-col row-start-2 col-start-1 text-white transition duration-200 ease-in-out transform -translate-x-full border-r top-10 border-navigation bg-navigation md:translate-x-0"> <aside data-sidebar-toggler="sidebar" data-toggle-class="-translate-x-full" data-hide-class="-translate-x-full" class="h-full max-h-[calc(100vh-40px)] sticky z-50 flex flex-col row-start-2 col-start-1 text-white transition duration-200 ease-in-out transform -translate-x-full border-r top-10 border-navigation bg-navigation md:translate-x-0">
<?php if (isset($podcast) && isset($episode)): ?> <?php if ($isEpisodeArea): ?>
<?= $this->include('episode/_sidebar') ?> <?= $this->include('episode/_sidebar') ?>
<?php elseif (isset($podcast)): ?> <?php elseif ($isPodcastArea): ?>
<?= $this->include('podcast/_sidebar') ?> <?= $this->include('podcast/_sidebar') ?>
<?php else: ?> <?php else: ?>
<?= $this->include('_sidebar') ?> <?= $this->include('_sidebar') ?>

View File

@ -0,0 +1,38 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.publish_date_edit') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Episode.publish_date_edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= anchor(
route_to('episode-view', $podcast->id, $episode->id),
icon('arrow-left', 'mr-2 text-lg') . lang('Episode.publish_form.back_to_episode_dashboard'),
[
'class' => 'inline-flex items-center font-semibold mr-4 text-sm',
],
) ?>
<form action="<?= route_to('episode-publish_date_edit', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col items-start w-full max-w-lg mx-auto mt-4" data-submit="validate-message">
<?= csrf_field() ?>
<input type="hidden" name="client_timezone" value="UTC" />
<Forms.Field
as="DatetimePicker"
name="new_publication_date"
label="<?= lang('Episode.publish_date_edit_form.new_publication_date') ?>"
hint="<?= lang('Episode.publish_date_edit_form.new_publication_date_hint') ?>"
value="<?= $episode->published_at ?>"
required="true"
/>
<Button variant="primary" type="submit" class="mt-4"><?= lang('Episode.publish_date_edit_form.submit') ?></Button>
</form>
<?= $this->endSection() ?>

View File

@ -17,6 +17,14 @@
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('headerRight') ?> <?= $this->section('headerRight') ?>
<?php if ($episode->publication_status === 'published'): ?>
<IconButton
uri="<?= route_to('episode-publish_date_edit', $podcast->id, $episode->id) ?>"
glyph="history"
variant="secondary"
glyphClass="text-xl"
><?= lang('Episode.publish_date_edit') ?></IconButton>
<?php endif; ?>
<?= publication_button( <?= publication_button(
$podcast->id, $podcast->id,
$episode->id, $episode->id,