From 28c706fee3b094f2a580975592014ce3d5205dce Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 Jan 2021 23:01:11 +0000 Subject: [PATCH 1/4] Added strikethrough support to back-end md rendering Needed to tweak the default library strikethrough extension so that it uses the same element as front-end. Added testing to cover. For #2470. --- .../Markdown/CustomStrikeThroughExtension.php | 16 +++++++++++++ .../Markdown/CustomStrikethroughRenderer.php | 24 +++++++++++++++++++ app/Entities/Tools/PageContent.php | 2 ++ tests/Entity/PageContentTest.php | 18 ++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php create mode 100644 app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php diff --git a/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php b/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php new file mode 100644 index 000000000..cb8b0ffc2 --- /dev/null +++ b/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php @@ -0,0 +1,16 @@ +addDelimiterProcessor(new StrikethroughDelimiterProcessor()); + $environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer()); + } +} \ No newline at end of file diff --git a/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php b/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php new file mode 100644 index 000000000..4371fb84c --- /dev/null +++ b/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php @@ -0,0 +1,24 @@ + HTML tags instead of in order to + * match front-end markdown-it rendering. + */ +class CustomStrikethroughRenderer implements InlineRendererInterface +{ + public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer) + { + if (!($inline instanceof Strikethrough)) { + throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline)); + } + + return new HtmlElement('s', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children())); + } +} \ No newline at end of file diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 91de94211..62982f4ad 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -1,6 +1,7 @@ addExtension(new TableExtension()); $environment->addExtension(new TaskListExtension()); + $environment->addExtension(new CustomStrikeThroughExtension()); $converter = new CommonMarkConverter([], $environment); return $converter->convertToHtml($markdown); } diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 857645823..6d5200794 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -461,4 +461,22 @@ class PageContentTest extends TestCase $pageView = $this->get($page->getUrl()); $pageView->assertElementExists('.page-content input[type=checkbox]'); } + + public function test_page_markdown_strikethrough_rendering() + { + $this->asEditor(); + $page = Page::query()->first(); + + $content = '~~some crossed out text~~'; + $this->put($page->getUrl(), [ + 'name' => $page->name, 'markdown' => $content, + 'html' => '', 'summary' => '' + ]); + + $page->refresh(); + $this->assertStringMatchesFormat('%Asome crossed out text%A', $page->html); + + $pageView = $this->get($page->getUrl()); + $pageView->assertElementExists('.page-content p > s'); + } } From d0a7a8b890cfe570b273addbf8d9cba120a5bc9e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 Jan 2021 22:43:22 +0000 Subject: [PATCH 2/4] Improved some query efficiencies on user list --- app/Auth/User.php | 24 ++++++++++++++++-------- app/Auth/UserRepo.php | 10 +++------- app/Http/Controllers/UserController.php | 1 + resources/views/users/index.blade.php | 6 +++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/Auth/User.php b/app/Auth/User.php index fdfd9e616..9d7eaa72e 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -1,19 +1,20 @@ 'datetime']; + /** * The attributes excluded from the model's JSON form. * @var array @@ -181,7 +184,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Get the social account associated with this user. - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * @return HasMany */ public function socialAccounts() { @@ -218,7 +221,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon try { $avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default; - } catch (\Exception $err) { + } catch (Exception $err) { $avatar = $default; } return $avatar; @@ -226,7 +229,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Get the avatar for the user. - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return BelongsTo */ public function avatar() { @@ -242,11 +245,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } /** - * Get the latest activity instance for this user. + * Get the last activity time for this user. */ - public function latestActivity(): HasOne + public function scopeWithLastActivityAt(Builder $query) { - return $this->hasOne(Activity::class)->latest(); + $query->addSelect(['activities.created_at as last_activity_at']) + ->leftJoinSub(function (\Illuminate\Database\Query\Builder $query) { + $query->from('activities')->select('user_id') + ->selectRaw('max(created_at) as created_at') + ->groupBy('user_id'); + }, 'activities', 'users.id', '=', 'activities.user_id'); } /** diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 6fb5dfa0f..29a0ebc14 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -59,14 +59,10 @@ class UserRepo public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator { $sort = $sortData['sort']; - if ($sort === 'latest_activity') { - $sort = \BookStack\Actions\Activity::query()->select('created_at') - ->whereColumn('activities.user_id', 'users.id') - ->latest() - ->take(1); - } - $query = User::query()->with(['roles', 'avatar', 'latestActivity']) + $query = User::query()->select(['*']) + ->withLastActivityAt() + ->with(['roles', 'avatar']) ->orderBy($sort, $sortData['order']); if ($sortData['search']) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 852d507c1..92e1cd8b7 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -41,6 +41,7 @@ class UserController extends Controller 'sort' => $request->get('sort', 'name'), ]; $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails); + $this->setPageTitle(trans('settings.users')); $users->appends($listDetails); return view('users.index', ['users' => $users, 'listDetails' => $listDetails]); diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 4b5bad0fd..68641ca64 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -37,7 +37,7 @@ {{ trans('settings.role_user_roles') }} - {{ trans('settings.users_latest_activity') }} + {{ trans('settings.users_latest_activity') }} @foreach($users as $user) @@ -58,8 +58,8 @@ @endforeach - @if($user->latestActivity) - {{ $user->latestActivity->created_at->diffForHumans() }} + @if($user->last_activity_at) + {{ $user->last_activity_at->diffForHumans() }} @endif From da7c686541eee0f20b9c85fc2a5e12f6aa6e0ec1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 Jan 2021 23:12:51 +0000 Subject: [PATCH 3/4] Made books and shelf listing views slightly more efficient --- app/Entities/Repos/BookRepo.php | 2 +- app/Entities/Repos/BookshelfRepo.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 68d62887b..27d0b4075 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -35,7 +35,7 @@ class BookRepo */ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator { - return Book::visible()->orderBy($sort, $order)->paginate($count); + return Book::visible()->with('cover')->orderBy($sort, $order)->paginate($count); } /** diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index b15241fb3..649f4b0c4 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -30,7 +30,7 @@ class BookshelfRepo public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator { return Bookshelf::visible() - ->with('visibleBooks') + ->with(['visibleBooks', 'cover']) ->orderBy($sort, $order) ->paginate($count); } From ee400eece6a926ed9769ea26cd504b36248af8c8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 Jan 2021 23:28:33 +0000 Subject: [PATCH 4/4] New Crowdin updates (#2469) * New translations settings.php (Turkish) * New translations entities.php (Turkish) * New translations settings.php (Turkish) * New translations activities.php (Turkish) * New translations validation.php (Turkish) --- resources/lang/tr/activities.php | 2 +- resources/lang/tr/entities.php | 4 +- resources/lang/tr/settings.php | 70 ++++++++++++++++---------------- resources/lang/tr/validation.php | 2 +- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/resources/lang/tr/activities.php b/resources/lang/tr/activities.php index 7dc489618..67f9653f4 100644 --- a/resources/lang/tr/activities.php +++ b/resources/lang/tr/activities.php @@ -45,5 +45,5 @@ return [ // Other 'commented_on' => 'yorum yaptı', - 'permissions_update' => 'updated permissions', + 'permissions_update' => 'güncellenmiş izinler', ]; diff --git a/resources/lang/tr/entities.php b/resources/lang/tr/entities.php index 5afd59bcf..2f447e795 100644 --- a/resources/lang/tr/entities.php +++ b/resources/lang/tr/entities.php @@ -40,7 +40,7 @@ return [ 'permissions_intro' => 'Etkinleştirildikten sonra bu izinler, diğer bütün izinlerden öncelikli olacaktır.', 'permissions_enable' => 'Özelleştirilmiş Yetkileri Etkinleştir', 'permissions_save' => 'İzinleri Kaydet', - 'permissions_owner' => 'Owner', + 'permissions_owner' => 'Sahip', // Search 'search_results' => 'Arama Sonuçları', @@ -268,7 +268,7 @@ return [ 'attachments_link_url' => 'Dosya bağlantısı', 'attachments_link_url_hint' => 'Dosyanın veya sitenin url adresi', 'attach' => 'Ekle', - 'attachments_insert_link' => 'Add Attachment Link to Page', + 'attachments_insert_link' => 'Sayfaya Bağlantı Ekle', 'attachments_edit_file' => 'Dosyayı Düzenle', 'attachments_edit_file_name' => 'Dosya Adı', 'attachments_edit_drop_upload' => 'Üzerine yazılacak dosyaları sürükleyin veya seçin', diff --git a/resources/lang/tr/settings.php b/resources/lang/tr/settings.php index be4e7baff..fd861c170 100755 --- a/resources/lang/tr/settings.php +++ b/resources/lang/tr/settings.php @@ -68,7 +68,7 @@ return [ 'maint' => 'Bakım', 'maint_image_cleanup' => 'Görselleri Temizle', 'maint_image_cleanup_desc' => "Sayfaları ve revizyon içeriklerini tarayarak hangi görsellerin ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan önce veritabanının ve görsellerin tam bir yedeğinin alındığından emin olun.", - 'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions', + 'maint_delete_images_only_in_revisions' => 'Eski sayfa revizyonlarındaki görselleri de sil', 'maint_image_cleanup_run' => 'Temizliği Başlat', 'maint_image_cleanup_warning' => 'Muhtemelen kullanılmayan :count adet görsel bulundu. Bu görselleri silmek istediğinize emin misiniz?', 'maint_image_cleanup_success' => 'Muhtemelen kullanılmayan :count adet görsel bulundu ve silindi!', @@ -80,41 +80,41 @@ return [ 'maint_send_test_email_mail_subject' => 'Deneme E-postası', 'maint_send_test_email_mail_greeting' => 'E-posta iletimi çalışıyor gibi görünüyor!', 'maint_send_test_email_mail_text' => 'Tebrikler! Eğer bu e-posta bildirimini alıyorsanız, e-posta ayarlarınız doğru bir şekilde ayarlanmış demektir.', - 'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.', - 'maint_recycle_bin_open' => 'Open Recycle Bin', + 'maint_recycle_bin_desc' => 'Silinen raflar, kitaplar, bölümler ve sayfalar geri dönüşüm kutusuna gönderilir, böylece geri yüklenebilir veya kalıcı olarak silinebilir. Geri dönüşüm kutusundaki daha eski öğeler, sistem yapılandırmasına bağlı olarak bir süre sonra otomatik olarak kaldırılabilir.', + 'maint_recycle_bin_open' => 'Geri Dönüşüm Kutusunu Aç', // Recycle Bin - 'recycle_bin' => 'Recycle Bin', - 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'recycle_bin_deleted_item' => 'Deleted Item', - 'recycle_bin_deleted_by' => 'Deleted By', - 'recycle_bin_deleted_at' => 'Deletion Time', - 'recycle_bin_permanently_delete' => 'Permanently Delete', - 'recycle_bin_restore' => 'Restore', - 'recycle_bin_contents_empty' => 'The recycle bin is currently empty', - 'recycle_bin_empty' => 'Empty Recycle Bin', - 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?', - 'recycle_bin_destroy_list' => 'Items to be Destroyed', - 'recycle_bin_restore_list' => 'Items to be Restored', - 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', - 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin' => 'Geri Dönüşüm Kutusu', + 'recycle_bin_desc' => 'Burada silinen öğeleri geri yükleyebilir veya bunları sistemden kalıcı olarak kaldırmayı seçebilirsiniz. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.', + 'recycle_bin_deleted_item' => 'Silinen öge', + 'recycle_bin_deleted_by' => 'Tarafından silindi', + 'recycle_bin_deleted_at' => 'Silinme Zamanı', + 'recycle_bin_permanently_delete' => 'Kalıcı Olarak Sil', + 'recycle_bin_restore' => 'Geri Yükle', + 'recycle_bin_contents_empty' => 'Geri dönüşüm kutusu boş', + 'recycle_bin_empty' => 'Geri Dönüşüm Kutusunu Boşalt', + 'recycle_bin_empty_confirm' => 'Bu işlem, her bir öğenin içinde bulunan içerik de dahil olmak üzere geri dönüşüm kutusundaki tüm öğeleri kalıcı olarak imha edecektir. Geri dönüşüm kutusunu boşaltmak istediğinizden emin misiniz?', + 'recycle_bin_destroy_confirm' => 'Bu işlem, bu öğeyi kalıcı olarak ve aşağıda listelenen alt öğelerle birlikte sistemden silecek ve bu içeriği geri yükleyemeyeceksiniz. Bu öğeyi kalıcı olarak silmek istediğinizden emin misiniz?', + 'recycle_bin_destroy_list' => 'Kalıcı Olarak Silinecek Öğeler', + 'recycle_bin_restore_list' => 'Geri Yüklenecek Öğeler', + 'recycle_bin_restore_confirm' => 'Bu eylem, tüm alt öğeler dahil olmak üzere silinen öğeyi orijinal konumlarına geri yükleyecektir. Orijinal konum o zamandan beri silinmişse ve şimdi geri dönüşüm kutusunda bulunuyorsa, üst öğenin de geri yüklenmesi gerekecektir.', + 'recycle_bin_restore_deleted_parent' => 'Bu öğenin üst öğesi de silindi. Bunlar, üst öğe de geri yüklenene kadar silinmiş olarak kalacaktır.', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', // Audit Log - 'audit' => 'Audit Log', - 'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'audit_event_filter' => 'Event Filter', - 'audit_event_filter_no_filter' => 'No Filter', - 'audit_deleted_item' => 'Deleted Item', - 'audit_deleted_item_name' => 'Name: :name', - 'audit_table_user' => 'User', - 'audit_table_event' => 'Event', - 'audit_table_related' => 'Related Item or Detail', - 'audit_table_date' => 'Activity Date', - 'audit_date_from' => 'Date Range From', - 'audit_date_to' => 'Date Range To', + 'audit' => 'Denetim Kaydı', + 'audit_desc' => 'Bu denetim günlüğü, sistemde izlenen etkinliklerin bir listesini görüntüler. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.', + 'audit_event_filter' => 'Etkinlik Filtresi', + 'audit_event_filter_no_filter' => 'Filtre Yok', + 'audit_deleted_item' => 'Silinen Öge', + 'audit_deleted_item_name' => 'Isim: :name', + 'audit_table_user' => 'Kullanıcı', + 'audit_table_event' => 'Etkinlik', + 'audit_table_related' => 'İlgili Öğe veya Detay', + 'audit_table_date' => 'Aktivite Tarihi', + 'audit_date_from' => 'Tarih Aralığından', + 'audit_date_to' => 'Tarih Aralığına', // Role Settings 'roles' => 'Roller', @@ -157,7 +157,7 @@ return [ 'user_profile' => 'Kullanıcı Profili', 'users_add_new' => 'Yeni Kullanıcı Ekle', 'users_search' => 'Kullanıcı Ara', - 'users_latest_activity' => 'Latest Activity', + 'users_latest_activity' => 'Son Etkinlik', 'users_details' => 'Kullanıcı Detayları', 'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve e-posta adresi belirleyin. Buraya yazacağınız e-posta adresi, uygulamaya giriş yaparken kullanılacaktır.', 'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.', @@ -175,10 +175,10 @@ return [ 'users_delete_named' => ':userName kullanıcısını sil ', 'users_delete_warning' => 'Bu işlem \':userName\' kullanıcısını sistemden tamamen silecektir.', 'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?', - 'users_migrate_ownership' => 'Migrate Ownership', - 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.', - 'users_none_selected' => 'No user selected', - 'users_delete_success' => 'User successfully removed', + 'users_migrate_ownership' => 'Sahipliği Taşıyın', + 'users_migrate_ownership_desc' => 'Başka bir kullanıcının şu anda bu kullanıcıya ait olan tüm öğelerin sahibi olmasını istiyorsanız buradan bir kullanıcı seçin.', + 'users_none_selected' => 'Hiçbir kullanıcı seçilmedi', + 'users_delete_success' => 'Kullanıcı başarıyla kaldırıldı', 'users_edit' => 'Kullanıcıyı Düzenle', 'users_edit_profile' => 'Profili Düzenle', 'users_edit_success' => 'Kullanıcı başarıyla güncellendi', diff --git a/resources/lang/tr/validation.php b/resources/lang/tr/validation.php index f0e1a1b5f..45b7189d7 100644 --- a/resources/lang/tr/validation.php +++ b/resources/lang/tr/validation.php @@ -90,7 +90,7 @@ return [ 'required_without' => ':values değerinin bulunmuyor olması, :attribute alanını zorunlu kılar.', 'required_without_all' => ':values değerlerinden hiçbirinin bulunmuyor olması, :attribute alanını zorunlu kılar.', 'same' => ':attribute ve :other eşleşmelidir.', - 'safe_url' => 'The provided link may not be safe.', + 'safe_url' => 'Sağlanan bağlantı güvenli olmayabilir.', 'size' => [ 'numeric' => ':attribute, :size boyutunda olmalıdır.', 'file' => ':attribute, :size kilobayt olmalıdır.',