From 4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203 Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> Date: Thu, 22 Oct 2020 17:41:59 +0000 Subject: [PATCH] feat(episodes): schedule episode with future publication_date by using cache expiration time - merge publication date fields into one field instanciated with flatpickr datetime picker - get user timezone to convert user publication_date input to UTC - remove setPublishedAt() method from episode entity - add publication pill component to display the episode publication date info - clear cache after episode insert - use CI is_really_writable() helper in install instead of is_writable() - fix latest episodes layout - update tsconfig to only include ts folders - update DEPENDENCIES.md to include flatpickr - add format_duration helper to format episode enclosure duration instead of translating it (causes translation bug) - add Time.ts module to convert UTC time to user localized time for episode publication dates - fix some layout issues - update php and js dependencies to latest versions closes #47 --- DEPENDENCIES.md | 2 + app/Controllers/Admin/BaseController.php | 2 +- app/Controllers/Admin/Episode.php | 25 ++-- app/Controllers/Admin/Podcast.php | 5 +- app/Controllers/BaseController.php | 2 +- app/Controllers/Episode.php | 11 +- app/Controllers/Feed.php | 17 ++- app/Controllers/Install.php | 2 +- app/Controllers/Podcast.php | 10 +- app/Entities/Episode.php | 30 ++-- app/Helpers/components_helper.php | 48 +++++++ app/Helpers/misc_helper.php | 24 ++++ app/Language/en/Common.php | 1 - app/Language/en/Episode.php | 10 +- app/Language/fr/Common.php | 1 - app/Language/fr/Episode.php | 10 +- app/Language/fr/MyAccount.php | 3 +- app/Models/EpisodeModel.php | 102 +++++++++++--- app/Models/PodcastModel.php | 2 +- app/Views/_assets/admin.ts | 6 + app/Views/_assets/modules/Charts.ts | 10 +- app/Views/_assets/modules/ClientTimezone.ts | 11 ++ app/Views/_assets/modules/DateTimePicker.ts | 41 ++++++ app/Views/_assets/modules/Time.ts | 24 ++++ app/Views/_assets/podcast.ts | 3 + app/Views/admin/_layout.php | 2 +- app/Views/admin/episode/create.php | 43 ++---- app/Views/admin/episode/edit.php | 58 +++----- app/Views/admin/episode/list.php | 17 +-- app/Views/admin/episode/view.php | 7 +- .../admin/my_account/change_password.php | 3 +- app/Views/admin/my_account/view.php | 3 +- app/Views/admin/podcast/create.php | 9 +- app/Views/admin/podcast/latest_episodes.php | 8 +- app/Views/admin/podcast/list.php | 3 +- app/Views/admin/user/create.php | 3 +- app/Views/admin/user/list.php | 3 +- app/Views/episode.php | 13 +- app/Views/errors/cli/error_exception.php | 3 +- app/Views/podcast.php | 15 +- composer.lock | 19 ++- package-lock.json | 133 ++++++++++-------- package.json | 19 +-- tests/README.md | 98 +++++++------ tsconfig.json | 3 +- 45 files changed, 556 insertions(+), 308 deletions(-) create mode 100644 app/Views/_assets/modules/ClientTimezone.ts create mode 100644 app/Views/_assets/modules/DateTimePicker.ts create mode 100644 app/Views/_assets/modules/Time.ts create mode 100644 app/Views/_assets/podcast.ts diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 0075667615..d4c1c29cc1 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -37,6 +37,8 @@ Javascript dependencies: ([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE)) - [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE)) +- [flatpickr](https://flatpickr.js.org/) + ([MIT License](https://github.com/flatpickr/flatpickr/blob/master/LICENSE.md)) Other: diff --git a/app/Controllers/Admin/BaseController.php b/app/Controllers/Admin/BaseController.php index 5d7de7d273..6bc9ff8567 100644 --- a/app/Controllers/Admin/BaseController.php +++ b/app/Controllers/Admin/BaseController.php @@ -26,7 +26,7 @@ class BaseController extends Controller * * @var array */ - protected $helpers = ['auth', 'breadcrumb', 'svg', 'components']; + protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc']; /** * Constructor. diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index e10c40609c..82a73a14c2 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -10,6 +10,7 @@ namespace App\Controllers\Admin; use App\Models\EpisodeModel; use App\Models\PodcastModel; +use CodeIgniter\I18n\Time; class Episode extends BaseController { @@ -95,9 +96,7 @@ class Episode extends BaseController 'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]', 'image' => 'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]', - 'publication_date' => 'valid_date[Y-m-d]|permit_empty', - 'publication_time' => - 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', + 'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty', ]; if (!$this->validate($rules)) { @@ -125,11 +124,12 @@ class Episode extends BaseController 'block' => $this->request->getPost('block') == 'yes', 'created_by' => user(), 'updated_by' => user(), + 'published_at' => Time::createFromFormat( + 'Y-m-d H:i', + $this->request->getPost('publication_date'), + $this->request->getPost('client_timezone') + )->setTimezone('UTC'), ]); - $newEpisode->setPublishedAt( - $this->request->getPost('publication_date'), - $this->request->getPost('publication_time') - ); $episodeModel = new EpisodeModel(); @@ -185,9 +185,7 @@ class Episode extends BaseController 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty', 'image' => 'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]', - 'publication_date' => 'valid_date[Y-m-d]|permit_empty', - 'publication_time' => - 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', + 'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty', ]; if (!$this->validate($rules)) { @@ -210,10 +208,11 @@ class Episode extends BaseController : null; $this->episode->type = $this->request->getPost('type'); $this->episode->block = $this->request->getPost('block') == 'yes'; - $this->episode->setPublishedAt( + $this->episode->published_at = Time::createFromFormat( + 'Y-m-d H:i', $this->request->getPost('publication_date'), - $this->request->getPost('publication_time') - ); + $this->request->getPost('client_timezone') + )->setTimezone('UTC'); $this->episode->updated_by = user(); $enclosure = $this->request->getFile('enclosure'); diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 05503ca221..afac930d4a 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -388,11 +388,8 @@ class Podcast extends BaseController : $nsItunes->block === 'yes', 'created_by' => user(), 'updated_by' => user(), + 'published_at' => strtotime($item->pubDate), ]); - $newEpisode->setPublishedAt( - date('Y-m-d', strtotime($item->pubDate)), - date('H:i:s', strtotime($item->pubDate)) - ); $episodeModel = new EpisodeModel(); diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 3bfbd4b0ff..ab249c803d 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -26,7 +26,7 @@ class BaseController extends Controller * * @var array */ - protected $helpers = ['analytics', 'svg', 'components']; + protected $helpers = ['analytics', 'svg', 'components', 'misc']; /** * Constructor. diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index 38c83da97d..7b5dc9f7ab 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -48,7 +48,8 @@ class Episode extends BaseController $cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_{$locale}"; if (!($cachedView = cache($cacheName))) { - $previousNextEpisodes = (new EpisodeModel())->getPreviousNextEpisodes( + $episodeModel = new EpisodeModel(); + $previousNextEpisodes = $episodeModel->getPreviousNextEpisodes( $this->episode, $this->podcast->type ); @@ -60,9 +61,15 @@ class Episode extends BaseController 'episode' => $this->episode, ]; + $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode( + $this->podcast->id + ); + // The page cache is set to a decade so it is deleted manually upon podcast update return view('episode', $data, [ - 'cache' => DECADE, + 'cache' => $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE, 'cache_name' => $cacheName, ]); } diff --git a/app/Controllers/Feed.php b/app/Controllers/Feed.php index 3da87f0dd3..0170e05ea3 100644 --- a/app/Controllers/Feed.php +++ b/app/Controllers/Feed.php @@ -8,6 +8,7 @@ namespace App\Controllers; +use App\Models\EpisodeModel; use App\Models\PodcastModel; use CodeIgniter\Controller; @@ -31,15 +32,29 @@ class Feed extends Controller // If things go wrong the show must go on and the user must be able to download the file log_message('critical', $e); } + $cacheName = "podcast{$podcast->id}_feed" . ($service ? "_{$service['slug']}" : ''); + if (!($found = cache($cacheName))) { $found = get_rss_feed( $podcast, $service ? '?s=' . urlencode($service['name']) : '' ); - cache()->save($cacheName, $found, DECADE); + + // The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update + $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode( + $podcast->id + ); + + cache()->save( + $cacheName, + $found, + $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE + ); } return $this->response->setXML($found); } diff --git a/app/Controllers/Install.php b/app/Controllers/Install.php index cc9e26d48d..053e3faed4 100644 --- a/app/Controllers/Install.php +++ b/app/Controllers/Install.php @@ -48,7 +48,7 @@ class Install extends Controller } // Check if the created .env file is writable to continue install process - if (is_writable(ROOTPATH . '.env')) { + if (is_really_writable(ROOTPATH . '.env')) { try { $dotenv->required([ 'app.baseURL', diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 5d555f293e..1e1dbda3b3 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -113,7 +113,7 @@ class Podcast extends BaseController 'podcast' => $this->podcast, 'episodesNav' => $episodesNavigation, 'activeQuery' => $activeQuery, - 'episodes' => (new EpisodeModel())->getPodcastEpisodes( + 'episodes' => $episodeModel->getPodcastEpisodes( $this->podcast->id, $this->podcast->type, $yearQuery, @@ -121,8 +121,14 @@ class Podcast extends BaseController ), ]; + $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode( + $this->podcast->id + ); + return view('podcast', $data, [ - 'cache' => DECADE, + 'cache' => $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE, 'cache_name' => $cacheName, ]); } diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 70ab1ed3f0..23f7737c24 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -10,6 +10,7 @@ namespace App\Entities; use App\Models\PodcastModel; use CodeIgniter\Entity; +use CodeIgniter\I18n\Time; use League\CommonMark\CommonMarkConverter; class Episode extends Entity @@ -49,6 +50,11 @@ class Episode extends Entity */ protected $description_html; + /** + * @var boolean + */ + protected $is_published; + protected $dates = [ 'published_at', 'created_at', @@ -232,17 +238,6 @@ class Episode extends Entity return $converter->convertToHtml($this->attributes['description']); } - public function setPublishedAt($date, $time) - { - if (empty($date)) { - $this->attributes['published_at'] = null; - } else { - $this->attributes['published_at'] = $date . ' ' . $time; - } - - return $this; - } - public function setCreatedBy(\App\Entities\User $user) { $this->attributes['created_by'] = $user->id; @@ -256,4 +251,17 @@ class Episode extends Entity return $this; } + + public function getIsPublished() + { + if ($this->is_published) { + return $this->is_published; + } + + helper('date'); + + $this->is_published = $this->published_at->isBefore(Time::now()); + + return $this->is_published; + } } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 5b26da6df1..0e31cb651a 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -256,3 +256,51 @@ if (!function_exists('data_table')) { } // ------------------------------------------------------------------------ + +if (!function_exists('publication_pill')) { + /** + * Data table component + * + * Creates a stylized table. + * + * @param \CodeIgniter\I18n\Time $publicationDate publication datetime of the episode + * @param boolean $isPublished whether or not the episode has been published + * @param string $customClass css class to add to the component + * + * @return string + */ + function publication_pill( + $publicationDate, + $isPublished, + $customClass = '' + ): string { + $class = $isPublished + ? 'text-green-500 border-green-500' + : 'text-orange-600 border-orange-600'; + + $label = lang( + $isPublished ? 'Episode.published' : 'Episode.scheduled', + [ + '<time + pubdate + datetime="' . + $publicationDate->format(DateTime::ATOM) . + '" + title="' . + $publicationDate . + '">' . + lang('Common.mediumDate', [$publicationDate]) . + '</time>', + ] + ); + + return '<span class="px-1 border ' . + $class . + ' ' . + $customClass . + '">' . + $label . + '</span>'; + } +} +// ------------------------------------------------------------------------ diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php index b1d1b168a3..b87051c479 100644 --- a/app/Helpers/misc_helper.php +++ b/app/Helpers/misc_helper.php @@ -143,3 +143,27 @@ function slugify($text) return $text; } + +//-------------------------------------------------------------------- + +if (!function_exists('format_duration')) { + /** + * Formats duration in seconds to an hh:mm:ss string + * + * @param int $seconds seconds to format + * @param string $separator + * + * @return string + */ + function format_duration($seconds, $separator = ':') + { + return sprintf( + '%02d%s%02d%s%02d', + floor($seconds / 3600), + $separator, + ($seconds / 60) % 60, + $separator, + $seconds % 60 + ); + } +} diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index 7e90eb891b..f1d101e1e6 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -14,7 +14,6 @@ return [ 'home' => 'Home', 'explicit' => 'Explicit', 'mediumDate' => '{0,date,medium}', - 'duration' => '{0,duration}', 'powered_by' => 'Powered by {castopod}.', 'actions' => 'Actions', 'pageInfo' => 'Page {currentPage} out of {pageCount}', diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index d2a294c26b..61f0132db0 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -22,6 +22,8 @@ return [ 'delete' => 'Delete', 'go_to_page' => 'Go to page', 'create' => 'Add an episode', + 'published' => 'Published on {0}', + 'scheduled' => 'Scheduled for {0}', 'form' => [ 'enclosure' => 'Audio file', 'enclosure_hint' => 'Choose an .mp3 or .m4a audio file.', @@ -54,11 +56,9 @@ return [ 'This text is added at the end of each episode description, it is a good place to input your social links for example.', 'publication_section_title' => 'Publication info', 'publication_section_subtitle' => '', - 'published_at' => [ - 'label' => 'Publication date', - 'date' => 'Date', - 'time' => 'Time', - ], + 'publication_date' => 'Publication date', + 'publication_date_hint' => + 'You can schedule the episode release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm', 'parental_advisory' => [ 'label' => 'Parental advisory', 'hint' => 'Does the episode contain explicit content?', diff --git a/app/Language/fr/Common.php b/app/Language/fr/Common.php index 0bef7680c4..59a9514eca 100644 --- a/app/Language/fr/Common.php +++ b/app/Language/fr/Common.php @@ -14,7 +14,6 @@ return [ 'home' => 'Accueil', 'explicit' => 'Explicite', 'mediumDate' => '{0,date,medium}', - 'duration' => '{0,duration}', 'powered_by' => 'Propulsé par {castopod}.', 'actions' => 'Actions', 'pageInfo' => 'Page {currentPage} sur {pageCount}', diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index 1471e307cb..2b5716cd76 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -22,6 +22,8 @@ return [ 'delete' => 'Supprimer', 'go_to_page' => 'Voir', 'create' => 'Ajouter un épisode', + 'published' => 'Publié le {0}', + 'scheduled' => 'Planifié pour le {0}', 'form' => [ 'enclosure' => 'Fichier audio', 'enclosure_hint' => 'Sélectionnez un fichier audio .mp3 ou .m4a.', @@ -54,11 +56,9 @@ return [ 'Ce texte est ajouté à la fin de chaque description d’épisode, c’est un bon endroit pour placer vos liens sociaux par exemple.', 'publication_section_title' => 'Information de publication', 'publication_section_subtitle' => '', - 'published_at' => [ - 'label' => 'Date de publication', - 'date' => 'Date', - 'time' => 'Heure', - ], + 'publication_date' => 'Date de publication', + 'publication_date_hint' => + 'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm', 'parental_advisory' => [ 'label' => 'Avertissement parental', 'hint' => 'L’épisode contient-il un contenu explicite ?', diff --git a/app/Language/fr/MyAccount.php b/app/Language/fr/MyAccount.php index 5a2181f28d..3d75c589dc 100644 --- a/app/Language/fr/MyAccount.php +++ b/app/Language/fr/MyAccount.php @@ -12,6 +12,7 @@ return [ 'messages' => [ 'wrongPasswordError' => 'Le mot de passe que vous avez saisi est invalide.', - 'passwordChangeSuccess' => 'Le mot de passe a été modifié avec succès !', + 'passwordChangeSuccess' => + 'Le mot de passe a été modifié avec succès !', ], ]; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 2242266c26..e01d5e35ca 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -57,32 +57,21 @@ class EpisodeModel extends Model ]; protected $validationMessages = []; - protected $afterInsert = ['writeEnclosureMetadata']; + protected $afterInsert = ['writeEnclosureMetadata', 'clearCache']; // clear cache beforeUpdate because if slug changes, so will the episode link protected $beforeUpdate = ['clearCache']; protected $afterUpdate = ['writeEnclosureMetadata']; protected $beforeDelete = ['clearCache']; - protected function writeEnclosureMetadata(array $data) - { - helper('id3'); - - $episode = (new EpisodeModel())->find( - is_array($data['id']) ? $data['id'][0] : $data['id'] - ); - - write_enclosure_tags($episode); - - return $data; - } - public function getEpisodeBySlug($podcastId, $episodeSlug) { if (!($found = cache("podcast{$podcastId}_episode@{$episodeSlug}"))) { $found = $this->where([ 'podcast_id' => $podcastId, 'slug' => $episodeSlug, - ])->first(); + ]) + ->where('`published_at` <= NOW()', null, false) + ->first(); cache()->save( "podcast{$podcastId}_episode@{$episodeSlug}", @@ -120,6 +109,7 @@ class EpisodeModel extends Model 'podcast_id' => $episode->podcast_id, $sortNumberField . ' <' => $sortNumberValue, ]) + ->where('`published_at` <= NOW()', null, false) ->first(); $nextData = $this->orderBy('(' . $sortNumberField . ') ASC') @@ -127,6 +117,7 @@ class EpisodeModel extends Model 'podcast_id' => $episode->podcast_id, $sortNumberField . ' >' => $sortNumberValue, ]) + ->where('`published_at` <= NOW()', null, false) ->first(); return [ @@ -160,7 +151,9 @@ class EpisodeModel extends Model ); if (!($found = cache($cacheName))) { - $where = ['podcast_id' => $podcastId]; + $where = [ + 'podcast_id' => $podcastId, + ]; if ($year) { $where['YEAR(published_at)'] = $year; $where['season_number'] = null; @@ -172,15 +165,27 @@ class EpisodeModel extends Model if ($podcastType == 'serial') { // podcast is serial $found = $this->where($where) + ->where('`published_at` <= NOW()', null, false) ->orderBy('season_number DESC, number ASC') ->findAll(); } else { $found = $this->where($where) + ->where('`published_at` <= NOW()', null, false) ->orderBy('published_at', 'DESC') ->findAll(); } - cache()->save($cacheName, $found, DECADE); + $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode( + $podcastId + ); + + cache()->save( + $cacheName, + $found, + $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE + ); } return $found; @@ -197,12 +202,23 @@ class EpisodeModel extends Model 'season_number' => null, $this->deletedField => null, ]) + ->where('`published_at` <= NOW()', null, false) ->groupBy('year') ->orderBy('year', 'DESC') ->get() ->getResultArray(); - cache()->save("podcast{$podcastId}_years", $found, DECADE); + $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode( + $podcastId + ); + + cache()->save( + "podcast{$podcastId}_years", + $found, + $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE + ); } return $found; @@ -219,12 +235,23 @@ class EpisodeModel extends Model 'season_number is not' => null, $this->deletedField => null, ]) + ->where('`published_at` <= NOW()', null, false) ->groupBy('season_number') ->orderBy('season_number', 'ASC') ->get() ->getResultArray(); - cache()->save("podcast{$podcastId}_seasons", $found, DECADE); + $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode( + $podcastId + ); + + cache()->save( + "podcast{$podcastId}_seasons", + $found, + $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE + ); } return $found; @@ -264,6 +291,43 @@ class EpisodeModel extends Model return $defaultQuery; } + /** + * Returns the timestamp difference in seconds between the next episode to publish and the current timestamp + * Returns false if there's no episode to publish + * + * @param int $podcastId + * + * @return int|false seconds + */ + public function getSecondsToNextUnpublishedEpisode(int $podcastId) + { + $result = $this->select( + 'TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff' + ) + ->where([ + 'podcast_id' => $podcastId, + ]) + ->where('`published_at` > NOW()', null, false) + ->orderBy('published_at', 'asc') + ->get() + ->getResultArray(); + + return (int) $result ? $result[0]['timestamp_diff'] : false; + } + + protected function writeEnclosureMetadata(array $data) + { + helper('id3'); + + $episode = (new EpisodeModel())->find( + is_array($data['id']) ? $data['id'][0] : $data['id'] + ); + + write_enclosure_tags($episode); + + return $data; + } + protected function clearCache(array $data) { $episodeModel = new EpisodeModel(); diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 5c75640eba..0cc21617f1 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -59,7 +59,7 @@ class PodcastModel extends Model ]; protected $validationMessages = []; - // clear cache before update if by any chance, the podcast name changes, and so will the podcast link + // clear cache before update if by any chance, the podcast name changes, so will the podcast link protected $beforeUpdate = ['clearCache']; protected $beforeDelete = ['clearCache']; diff --git a/app/Views/_assets/admin.ts b/app/Views/_assets/admin.ts index 4adc1b5b75..d05abf005a 100644 --- a/app/Views/_assets/admin.ts +++ b/app/Views/_assets/admin.ts @@ -1,8 +1,11 @@ +import ClientTimezone from "./modules/ClientTimezone"; +import DateTimePicker from "./modules/DateTimePicker"; import Dropdown from "./modules/Dropdown"; import MarkdownEditor from "./modules/MarkdownEditor"; import MultiSelect from "./modules/MultiSelect"; import SidebarToggler from "./modules/SidebarToggler"; import Slugify from "./modules/Slugify"; +import Time from "./modules/Time"; import Tooltip from "./modules/Tooltip"; Dropdown(); @@ -11,3 +14,6 @@ MarkdownEditor(); MultiSelect(); Slugify(); SidebarToggler(); +ClientTimezone(); +DateTimePicker(); +Time(); diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index 2f6053afd7..41149edd4b 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -68,7 +68,10 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { chart.scrollbarX = new am4core.Scrollbar(); }; -const drawXYDurationChart = (chartDivId: string, dataUrl: string | null): void => { +const drawXYDurationChart = ( + chartDivId: string, + dataUrl: string | null +): void => { // Create chart instance const chart = am4core.create(chartDivId, am4charts.XYChart); am4core.percent(100); @@ -203,7 +206,10 @@ const DrawCharts = (): void => { drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); break; case "xy-duration-chart": - drawXYDurationChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + drawXYDurationChart( + chartDiv.id, + chartDiv.getAttribute("data-chart-url") + ); break; case "xy-series-chart": drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); diff --git a/app/Views/_assets/modules/ClientTimezone.ts b/app/Views/_assets/modules/ClientTimezone.ts new file mode 100644 index 0000000000..94ad23686f --- /dev/null +++ b/app/Views/_assets/modules/ClientTimezone.ts @@ -0,0 +1,11 @@ +const ClientTimezone = (): void => { + const input: HTMLInputElement | null = document.querySelector( + "input[name='client_timezone']" + ); + + if (input) { + input.value = Intl.DateTimeFormat().resolvedOptions().timeZone; + } +}; + +export default ClientTimezone; diff --git a/app/Views/_assets/modules/DateTimePicker.ts b/app/Views/_assets/modules/DateTimePicker.ts new file mode 100644 index 0000000000..235cf69f78 --- /dev/null +++ b/app/Views/_assets/modules/DateTimePicker.ts @@ -0,0 +1,41 @@ +import flatpickr from "flatpickr"; +import "flatpickr/dist/flatpickr.min.css"; + +/* + * Detects navigator locale 24h time preference + * It works by checking whether hour output contains AM ('1 AM' or '01 h') + */ +const isBrowserLocale24h = () => + !new Intl.DateTimeFormat(navigator.language, { hour: "numeric" }) + .format(0) + .match(/AM/); + +const DateTimePicker = (): void => { + const dateTimeContainers: NodeListOf<HTMLInputElement> = document.querySelectorAll( + "input[data-picker='datetime']" + ); + + for (let i = 0; i < dateTimeContainers.length; i++) { + const dateTimeContainer = dateTimeContainers[i]; + + const flatpickrInstance = flatpickr(dateTimeContainer, { + enableTime: true, + time_24hr: isBrowserLocale24h(), + }); + + // convert container UTC date value to user timezone + const dateTime = new Date(dateTimeContainer.value); + const dateUTC = Date.UTC( + dateTime.getFullYear(), + dateTime.getMonth(), + dateTime.getDate(), + dateTime.getHours(), + dateTime.getMinutes() + ); + + // set converted date as field value + flatpickrInstance.setDate(new Date(dateUTC)); + } +}; + +export default DateTimePicker; diff --git a/app/Views/_assets/modules/Time.ts b/app/Views/_assets/modules/Time.ts new file mode 100644 index 0000000000..58ea0f2695 --- /dev/null +++ b/app/Views/_assets/modules/Time.ts @@ -0,0 +1,24 @@ +const Time = (): void => { + const timeElements: NodeListOf<HTMLTimeElement> = document.querySelectorAll( + "time" + ); + + console.log(timeElements); + + for (let i = 0; i < timeElements.length; i++) { + const timeElement = timeElements[i]; + + // convert UTC date value to user timezone + const timeElementDateTime = timeElement.getAttribute("datetime"); + + // check if timeElementDateTime is not null and not a duration + if (timeElementDateTime && !timeElementDateTime.startsWith("PT")) { + const dateTime = new Date(timeElementDateTime); + + // replace <time/> title with localized datetime + timeElement.setAttribute("title", dateTime.toLocaleString()); + } + } +}; + +export default Time; diff --git a/app/Views/_assets/podcast.ts b/app/Views/_assets/podcast.ts new file mode 100644 index 0000000000..c5e125b3f5 --- /dev/null +++ b/app/Views/_assets/podcast.ts @@ -0,0 +1,3 @@ +import Time from "./modules/Time"; + +Time(); diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index d04edd4e19..cc46a59136 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -26,7 +26,7 @@ <div class="container flex flex-wrap items-end justify-between px-2 py-10 mx-auto md:px-12 gap-y-6 gap-x-6"> <div class="flex flex-col"> <?= render_breadcrumb('text-gray-300') ?> - <h1 class="text-3xl leading-none"><?= $this->renderSection( + <h1 class="text-3xl"><?= $this->renderSection( 'pageTitle' ) ?></h1> </div> diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php index 8f3b5a2f05..e72961cac3 100644 --- a/app/Views/admin/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -16,6 +16,7 @@ 'class' => 'flex flex-col', ]) ?> <?= csrf_field() ?> +<?= form_hidden('client_timezone', 'UTC') ?> <?= form_section( lang('Episode.form.info_section_title'), @@ -193,35 +194,19 @@ lang('Episode.form.publication_section_subtitle') ) ?> -<?= form_fieldset('', ['class' => 'flex mb-4']) ?> -<legend><?= lang('Episode.form.published_at.label') ?></legend> -<div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.publication_date'), 'publication_date', [ - 'class' => 'sr-only', - ]) ?> - <?= form_input([ - 'id' => 'publication_date', - 'name' => 'publication_date', - 'class' => 'form-input', - 'value' => old('publication_date', date('Y-m-d')), - 'type' => 'date', - ]) ?> -</div> - -<div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.publication_time'), 'publication_time', [ - 'class' => 'sr-only', - ]) ?> - <?= form_input([ - 'id' => 'publication_time', - 'name' => 'publication_time', - 'class' => 'form-input', - 'value' => old('publication_time', date('H:i')), - 'placeholder' => '--:--', - 'type' => 'time', - ]) ?> -</div> -<?= form_fieldset_close() ?> +<?= form_label( + lang('Episode.form.publication_date'), + 'publication_date', + [], + lang('Episode.form.publication_date_hint') +) ?> +<?= form_input([ + 'id' => 'publication_date', + 'name' => 'publication_date', + 'class' => 'form-input mb-4', + 'value' => old('publication_date', date('Y-m-d H:i')), + 'data-picker' => 'datetime', +]) ?> <?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?> <legend> diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php index 8ef2850a8e..cbce3925ab 100644 --- a/app/Views/admin/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -16,6 +16,7 @@ 'class' => 'flex flex-col', ]) ?> <?= csrf_field() ?> +<?= form_hidden('client_timezone', 'UTC') ?> <?= form_section( lang('Episode.form.info_section_title'), @@ -197,44 +198,24 @@ lang('Episode.form.publication_section_subtitle') ) ?> -<?= form_fieldset('', ['class' => 'flex mb-4']) ?> -<legend><?= lang('Episode.form.published_at.label') ?></legend> -<div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.publication_date'), 'publication_date', [ - 'class' => 'sr-only', - ]) ?> - <?= form_input([ - 'id' => 'publication_date', - 'name' => 'publication_date', - 'class' => 'form-input', - 'value' => old( - 'publication_date', - $episode->published_at - ? $episode->published_at->format('Y-m-d') - : '' - ), - 'type' => 'date', - ]) ?> -</div> - -<div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.publication_time'), 'publication_time', [ - 'class' => 'sr-only', - ]) ?> - <?= form_input([ - 'id' => 'publication_time', - 'name' => 'publication_time', - 'class' => 'form-input', - 'value' => old( - 'publication_time', - $episode->published_at ? $episode->published_at->format('H:i') : '' - ), - 'placeholder' => '--:--', - 'type' => 'time', - ]) ?> -</div> -<?= form_fieldset_close() ?> - +<?= form_label( + lang('Episode.form.publication_date'), + 'publication_date', + [], + lang('Episode.form.publication_date_hint') +) ?> +<?= form_input([ + 'id' => 'publication_date', + 'name' => 'publication_date', + 'class' => 'form-input mb-4', + 'value' => old( + 'publication_date', + $episode->published_at + ? $episode->published_at->format('Y-m-d H:i') + : '' + ), + 'data-picker' => 'datetime', +]) ?> <?= form_fieldset('', ['class' => 'mb-6']) ?> <legend> @@ -288,6 +269,7 @@ <?= form_switch( lang('Episode.form.block') . hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'), + ['id' => 'block', 'name' => 'block'], 'yes', old('block', $episode->block) diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index 5adf9c8cd9..9f0330086b 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -97,19 +97,13 @@ </div> </div> <div class="mb-2 text-xs"> - <time - pubdate - datetime="<?= $episode->published_at->toDateTimeString() ?>" - title="<?= $episode->published_at ?>"> - <?= lang('Common.mediumDate', [ + <?= publication_pill( $episode->published_at, - ]) ?> - </time> + $episode->is_published + ) ?> <span class="mx-1">•</span> <time datetime="PT<?= $episode->enclosure_duration ?>S"> - <?= lang('Common.duration', [ - $episode->enclosure_duration, - ]) ?> + <?= format_duration($episode->enclosure_duration) ?> </time> </div> <audio controls preload="none" class="w-full mt-auto"> @@ -126,5 +120,4 @@ <?= $pager->links() ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index 07a88fd9a1..d36151f19e 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -5,7 +5,12 @@ <?= $this->endSection() ?> <?= $this->section('pageTitle') ?> -<?= $episode->title ?> +<?= $episode->title . + publication_pill( + $episode->published_at, + $episode->is_published, + 'text-sm ml-2 align-middle' + ) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php index 3c34726ba4..0ebf49946d 100644 --- a/app/Views/admin/my_account/change_password.php +++ b/app/Views/admin/my_account/change_password.php @@ -44,5 +44,4 @@ <?= form_close() ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/my_account/view.php b/app/Views/admin/my_account/view.php index 6dc6b8d932..77d3092219 100644 --- a/app/Views/admin/my_account/view.php +++ b/app/Views/admin/my_account/view.php @@ -13,5 +13,4 @@ <?= view('admin/_partials/_user_info.php', ['user' => user()]) ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index 7b08bd8bf1..09b2aa08c8 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -238,7 +238,14 @@ 'value' => old('publisher'), ]) ?> -<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?> +<?= form_label( + lang('Podcast.form.copyright'), + 'copyright', + [], + + '', + true +) ?> <?= form_input([ 'id' => 'copyright', 'name' => 'copyright', diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php index 840bfa5569..b4dadbfa8c 100644 --- a/app/Views/admin/podcast/latest_episodes.php +++ b/app/Views/admin/podcast/latest_episodes.php @@ -10,9 +10,9 @@ </a> </header> <?php if ($episodes): ?> - <div class="flex justify-between gap-4 overflow-x-auto"> + <div class="flex p-2 space-x-4 overflow-x-auto"> <?php foreach ($episodes as $episode): ?> - <article class="flex flex-col w-56 mb-4 bg-white border rounded shadow" style="min-width: 12rem;"> + <article class="flex flex-col w-56 bg-white border rounded shadow" style="min-width: 12rem;"> <img src="<?= $episode->image->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover" /> @@ -61,7 +61,9 @@ <span class="mx-1">•</span> <time pubdate - datetime="<?= $episode->published_at->toDateTimeString() ?>" + datetime="<?= $episode->published_at->format( + DateTime::ATOM + ) ?>" title="<?= $episode->published_at ?>"> <?= lang('Common.mediumDate', [ $episode->published_at, diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php index a6a00c7237..28aa7ef625 100644 --- a/app/Views/admin/podcast/list.php +++ b/app/Views/admin/podcast/list.php @@ -62,5 +62,4 @@ <?php endif; ?> </div> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php index b55c0d136e..aaf8369b52 100644 --- a/app/Views/admin/user/create.php +++ b/app/Views/admin/user/create.php @@ -50,5 +50,4 @@ <?= form_close() ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php index 1e54610cb0..9f409dc126 100644 --- a/app/Views/admin/user/list.php +++ b/app/Views/admin/user/list.php @@ -85,5 +85,4 @@ $users ) ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/episode.php b/app/Views/episode.php index 9baa9270db..c3c9e7f8a6 100644 --- a/app/Views/episode.php +++ b/app/Views/episode.php @@ -11,6 +11,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="stylesheet" href="/assets/index.css"/> + <script src="/assets/podcast.js" type="module" defer></script> </head> <body class="flex flex-col min-h-screen mx-auto"> @@ -85,17 +86,13 @@ <div class="text-sm"> <time pubdate - datetime="<?= $episode->published_at->toDateTimeString() ?>" + datetime="<?= $episode->published_at->format(DateTime::ATOM) ?>" title="<?= $episode->published_at ?>"> <?= lang('Common.mediumDate', [$episode->published_at]) ?> </time> <span class="mx-1">•</span> <time datetime="PT<?= $episode->enclosure_duration ?>S"> - <?= lang( - 'Common.duration', - [$episode->enclosure_duration], - 'en' - ) ?> + <?= format_duration($episode->enclosure_duration) ?> </time> </div> <audio controls preload="none" class="w-full mt-auto"> @@ -110,9 +107,9 @@ </section> </main> <footer class="px-2 py-4 border-t "> - <div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row "> + <div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row "> <?= render_page_links('inline-flex mb-4 md:mb-0') ?> - <div class="flex flex-col items-end text-xs"> + <div class="flex flex-col items-end"> <p><?= $podcast->copyright ?></p> <p><?= lang('Common.powered_by', [ 'castopod' => diff --git a/app/Views/errors/cli/error_exception.php b/app/Views/errors/cli/error_exception.php index 1ad33d0679..c161311cc5 100644 --- a/app/Views/errors/cli/error_exception.php +++ b/app/Views/errors/cli/error_exception.php @@ -20,4 +20,5 @@ Line Number: <?= $exception->getLine() ?> <?php endif; ?> <?php endforeach; ?> -<?php endif; ?> +<?php endif; +?> diff --git a/app/Views/podcast.php b/app/Views/podcast.php index 346e296fa2..c70db9fa25 100644 --- a/app/Views/podcast.php +++ b/app/Views/podcast.php @@ -13,6 +13,7 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="stylesheet" href="/assets/index.css"/> <link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>"/> + <script src="/assets/podcast.js" type="module" defer></script> </head> <body class="flex flex-col min-h-screen"> @@ -127,7 +128,9 @@ <div class="mb-2 text-xs"> <time pubdate - datetime="<?= $episode->published_at->toDateTimeString() ?>" + datetime="<?= $episode->published_at->format( + DateTime::ATOM + ) ?>" title="<?= $episode->published_at ?>"> <?= lang('Common.mediumDate', [ $episode->published_at, @@ -135,9 +138,9 @@ </time> <span class="mx-1">•</span> <time datetime="PT<?= $episode->enclosure_duration ?>S"> - <?= lang('Common.duration', [ - $episode->enclosure_duration, - ]) ?> + <?= format_duration( + $episode->enclosure_duration + ) ?> </time> </div> <audio controls preload="none" class="w-full mt-auto"> @@ -159,9 +162,9 @@ </section> </main> <footer class="px-2 py-4 border-t "> - <div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row "> + <div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row "> <?= render_page_links('inline-flex mb-4 md:mb-0') ?> - <div class="flex flex-col items-center text-xs md:items-end"> + <div class="flex flex-col items-center md:items-end"> <p><?= $podcast->copyright ?></p> <p><?= lang('Common.powered_by', [ 'castopod' => diff --git a/composer.lock b/composer.lock index 38bacea659..b893284509 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37551523e4097a9341bc00dd317f573d", + "content-hash": "58e59ff661eaa3553d3f9f9f88b9d274", "packages": [ { "name": "codeigniter4/codeigniter4", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/codeigniter4/CodeIgniter4.git", - "reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a" + "reference": "58993fbbab54a2523be25e8230337b855f465a7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/13ff147fa4cd9db15888b041ef35bc22ed94252a", - "reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/58993fbbab54a2523be25e8230337b855f465a7a", + "reference": "58993fbbab54a2523be25e8230337b855f465a7a", "shasum": "" }, "require": { @@ -53,7 +53,6 @@ }, "scripts": { "post-update-cmd": [ - "@composer dump-autoload", "CodeIgniter\\ComposerScripts::postUpdate", "bash admin/setup.sh" ], @@ -75,7 +74,7 @@ "slack": "https://codeigniterchat.slack.com", "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" }, - "time": "2020-10-20T18:13:11+00:00" + "time": "2020-10-21T16:26:19+00:00" }, { "name": "composer/ca-bundle", @@ -805,12 +804,12 @@ "source": { "type": "git", "url": "https://github.com/lonnieezell/myth-auth.git", - "reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c" + "reference": "fe9739e1a410d9a30292faee9e8b6369667241e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/e9d6a2f557bd275158e0b84624534b2abeeb539c", - "reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c", + "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/fe9739e1a410d9a30292faee9e8b6369667241e8", + "reference": "fe9739e1a410d9a30292faee9e8b6369667241e8", "shasum": "" }, "require": { @@ -860,7 +859,7 @@ "type": "patreon" } ], - "time": "2020-10-16T18:51:37+00:00" + "time": "2020-10-22T03:25:47+00:00" }, { "name": "opawg/user-agents-php", diff --git a/package-lock.json b/package-lock.json index 047c1bfae6..0267e0055c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@amcharts/amcharts4": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.7.tgz", - "integrity": "sha512-XWITAuewadEnkX9XgZTqT6CUn91gCJpvLJYrnSdnwu4GOGV4Siu6esoEb4JEYQYEDCzDIK3zlmOT5+a0fulcTw==", + "version": "4.10.8", + "resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.8.tgz", + "integrity": "sha512-2xIPHkvuxhsN49btE+K0ThO0CxvEgHC+n2aFa05GLwIH2JKgSjFBmjSvELrEqlEYf2mEPjmKjuYe6d4TgHfGUA==", "requires": { "@babel/runtime": "^7.6.3", "core-js": "^3.0.0", @@ -53,16 +53,16 @@ "dev": true }, "@babel/core": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.1.tgz", - "integrity": "sha512-6bGmltqzIJrinwRRdczQsMhruSi9Sqty9Te+/5hudn4Izx/JYRhW1QELpR+CIL0gC/c9A7WroH6FmkDGxmWx3w==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", + "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.1", "@babel/helper-module-transforms": "^7.12.1", "@babel/helpers": "^7.12.1", - "@babel/parser": "^7.12.1", + "@babel/parser": "^7.12.3", "@babel/template": "^7.10.4", "@babel/traverse": "^7.12.1", "@babel/types": "^7.12.1", @@ -102,6 +102,12 @@ "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", + "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", + "dev": true + }, "@babel/types": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", @@ -2379,13 +2385,13 @@ "integrity": "sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==" }, "@prettier/plugin-php": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.15.0.tgz", - "integrity": "sha512-OnzCmDTDdWLkm2nsvtiWKip1ePoy+KucY1h9zHDVXIFWBrd+OZATeZZgC7JU7gjly96g86hW1ZHpbF9ip9KHfg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.15.1.tgz", + "integrity": "sha512-uQiaGGXCs0uqpck1LyDU+V4Z50Qqml7ltajPQL+DB43r5aHVawDCSkgLGYZJSb1g+hK5eBmdVBqMa7ED8EBjbA==", "dev": true, "requires": { "linguist-languages": "^7.5.1", - "mem": "^6.0.1", + "mem": "^8.0.0", "php-parser": "3.0.2" } }, @@ -3133,13 +3139,13 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.4.1.tgz", - "integrity": "sha512-O+8Utz8pb4OmcA+Nfi5THQnQpHSD2sDUNw9AxNHpuYOo326HZTtG8gsfT+EAYuVrFNaLyNb2QnUNkmTRDskuRA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.5.0.tgz", + "integrity": "sha512-mjb/gwNcmDKNt+6mb7Aj/TjKzIJjOPcoCJpjBQC9ZnTRnBt1p4q5dJSSmIqAtsZ/Pff5N+hJlbiPc5bl6QN4OQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.4.1", - "@typescript-eslint/scope-manager": "4.4.1", + "@typescript-eslint/experimental-utils": "4.5.0", + "@typescript-eslint/scope-manager": "4.5.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -3156,55 +3162,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.1.tgz", - "integrity": "sha512-Nt4EVlb1mqExW9cWhpV6pd1a3DkUbX9DeyYsdoeziKOpIJ04S2KMVDO+SEidsXRH/XHDpbzXykKcMTLdTXH6cQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.5.0.tgz", + "integrity": "sha512-bW9IpSAKYvkqDGRZzayBXIgPsj2xmmVHLJ+flGSoN0fF98pGoKFhbunIol0VF2Crka7z984EEhFi623Rl7e6gg==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.4.1", - "@typescript-eslint/types": "4.4.1", - "@typescript-eslint/typescript-estree": "4.4.1", + "@typescript-eslint/scope-manager": "4.5.0", + "@typescript-eslint/types": "4.5.0", + "@typescript-eslint/typescript-estree": "4.5.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.4.1.tgz", - "integrity": "sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.5.0.tgz", + "integrity": "sha512-xb+gmyhQcnDWe+5+xxaQk5iCw6KqXd8VQxGiTeELTMoYeRjpocZYYRP1gFVM2C8Yl0SpUvLa1lhprwqZ00w3Iw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.4.1", - "@typescript-eslint/types": "4.4.1", - "@typescript-eslint/typescript-estree": "4.4.1", + "@typescript-eslint/scope-manager": "4.5.0", + "@typescript-eslint/types": "4.5.0", + "@typescript-eslint/typescript-estree": "4.5.0", "debug": "^4.1.1" } }, "@typescript-eslint/scope-manager": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz", - "integrity": "sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.5.0.tgz", + "integrity": "sha512-C0cEO0cTMPJ/w4RA/KVe4LFFkkSh9VHoFzKmyaaDWAnPYIEzVCtJ+Un8GZoJhcvq+mPFXEsXa01lcZDHDG6Www==", "dev": true, "requires": { - "@typescript-eslint/types": "4.4.1", - "@typescript-eslint/visitor-keys": "4.4.1" + "@typescript-eslint/types": "4.5.0", + "@typescript-eslint/visitor-keys": "4.5.0" } }, "@typescript-eslint/types": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.4.1.tgz", - "integrity": "sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.5.0.tgz", + "integrity": "sha512-n2uQoXnyWNk0Les9MtF0gCK3JiWd987JQi97dMSxBOzVoLZXCNtxFckVqt1h8xuI1ix01t+iMY4h4rFMj/303g==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz", - "integrity": "sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.5.0.tgz", + "integrity": "sha512-gN1mffq3zwRAjlYWzb5DanarOPdajQwx5MEWkWCk0XvqC8JpafDTeioDoow2L4CA/RkYZu7xEsGZRhqrTsAG8w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.4.1", - "@typescript-eslint/visitor-keys": "4.4.1", + "@typescript-eslint/types": "4.5.0", + "@typescript-eslint/visitor-keys": "4.5.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -3222,12 +3228,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz", - "integrity": "sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.5.0.tgz", + "integrity": "sha512-UHq4FSa55NDZqscRU//O5ROFhHa9Hqn9KWTEvJGTArtTQp5GKv9Zqf6d/Q3YXXcFv4woyBml7fJQlQ+OuqRcHA==", "dev": true, "requires": { - "@typescript-eslint/types": "4.4.1", + "@typescript-eslint/types": "4.5.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -5909,9 +5915,9 @@ } }, "eslint-config-prettier": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.13.0.tgz", - "integrity": "sha512-LcT0i0LSmnzqK2t764pyIt7kKH2AuuqKRTtJTdddWxOiUja9HdG5GXBVF2gmCTvVYWVsTu8J2MhJLVGRh+pj8w==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.14.0.tgz", + "integrity": "sha512-DbVwh0qZhAC7CNDWcq8cBdK6FcVHiMTKmCypOPWeZkp9hJ8xYwTaWSa6bb6cjfi8KOeJy0e9a8Izxyx+O4+gCQ==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -6488,6 +6494,11 @@ "write": "1.0.3" } }, + "flatpickr": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.6.tgz", + "integrity": "sha512-EZ48CJMttMg3maMhJoX+GvTuuEhX/RbA1YeuI19attP3pwBdbYy6+yqAEVm0o0hSBFYBiLbVxscLW6gJXq6H3A==" + }, "flatted": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", @@ -7888,9 +7899,9 @@ } }, "lint-staged": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.1.tgz", - "integrity": "sha512-E2Y6Mu1haUD3ZefzwBG8tqy3QDQ9udWRS946YcuDCU8Mi22RjwxrEhLrqTLszxl80DG/sCtKdGCArzEkTsBzJQ==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.2.tgz", + "integrity": "sha512-OLCA9K1hS+Sl179SO6kX0JtnsaKj/MZalEhUj5yAgXsb63qPI/Gfn6Ua1KuZdbfkZNEu3/n5C/obYCu70IMt9g==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -8612,13 +8623,13 @@ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, "mem": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-6.1.1.tgz", - "integrity": "sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.0.0.tgz", + "integrity": "sha512-qrcJOe6uD+EW8Wrci1Vdiua/15Xw3n/QnaNXE7varnB6InxSk7nu3/i5jfy3S6kWxr8WYJ6R1o0afMUtvorTsA==", "dev": true, "requires": { "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.0.0" + "mimic-fn": "^3.1.0" } }, "meow": { @@ -15403,9 +15414,9 @@ } }, "rollup": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.31.0.tgz", - "integrity": "sha512-0d8S3XwEZ7aCP910/9SjnelgLvC+ZXziouVolzxPOM1zvKkHioGkWGJIWmlOULlmvB8BZ6S0wrgsT4yMz0eyMg==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz", + "integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -17027,9 +17038,9 @@ } }, "tailwindcss": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.2.tgz", - "integrity": "sha512-D3uKSZZkh4GaKiZWmPEfNrqEmEuYdwaqXOQ7trYSQQFI5laSD9+b2FUUj5g39nk5R1omKp5tBW9wZsfJq+KIVA==", + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.5.tgz", + "integrity": "sha512-Je5t1fAfyW333YTpSxF+8uJwbnrkpyBskDtZYgSMMKQbNp6QUhEKJ4g/JIevZjD2Zidz9VxLraEUq/yWOx6nQg==", "dev": true, "requires": { "@fullhuman/postcss-purgecss": "^2.1.2", diff --git a/package.json b/package.json index 812c0818e0..d1b9cf549e 100644 --- a/package.json +++ b/package.json @@ -25,23 +25,24 @@ "release": "semantic-release" }, "dependencies": { - "@amcharts/amcharts4": "^4.10.7", + "@amcharts/amcharts4": "^4.10.8", "@amcharts/amcharts4-geodata": "^4.1.17", "@popperjs/core": "^2.5.3", "choices.js": "^9.0.1", + "flatpickr": "^4.6.6", "prosemirror-example-setup": "^1.1.2", "prosemirror-markdown": "^1.5.0", "prosemirror-state": "^1.3.3", "prosemirror-view": "^1.16.0" }, "devDependencies": { - "@babel/core": "^7.12.1", + "@babel/core": "^7.12.3", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/preset-env": "^7.12.1", "@babel/preset-typescript": "^7.12.1", "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", - "@prettier/plugin-php": "^0.15.0", + "@prettier/plugin-php": "^0.15.1", "@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-commonjs": "^15.1.0", "@rollup/plugin-json": "^4.1.0", @@ -55,22 +56,22 @@ "@tailwindcss/typography": "^0.2.0", "@types/prosemirror-markdown": "^1.0.3", "@types/prosemirror-view": "^1.16.1", - "@typescript-eslint/eslint-plugin": "^4.4.1", - "@typescript-eslint/parser": "^4.4.1", + "@typescript-eslint/eslint-plugin": "^4.5.0", + "@typescript-eslint/parser": "^4.5.0", "cross-env": "^7.0.2", "cssnano": "^4.1.10", "cz-conventional-changelog": "^3.3.0", "eslint": "^7.11.0", - "eslint-config-prettier": "^6.13.0", + "eslint-config-prettier": "^6.14.0", "eslint-plugin-prettier": "^3.1.4", "husky": "^4.3.0", - "lint-staged": "^10.4.1", + "lint-staged": "^10.4.2", "postcss-cli": "^8.1.0", "postcss-import": "^12.0.1", "postcss-preset-env": "^6.7.0", "prettier": "2.1.2", "prettier-plugin-organize-imports": "^1.1.1", - "rollup": "^2.31.0", + "rollup": "^2.32.1", "rollup-plugin-multi-input": "^1.1.1", "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-postcss": "^3.1.8", @@ -79,7 +80,7 @@ "stylelint": "^13.7.2", "stylelint-config-standard": "^20.0.0", "svgo": "^1.3.2", - "tailwindcss": "^1.9.2", + "tailwindcss": "^1.9.5", "typescript": "^4.0.3" }, "husky": { diff --git a/tests/README.md b/tests/README.md index 24f7c828c6..ba8aa0e254 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,9 +1,9 @@ # Running Application Tests This is the quick-start to CodeIgniter testing. Its intent is to describe what -it takes to set up your application and get it ready to run unit tests. -It is not intended to be a full description of the test features that you can -use to test your application. Those details can be found in the documentation. +it takes to set up your application and get it ready to run unit tests. It is +not intended to be a full description of the test features that you can use to +test your application. Those details can be found in the documentation. ## Resources @@ -15,33 +15,36 @@ use to test your application. Those details can be found in the documentation. It is recommended to use the latest version of PHPUnit. At the time of this writing we are running version 8.5.2. Support for this has been built into the **composer.json** file that ships with CodeIgniter and can easily be installed -via [Composer](https://getcomposer.org/) if you don't already have it installed globally. +via [Composer](https://getcomposer.org/) if you don't already have it installed +globally. > composer install -If running under OS X or Linux, you can create a symbolic link to make running tests a touch nicer. +If running under OS X or Linux, you can create a symbolic link to make running +tests a touch nicer. > ln -s ./vendor/bin/phpunit ./phpunit -You also need to install [XDebug](https://xdebug.org/index.php) in order -for code coverage to be calculated successfully. +You also need to install [XDebug](https://xdebug.org/index.php) in order for +code coverage to be calculated successfully. ## Setting Up -A number of the tests use a running database. -In order to set up the database edit the details for the `tests` group in -**app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine -that is currently running on your machine. More details on a test database setup are in the +A number of the tests use a running database. In order to set up the database +edit the details for the `tests` group in **app/Config/Database.php** or +**phpunit.xml**. Make sure that you provide a database engine that is currently +running on your machine. More details on a test database setup are in the _Docs>>Testing>>Testing Your Database_ section of the documentation. -If you want to run the tests without using live database you can -exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** - -call it **phpunit.xml** - and comment out the <testsuite> named "database". This will make -the tests run quite a bit faster. +If you want to run the tests without using live database you can exclude +@DatabaseLive group. Or make a copy of **phpunit.dist.xml** - call it +**phpunit.xml** - and comment out the <testsuite> named "database". This will +make the tests run quite a bit faster. ## Running the tests -The entire test suite can be run by simply typing one command-line command from the main directory. +The entire test suite can be run by simply typing one command-line command from +the main directory. > ./phpunit @@ -52,59 +55,62 @@ directory name after phpunit. ## Generating Code Coverage -To generate coverage information, including HTML reports you can view in your browser, -you can use the following command: +To generate coverage information, including HTML reports you can view in your +browser, you can use the following command: > ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m This runs all of the tests again collecting information about how many lines, -functions, and files are tested. It also reports the percentage of the code that is covered by tests. -It is collected in two formats: a simple text file that provides an overview as well -as a comprehensive collection of HTML files that show the status of every line of code in the project. +functions, and files are tested. It also reports the percentage of the code that +is covered by tests. It is collected in two formats: a simple text file that +provides an overview as well as a comprehensive collection of HTML files that +show the status of every line of code in the project. -The text file can be found at **tests/coverage.txt**. -The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser. +The text file can be found at **tests/coverage.txt**. The HTML files can be +viewed by opening **tests/coverage/index.html** in your favorite browser. ## PHPUnit XML Configuration The repository has a `phpunit.xml.dist` file in the project root that's used for -PHPUnit configuration. This is used to provide a default configuration if you -do not have your own configuration file in the project root. +PHPUnit configuration. This is used to provide a default configuration if you do +not have your own configuration file in the project root. -The normal practice would be to copy `phpunit.xml.dist` to `phpunit.xml` -(which is git ignored), and to tailor it as you see fit. -For instance, you might wish to exclude database tests, or automatically generate -HTML code coverage reports. +The normal practice would be to copy `phpunit.xml.dist` to `phpunit.xml` (which +is git ignored), and to tailor it as you see fit. For instance, you might wish +to exclude database tests, or automatically generate HTML code coverage reports. ## Test Cases Every test needs a _test case_, or class that your tests extend. CodeIgniter 4 provides a few that you may use directly: -- `CodeIgniter\Test\CIUnitTestCase` - for basic tests with no other service needs +- `CodeIgniter\Test\CIUnitTestCase` - for basic tests with no other service + needs - `CodeIgniter\Test\CIDatabaseTestCase` - for tests that need database access -Most of the time you will want to write your own test cases to hold functions and services -common to your test suites. +Most of the time you will want to write your own test cases to hold functions +and services common to your test suites. ## Creating Tests -All tests go in the **tests/** directory. Each test file is a class that extends a -**Test Case** (see above) and contains methods for the individual tests. These method -names must start with the word "test" and should have descriptive names for precisely what -they are testing: -`testUserCanModifyFile()` `testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()` +All tests go in the **tests/** directory. Each test file is a class that extends +a **Test Case** (see above) and contains methods for the individual tests. These +method names must start with the word "test" and should have descriptive names +for precisely what they are testing: `testUserCanModifyFile()` +`testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()` -Writing tests is an art, and there are many resources available to help learn how. -Review the links above and always pay attention to your code coverage. +Writing tests is an art, and there are many resources available to help learn +how. Review the links above and always pay attention to your code coverage. ### Database Tests -Tests can include migrating, seeding, and testing against a mock or live<sup>1</sup> database. -Be sure to modify the test case (or create your own) to point to your seed and migrations -and include any additional steps to be run before tests in the `setUp()` method. +Tests can include migrating, seeding, and testing against a mock or +live<sup>1</sup> database. Be sure to modify the test case (or create your own) +to point to your seed and migrations and include any additional steps to be run +before tests in the `setUp()` method. -<sup>1</sup> Note: If you are using database tests that require a live database connection -you will need to rename **phpunit.xml.dist** to **phpunit.xml**, uncomment the database -configuration lines and add your connection details. Prevent **phpunit.xml** from being -tracked in your repo by adding it to **.gitignore**. +<sup>1</sup> Note: If you are using database tests that require a live database +connection you will need to rename **phpunit.xml.dist** to **phpunit.xml**, +uncomment the database configuration lines and add your connection details. +Prevent **phpunit.xml** from being tracked in your repo by adding it to +**.gitignore**. diff --git a/tsconfig.json b/tsconfig.json index 30890ef327..6cfc4c30f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,6 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - } + }, + "include": ["app/Views/_assets/**/*"] } -- GitLab