Compare commits

...

28 Commits

Author SHA1 Message Date
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
339d4ec355 Fixed misalignment of page and chapter parent book
Could occur when a chapter was moved with deleted pages.
Fixes #2632
2021-03-13 15:18:37 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
26730e56ea Updated composer dependancies
Primarily to fix aws library for non-amazon use.
Related to #2603
2021-03-02 21:06:45 +00:00
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
a7e3c26fe3 Fixed markdown content on revision restore
Closes #2496
2021-02-06 14:14:38 +00:00
Dan Brown
37de4e2e0a Added test for markdown page revision restore
Also added md change detection in revision saving.
2021-02-06 13:51:05 +00:00
Dan Brown
61a911dd39 Removed "isA" usages from trashcan 2021-02-06 13:29:39 +00:00
Aleksandr Sazhin
cc5d0ef4cf Update TrashCan.php
bookshelf
2021-02-06 13:23:12 +00:00
Dan Brown
7843d8f054 Added recycle-bin test to cover type deletions 2021-02-06 13:22:31 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
d547ed4a6b Updated laravel/framework to latest 6.x version 2021-02-02 20:56:19 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
d33f136660 Updated translator attribution before release v0.31.4 2021-01-16 17:49:56 +00:00
Dan Brown
173dad345e New Crowdin updates (#2482)
* New translations entities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)
2021-01-16 17:45:59 +00:00
Dan Brown
47b0eb6324 Updated framework and other php deps 2021-01-16 17:45:04 +00:00
Dan Brown
c35c37008d Added imagetools plugin back in
For #2493
2021-01-16 17:39:30 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
ee400eece6 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)
2021-01-10 23:28:33 +00:00
Dan Brown
da7c686541 Made books and shelf listing views slightly more efficient 2021-01-10 23:12:51 +00:00
Dan Brown
d0a7a8b890 Improved some query efficiencies on user list 2021-01-10 23:02:30 +00:00
Dan Brown
28c706fee3 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.
2021-01-10 23:01:11 +00:00
30 changed files with 690 additions and 544 deletions

View File

@@ -133,3 +133,4 @@ MatthieuParis :: French
Douradinho :: Portuguese, Brazilian
Gaku Yaguchi (tama11) :: Japanese
johnroyer :: Chinese Traditional
jackaaa :: Chinese Traditional

View File

@@ -3,8 +3,10 @@
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
@@ -112,21 +114,16 @@ class PermissionService
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return \BookStack\Entities\Models\Book
*/
protected function getChapter($chapterId)
protected function getChapter(int $chapterId): ?Chapter
{
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
return $this->entityCache['chapter']->get($chapterId);
}
$chapter = $this->entityProvider->chapter->find($chapterId);
if ($chapter === null) {
$chapter = false;
}
return $chapter;
return $this->entityProvider->chapter->newQuery()
->withTrashed()
->find($chapterId);
}
/**
@@ -460,7 +457,7 @@ class PermissionService
$hasPermissiveAccessToParents = !$book->restricted;
// For pages with a chapter, Check if explicit permissions are set on the Chapter
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
$chapter = $this->getChapter($entity->chapter_id);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
if ($chapter->restricted) {

View File

@@ -1,19 +1,20 @@
<?php namespace BookStack\Auth;
use BookStack\Actions\Activity;
use BookStack\Api\ApiToken;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
use Carbon\Carbon;
use Exception;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
@@ -46,6 +47,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $fillable = ['name', 'email'];
protected $casts = ['last_activity_at' => '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');
}
/**

View File

@@ -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']) {

View File

@@ -1,8 +1,5 @@
<?php namespace BookStack\Entities\Models;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Book;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -49,7 +46,7 @@ abstract class BookChild extends Entity
// Update all child pages if a chapter
if ($this instanceof Chapter) {
foreach ($this->pages as $page) {
foreach ($this->pages()->withTrashed()->get() as $page) {
$page->changeBook($newBookId);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}

View File

@@ -177,17 +177,13 @@ class PageRepo
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
$oldMarkdown = $page->markdown;
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
// Update with new details
$page->revision_count++;
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->save();
// Remove all update drafts for this user & page.
@@ -195,7 +191,10 @@ class PageRepo
// Save a revision after updating
$summary = $input['summary'] ?? null;
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
if ($htmlChanged || $nameChanged || $markdownChanged || $summary !== null) {
$this->savePageRevision($page, $summary);
}
@@ -224,10 +223,6 @@ class PageRepo
{
$revision = new PageRevision($page->getAttributes());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -290,7 +285,13 @@ class PageRepo
$page->fill($revision->toArray());
$content = new PageContent($page);
$content->setNewHTML($revision->html);
if (!empty($revision->markdown)) {
$content->setNewMarkdown($revision->markdown);
} else {
$content->setNewHTML($revision->html);
}
$page->updated_by = user()->id;
$page->refreshSlug();
$page->save();

View File

@@ -0,0 +1,16 @@
<?php namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
class CustomStrikeThroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
}
}

View File

@@ -0,0 +1,24 @@
<?php namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
/**
* This is a somewhat clone of the League\CommonMark\Extension\Strikethrough\StrikethroughRender
* class but modified slightly to use <s> HTML tags instead of <del> 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()));
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
@@ -51,6 +52,7 @@ class PageContent
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$converter = new CommonMarkConverter([], $environment);
return $converter->convertToHtml($markdown);
}

View File

@@ -273,11 +273,11 @@ class TrashCan
$count++;
};
if ($entity->isA('chapter') || $entity->isA('book')) {
if ($entity instanceof Chapter || $entity instanceof Book) {
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
if ($entity->isA('book')) {
if ($entity instanceof Book) {
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
@@ -286,19 +286,20 @@ class TrashCan
/**
* Destroy the given entity.
* @throws Exception
*/
protected function destroyEntity(Entity $entity): int
{
if ($entity->isA('page')) {
if ($entity instanceof Page) {
return $this->destroyPage($entity);
}
if ($entity->isA('chapter')) {
if ($entity instanceof Chapter) {
return $this->destroyChapter($entity);
}
if ($entity->isA('book')) {
if ($entity instanceof Book) {
return $this->destroyBook($entity);
}
if ($entity->isA('shelf')) {
if ($entity instanceof Bookshelf) {
return $this->destroyShelf($entity);
}
}

View File

@@ -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]);

View File

@@ -18,7 +18,7 @@
"facade/ignition": "^1.16.4",
"fideloper/proxy": "^4.4.1",
"intervention/image": "^2.5.1",
"laravel/framework": "^6.20",
"laravel/framework": "^6.20.16",
"laravel/socialite": "^5.1",
"league/commonmark": "^1.5",
"league/flysystem-aws-s3-v3": "^1.0.29",

896
composer.lock generated

File diff suppressed because it is too large Load Diff

2
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -422,7 +422,7 @@ class WysiwygEditor {
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.isDarkMode = document.documentElement.classList.contains('dark-mode');
this.plugins = "image table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
this.loadPlugins();
this.tinyMceConfig = this.getTinyMceConfig();

View File

@@ -45,5 +45,5 @@ return [
// Other
'commented_on' => 'yorum yaptı',
'permissions_update' => 'updated permissions',
'permissions_update' => 'güncellenmiş izinler',
];

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.',

View File

@@ -45,5 +45,5 @@ return [
// Other
'commented_on' => '評論',
'permissions_update' => 'updated permissions',
'permissions_update' => '更新權限',
];

View File

@@ -268,7 +268,7 @@ return [
'attachments_link_url' => '連結到檔案',
'attachments_link_url_hint' => '網站或檔案的網址',
'attach' => '附加',
'attachments_insert_link' => 'Add Attachment Link to Page',
'attachments_insert_link' => '將附件連結增加到頁面',
'attachments_edit_file' => '編輯檔案',
'attachments_edit_file_name' => '檔案名稱',
'attachments_edit_drop_upload' => '刪除檔案或點選這裡上傳並覆蓋',

View File

@@ -68,7 +68,7 @@ return [
'maint' => '維護',
'maint_image_cleanup' => '清理圖像',
'maint_image_cleanup_desc' => "掃描頁面和修訂內容以檢查哪些圖像是正在使用的以及哪些圖像是多余的。確保在運行前創建完整的數據庫和映像備份。",
'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
'maint_delete_images_only_in_revisions' => '包含刪除僅在舊頁面修訂版中存在的圖像',
'maint_image_cleanup_run' => '運行清理',
'maint_image_cleanup_warning' => '發現了:count 張可能未使用的圖像。您確定要刪除這些圖像嗎?',
'maint_image_cleanup_success' => '找到並刪除了:count 張可能未使用的圖像!',
@@ -80,11 +80,11 @@ return [
'maint_send_test_email_mail_subject' => '測試郵件',
'maint_send_test_email_mail_greeting' => '電子郵件傳遞似乎有效!',
'maint_send_test_email_mail_text' => '恭喜你! 收到此電子郵件通知時,您的電子郵件設置已經認證成功。',
'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_desc' => '刪除的書架,書籍,章節和頁面將發送到回收站,以便可以還原或永久刪除它們。 回收站中的較舊項目可能會在一段時間後自動刪除,具體取決於系統配置。',
'maint_recycle_bin_open' => 'Open Recycle Bin',
// Recycle Bin
'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',
@@ -103,16 +103,16 @@ return [
'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
// Audit Log
'audit' => 'Audit Log',
'audit' => '稽核記錄',
'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_user' => '使用者',
'audit_table_event' => '活動',
'audit_table_related' => 'Related Item or Detail',
'audit_table_date' => 'Activity Date',
'audit_table_date' => '最後活動日期',
'audit_date_from' => 'Date Range From',
'audit_date_to' => 'Date Range To',
@@ -157,7 +157,7 @@ return [
'user_profile' => '使用者資料',
'users_add_new' => '加入使用者',
'users_search' => '搜尋使用者',
'users_latest_activity' => 'Latest Activity',
'users_latest_activity' => '最新活動',
'users_details' => '用戶詳情',
'users_details_desc' => '請設置用戶的顯示名稱和電子郵件地址, 該電子郵件地址將用於登錄該應用。',
'users_details_desc_no_email' => '設置一個用戶的顯示名稱,以便其他人可以認出你。',
@@ -177,7 +177,7 @@ return [
'users_delete_confirm' => '您確定要刪除這個使用者?',
'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_none_selected' => '沒有選定的使用者',
'users_delete_success' => 'User successfully removed',
'users_edit' => '編輯使用者',
'users_edit_profile' => '編輯資料',

View File

@@ -37,7 +37,7 @@
</th>
<th>{{ trans('settings.role_user_roles') }}</th>
<th class="text-right">
<a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'latest_activity']) }}">{{ trans('settings.users_latest_activity') }}</a>
<a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'last_activity_at']) }}">{{ trans('settings.users_latest_activity') }}</a>
</th>
</tr>
@foreach($users as $user)
@@ -58,8 +58,8 @@
@endforeach
</td>
<td class="text-right text-muted">
@if($user->latestActivity)
<small title="{{ $user->latestActivity->created_at->format('Y-m-d H:i:s') }}">{{ $user->latestActivity->created_at->diffForHumans() }}</small>
@if($user->last_activity_at)
<small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_at->diffForHumans() }}</small>
@endif
</td>
</tr>

View File

@@ -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('%A<s%A>some crossed out text</s>%A', $page->html);
$pageView = $this->get($page->getUrl());
$pageView->assertElementExists('.page-content p > s');
}
}

View File

@@ -66,6 +66,36 @@ class PageRevisionTest extends TestCase
$pageView->assertSee('def456');
}
public function test_page_revision_restore_with_markdown_retains_markdown_content()
{
$this->asEditor();
$pageRepo = app(PageRepo::class);
$page = Page::first();
$pageRepo->update($page, ['name' => 'updated page abc123', 'markdown' => '## New Content def456', 'summary' => 'initial page revision testing']);
$pageRepo->update($page, ['name' => 'updated page again', 'markdown' => '## New Content Updated', 'summary' => 'page revision testing']);
$page = Page::find($page->id);
$pageView = $this->get($page->getUrl());
$pageView->assertDontSee('abc123');
$pageView->assertDontSee('def456');
$revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
$restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
$page = Page::find($page->id);
$restoreReq->assertStatus(302);
$restoreReq->assertRedirect($page->getUrl());
$pageView = $this->get($page->getUrl());
$this->assertDatabaseHas('pages', [
'id' => $page->id,
'markdown' => '## New Content def456',
]);
$pageView->assertSee('abc123');
$pageView->assertSee('def456');
}
public function test_page_revision_restore_sets_new_revision_with_summary()
{
$this->asEditor();

View File

@@ -196,6 +196,24 @@ class SortTest extends TestCase
$this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');
}
public function test_chapter_move_changes_book_for_deleted_pages_within()
{
/** @var Chapter $chapter */
$chapter = Chapter::query()->whereHas('pages')->first();
$currentBook = $chapter->book;
$pageToCheck = $chapter->pages->first();
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$pageToCheck->delete();
$this->asEditor()->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$pageToCheck->refresh();
$this->assertEquals($newBook->id, $pageToCheck->book_id);
}
public function test_book_sort()
{
$oldBook = Book::query()->first();

View File

@@ -1,7 +1,10 @@
<?php namespace Tests;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use DB;
use Illuminate\Support\Carbon;
@@ -129,6 +132,21 @@ class RecycleBinTest extends TestCase
$redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
}
public function test_permanent_delete_for_each_type()
{
/** @var Entity $entity */
foreach ([new Bookshelf, new Book, new Chapter, new Page] as $entity) {
$entity = $entity->newQuery()->first();
$this->asEditor()->delete($entity->getUrl());
$deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
$deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
$deleteReq->assertRedirect('/settings/recycle-bin');
$this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
$this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
}
}
public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
{
$page = Page::query()->firstOrFail();

View File

@@ -1 +1 @@
v0.31.2
v0.31.8