Dan Brown

Merge branch 'master' into release

Showing 78 changed files with 1622 additions and 539 deletions
......@@ -56,4 +56,13 @@ class Book extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}
......
......@@ -51,4 +51,13 @@ class Chapter extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}
......
......@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
*
* @var string
*/
protected $signature = 'bookstack:regenerate-permissions';
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
/**
* The console command description.
......@@ -46,7 +46,14 @@ class RegeneratePermissions extends Command
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
}
$this->permissionService->buildJointPermissions();
\DB::setDefaultConnection($connection);
$this->comment('Permissions regenerated');
}
}
......
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\SearchService;
use Illuminate\Console\Command;
class RegenerateSearch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected $searchService;
/**
* Create a new command instance.
*
* @param SearchService $searchService
*/
public function __construct(SearchService $searchService)
{
parent::__construct();
$this->searchService = $searchService;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
}
$this->searchService->indexAllEntities();
\DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}
}
<?php
namespace BookStack\Console;
<?php namespace BookStack\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
......@@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\ClearViews::class,
\BookStack\Console\Commands\ClearActivity::class,
\BookStack\Console\Commands\ClearRevisions::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
Commands\ClearViews::class,
Commands\ClearActivity::class,
Commands\ClearRevisions::class,
Commands\RegeneratePermissions::class,
Commands\RegenerateSearch::class
];
/**
......
......@@ -4,7 +4,7 @@
class Entity extends Ownable
{
protected $fieldsToSearch = ['name', 'description'];
public $textField = 'description';
/**
* Compares this entity to another given entity.
......@@ -66,6 +66,15 @@ class Entity extends Ownable
}
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function searchTerms()
{
return $this->morphMany(SearchTerm::class, 'entity');
}
/**
* Get this entities restrictions.
*/
public function permissions()
......@@ -153,67 +162,19 @@ class Entity extends Ownable
}
/**
* Perform a full-text search on this entity.
* @param string[] $fieldsToSearch
* @param string[] $terms
* @param string[] array $wheres
* Get the body text of this entity.
* @return mixed
*/
public function fullTextSearchQuery($terms, $wheres = [])
public function getText()
{
$exactTerms = [];
$fuzzyTerms = [];
$search = static::newQuery();
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
} else {
$term = '' . $term . '*';
if ($term !== '*') $fuzzyTerms[] = $term;
}
}
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
// Perform fulltext search if relevant terms exist.
if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
return $this->{$this->textField};
}
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) {
foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if ($this->isA('chapter')) {
$search = $search->with('book');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery(){return '';}
return $search->orderBy($orderBy, 'desc');
}
}
......
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\SearchService;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
......@@ -8,16 +9,19 @@ class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
/**
* SearchController constructor.
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
parent::__construct();
}
......@@ -27,105 +31,55 @@ class SearchController extends Controller
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchAll(Request $request)
public function search(Request $request)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $pages,
'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm
]);
}
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm
return view('search/all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'hasNextPage' => $hasNextPage,
'nextPageLink' => $nextPageLink
]);
}
/**
* Search only the books in the system.
* Searches all entities within a book.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
* @param integer $bookId
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchBooks(Request $request)
public function searchBook(Request $request, $bookId)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $books,
'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm
]);
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a book.
* Searches all entities within a chapter.
* @param Request $request
* @param integer $bookId
* @param integer $chapterId
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchBook(Request $request, $bookId)
public function searchChapter(Request $request, $chapterId)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
......@@ -134,18 +88,13 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entities = collect();
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains($entityType)) {
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
......
......@@ -2,12 +2,16 @@
namespace BookStack\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification
class ConfirmEmail extends Notification implements ShouldQueue
{
use Queueable;
public $token;
/**
......
......@@ -8,8 +8,7 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
protected $fieldsToSearch = ['name', 'text'];
public $textField = 'text';
/**
* Converts this page into a simplified array.
......@@ -96,4 +95,14 @@ class Page extends Entity
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent
* @return string
*/
public function entityRawQuery($withContent = false)
{ $htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
}
......
<?php namespace BookStack;
class SearchTerm extends Model
{
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
public $timestamps = false;
/**
* Get the entity that this term belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
return $this->morphTo('entity');
}
}
......@@ -479,8 +479,7 @@ class PermissionService
* @return \Illuminate\Database\Query\Builder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageContentSelect = $fetchPageContent ? 'html' : "''";
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
......@@ -488,7 +487,7 @@ class PermissionService
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
......@@ -514,7 +513,7 @@ class PermissionService
* @param string $entityType
* @param Builder|Entity $query
* @param string $action
* @return mixed
* @return Builder
*/
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{
......@@ -540,7 +539,7 @@ class PermissionService
}
/**
* Filter items that have entities set a a polymorphic relation.
* Filter items that have entities set as a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
......
......@@ -64,6 +64,10 @@
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
],
"refresh-test-database": [
"php artisan migrate:refresh --database=mysql_testing",
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
]
},
"config": {
......
......@@ -100,7 +100,7 @@ return [
|
*/
'log' => 'single',
'log' => env('APP_LOGGING', 'single'),
/*
|--------------------------------------------------------------------------
......
......@@ -43,7 +43,8 @@ $factory->define(BookStack\Page::class, function ($faker) {
'name' => $faker->sentence,
'slug' => str_random(10),
'html' => $html,
'text' => strip_tags($html)
'text' => strip_tags($html),
'revision_count' => 1
];
});
......
......@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration
*/
public function up()
{
DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)');
DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)');
DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)');
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
}
/**
......
......@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration
*/
public function up()
{
DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)');
DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)');
DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)');
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
}
/**
......
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSearchIndexTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('search_terms', function (Blueprint $table) {
$table->increments('id');
$table->string('term', 200);
$table->string('entity_type', 100);
$table->integer('entity_id');
$table->integer('score');
$table->index('term');
$table->index('entity_type');
$table->index(['entity_type', 'entity_id']);
$table->index('score');
});
// Drop search indexes
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
Schema::dropIfExists('search_terms');
}
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRevisionCounts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
$table->integer('revision_count');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->integer('revision_number');
$table->index('revision_number');
});
// Update revision count
$pTable = DB::getTablePrefix() . 'pages';
$rTable = DB::getTablePrefix() . 'page_revisions';
DB::statement("UPDATE ${pTable} SET ${pTable}.revision_count=(SELECT count(*) FROM ${rTable} WHERE ${rTable}.page_id=${pTable}.id)");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('revision_count');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('revision_number');
});
}
}
......@@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder
$user->attachRole($role);
$books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
......@@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder
$book->pages()->saveMany($pages);
});
$restrictionService = app(\BookStack\Services\PermissionService::class);
$restrictionService->buildJointPermissions();
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
}
......
var elixir = require('laravel-elixir');
const argv = require('yargs').argv;
const gulp = require('gulp'),
plumber = require('gulp-plumber');
const autoprefixer = require('gulp-autoprefixer');
const uglify = require('gulp-uglify');
const minifycss = require('gulp-clean-css');
const sass = require('gulp-sass');
const browserify = require("browserify");
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const babelify = require("babelify");
const watchify = require("watchify");
const envify = require("envify");
const gutil = require("gulp-util");
elixir(mix => {
mix.sass('styles.scss');
mix.sass('print-styles.scss');
mix.sass('export-styles.scss');
mix.browserify('global.js', './public/js/common.js');
if (argv.production) process.env.NODE_ENV = 'production';
gulp.task('styles', () => {
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
.pipe(plumber({
errorHandler: function (error) {
console.log(error.message);
this.emit('end');
}}))
.pipe(sass())
.pipe(autoprefixer('last 2 versions'));
if (argv.production) chain = chain.pipe(minifycss());
return chain.pipe(gulp.dest('public/css/'));
});
function scriptTask(watch=false) {
let props = {
basedir: 'resources/assets/js',
debug: true,
entries: ['global.js']
};
let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
function rebundle() {
let stream = bundler.bundle();
stream = stream.pipe(source('common.js'));
if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
return stream.pipe(gulp.dest('public/js/'));
}
bundler.on('update', function() {
rebundle();
gutil.log('Rebundle...');
});
bundler.on('log', gutil.log);
return rebundle();
}
gulp.task('scripts', () => {scriptTask(false)});
gulp.task('scripts-watch', () => {scriptTask(true)});
gulp.task('default', ['styles', 'scripts-watch'], () => {
gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
});
gulp.task('build', ['styles', 'scripts']);
\ No newline at end of file
......
{
"private": true,
"scripts": {
"build": "gulp --production",
"dev": "gulp watch",
"watch": "gulp watch"
"build": "gulp build",
"production": "gulp build --production",
"dev": "gulp",
"watch": "gulp"
},
"devDependencies": {
"babelify": "^7.3.0",
"browserify": "^14.3.0",
"envify": "^4.0.0",
"gulp": "3.9.1",
"gulp-autoprefixer": "3.1.1",
"gulp-clean-css": "^3.0.4",
"gulp-minify-css": "1.2.4",
"gulp-plumber": "1.1.0",
"gulp-sass": "3.1.0",
"gulp-uglify": "2.1.2",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.9.0",
"yargs": "^7.1.0"
},
"dependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.15.0",
"angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1",
"babel-preset-es2015": "^6.24.1",
"clipboard": "^1.5.16",
"dropzone": "^4.0.1",
"gulp": "^3.9.0",
"laravel-elixir": "^6.0.0-11",
"laravel-elixir-browserify-official": "^0.1.3",
"marked": "^0.3.5",
"moment": "^2.12.0"
"gulp-util": "^3.0.8",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"moment": "^2.12.0",
"vue": "^2.2.6"
},
"dependencies": {
"clipboard": "^1.5.16"
"browser": {
"vue": "vue/dist/vue.common.js"
}
}
......
......@@ -74,7 +74,7 @@ These are the great projects used to help build BookStack:
* [Dropzone.js](http://www.dropzonejs.com/)
* [ZeroClipboard](http://zeroclipboard.org/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [Marked](https://github.com/chjj/marked)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
......
"use strict";
import moment from 'moment';
import 'moment/locale/en-gb';
import editorOptions from "./pages/page-form";
const moment = require('moment');
require('moment/locale/en-gb');
const editorOptions = require("./pages/page-form");
moment.locale('en-gb');
export default function (ngApp, events) {
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
......@@ -259,39 +259,6 @@ export default function (ngApp, events) {
}]);
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
$scope.searching = false;
$scope.searchTerm = '';
$scope.searchResults = '';
$scope.searchBook = function (e) {
e.preventDefault();
let term = $scope.searchTerm;
if (term.length == 0) return;
$scope.searching = true;
$scope.searchResults = '';
let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
searchUrl += '?term=' + encodeURIComponent(term);
$http.get(searchUrl).then((response) => {
$scope.searchResults = $sce.trustAsHtml(response.data);
});
};
$scope.checkSearchForm = function () {
if ($scope.searchTerm.length < 1) {
$scope.searching = false;
}
};
$scope.clearSearch = function () {
$scope.searching = false;
$scope.searchTerm = '';
};
}]);
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
......
"use strict";
import DropZone from "dropzone";
import markdown from "marked";
const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
export default function (ngApp, events) {
module.exports = function (ngApp, events) {
/**
* Common tab controls using simple jQuery functions.
......@@ -214,18 +215,8 @@ export default function (ngApp, events) {
}
}]);
let renderer = new markdown.Renderer();
// Custom markdown checkbox list item
// Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001
renderer.listitem = function(text) {
if (/^\s*\[[x ]\]\s*/.test(text)) {
text = text
.replace(/^\s*\[ \]\s*/, '<input type="checkbox"/>')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" checked/>');
return `<li class="checkbox-item">${text}</li>`;
}
return `<li>${text}</li>`;
};
const md = new MarkdownIt();
md.use(mdTasksLists, {label: true});
/**
* Markdown input
......@@ -244,20 +235,20 @@ export default function (ngApp, events) {
element = element.find('textarea').first();
let content = element.val();
scope.mdModel = content;
scope.mdChange(markdown(content, {renderer: renderer}));
scope.mdChange(md.render(content));
element.on('change input', (event) => {
content = element.val();
$timeout(() => {
scope.mdModel = content;
scope.mdChange(markdown(content, {renderer: renderer}));
scope.mdChange(md.render(content));
});
});
scope.$on('markdown-update', (event, value) => {
element.val(value);
scope.mdModel = value;
scope.mdChange(markdown(value));
scope.mdChange(md.render(value));
});
}
......
"use strict";
// AngularJS - Create application and load components
import angular from "angular";
import "angular-resource";
import "angular-animate";
import "angular-sanitize";
import "angular-ui-sortable";
// Url retrieval function
window.baseUrl = function(path) {
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
......@@ -15,11 +8,33 @@ window.baseUrl = function(path) {
return basePath + '/' + path;
};
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
Vue.prototype.$http = axiosInstance;
require("./vues/vues");
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
import Translations from "./translations"
const Translations = require("./translations");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
......@@ -47,11 +62,12 @@ class EventManager {
}
window.Events = new EventManager();
Vue.prototype.$events = window.Events;
// Load in angular specific items
import Services from './services';
import Directives from './directives';
import Controllers from './controllers';
const Services = require('./services');
const Directives = require('./directives');
const Controllers = require('./controllers');
Services(ngApp, window.Events);
Directives(ngApp, window.Events);
Controllers(ngApp, window.Events);
......@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1
}
// Page specific items
import "./pages/page-show";
require("./pages/page-show");
......
......@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) {
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
}
export default function() {
module.exports = function() {
let settings = {
selector: '#html-editor',
content_css: [
......@@ -68,6 +68,7 @@ export default function() {
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
......@@ -213,4 +214,4 @@ export default function() {
}
};
return settings;
}
\ No newline at end of file
};
\ No newline at end of file
......
"use strict";
// Configure ZeroClipboard
import Clipboard from "clipboard";
const Clipboard = require("clipboard");
export default window.setupPageShow = function (pageId) {
let setupPageShow = window.setupPageShow = function (pageId) {
// Set up pointer
let $pointer = $('#pointer').detach();
......@@ -81,6 +81,12 @@ export default window.setupPageShow = function (pageId) {
let $idElem = $(idElem);
let color = $('#custom-styles').attr('data-color-light');
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
setTimeout(() => {
$idElem.addClass('anim').addClass('selectFade').css('background-color', '');
setTimeout(() => {
$idElem.removeClass('selectFade');
}, 3000);
}, 100);
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
......@@ -151,3 +157,5 @@ export default window.setupPageShow = function (pageId) {
});
};
module.exports = setupPageShow;
\ No newline at end of file
......
......@@ -44,4 +44,4 @@ class Translator {
}
export default Translator
module.exports = Translator;
......
let data = {
id: null,
type: '',
searching: false,
searchTerm: '',
searchResults: '',
};
let computed = {
};
let methods = {
searchBook() {
if (this.searchTerm.trim().length === 0) return;
this.searching = true;
this.searchResults = '';
let url = window.baseUrl(`/search/${this.type}/${this.id}`);
url += `?term=${encodeURIComponent(this.searchTerm)}`;
this.$http.get(url).then(resp => {
this.searchResults = resp.data;
});
},
checkSearchForm() {
this.searching = this.searchTerm > 0;
},
clearSearch() {
this.searching = false;
this.searchTerm = '';
}
};
function mounted() {
this.id = Number(this.$el.getAttribute('entity-id'));
this.type = this.$el.getAttribute('entity-type');
}
module.exports = {
data, computed, methods, mounted
};
\ No newline at end of file
const moment = require('moment');
let data = {
terms: '',
termString : '',
search: {
type: {
page: true,
chapter: true,
book: true
},
exactTerms: [],
tagTerms: [],
option: {},
dates: {
updated_after: false,
updated_before: false,
created_after: false,
created_before: false,
}
}
};
let computed = {
};
let methods = {
appendTerm(term) {
this.termString += ' ' + term;
this.termString = this.termString.replace(/\s{2,}/g, ' ');
this.termString = this.termString.replace(/^\s+/, '');
this.termString = this.termString.replace(/\s+$/, '');
},
exactParse(searchString) {
this.search.exactTerms = [];
let exactFilter = /"(.+?)"/g;
let matches;
while ((matches = exactFilter.exec(searchString)) !== null) {
this.search.exactTerms.push(matches[1]);
}
},
exactChange() {
let exactFilter = /"(.+?)"/g;
this.termString = this.termString.replace(exactFilter, '');
let matchesTerm = this.search.exactTerms.filter(term => {
return term.trim() !== '';
}).map(term => {
return `"${term}"`
}).join(' ');
this.appendTerm(matchesTerm);
},
addExact() {
this.search.exactTerms.push('');
setTimeout(() => {
let exactInputs = document.querySelectorAll('.exact-input');
exactInputs[exactInputs.length - 1].focus();
}, 100);
},
removeExact(index) {
this.search.exactTerms.splice(index, 1);
this.exactChange();
},
tagParse(searchString) {
this.search.tagTerms = [];
let tagFilter = /\[(.+?)\]/g;
let matches;
while ((matches = tagFilter.exec(searchString)) !== null) {
this.search.tagTerms.push(matches[1]);
}
},
tagChange() {
let tagFilter = /\[(.+?)\]/g;
this.termString = this.termString.replace(tagFilter, '');
let matchesTerm = this.search.tagTerms.filter(term => {
return term.trim() !== '';
}).map(term => {
return `[${term}]`
}).join(' ');
this.appendTerm(matchesTerm);
},
addTag() {
this.search.tagTerms.push('');
setTimeout(() => {
let tagInputs = document.querySelectorAll('.tag-input');
tagInputs[tagInputs.length - 1].focus();
}, 100);
},
removeTag(index) {
this.search.tagTerms.splice(index, 1);
this.tagChange();
},
typeParse(searchString) {
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
let match = searchString.match(typeFilter);
let type = this.search.type;
if (!match) {
type.page = type.book = type.chapter = true;
return;
}
let splitTypes = match[1].replace(/ /g, '').split('|');
type.page = (splitTypes.indexOf('page') !== -1);
type.chapter = (splitTypes.indexOf('chapter') !== -1);
type.book = (splitTypes.indexOf('book') !== -1);
},
typeChange() {
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
let type = this.search.type;
if (type.page === type.chapter && type.page === type.book) {
this.termString = this.termString.replace(typeFilter, '');
return;
}
let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|');
let typeTerm = '{type:'+selectedTypes+'}';
if (this.termString.match(typeFilter)) {
this.termString = this.termString.replace(typeFilter, typeTerm);
return;
}
this.appendTerm(typeTerm);
},
optionParse(searchString) {
let optionFilter = /{([a-z_\-:]+?)}/gi;
let matches;
while ((matches = optionFilter.exec(searchString)) !== null) {
this.search.option[matches[1].toLowerCase()] = true;
}
},
optionChange(optionName) {
let isChecked = this.search.option[optionName];
if (isChecked) {
this.appendTerm(`{${optionName}}`);
} else {
this.termString = this.termString.replace(`{${optionName}}`, '');
}
},
updateSearch(e) {
e.preventDefault();
window.location = '/search?term=' + encodeURIComponent(this.termString);
},
enableDate(optionName) {
this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD');
this.dateChange(optionName);
},
dateParse(searchString) {
let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi;
let dateTags = Object.keys(this.search.dates);
let matches;
while ((matches = dateFilter.exec(searchString)) !== null) {
if (dateTags.indexOf(matches[1]) === -1) continue;
this.search.dates[matches[1].toLowerCase()] = matches[2];
}
},
dateChange(optionName) {
let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi');
this.termString = this.termString.replace(dateFilter, '');
if (!this.search.dates[optionName]) return;
this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`);
},
dateRemove(optionName) {
this.search.dates[optionName] = false;
this.dateChange(optionName);
}
};
function created() {
this.termString = document.querySelector('[name=searchTerm]').value;
this.typeParse(this.termString);
this.exactParse(this.termString);
this.tagParse(this.termString);
this.optionParse(this.termString);
this.dateParse(this.termString);
}
module.exports = {
data, computed, methods, created
};
\ No newline at end of file
const Vue = require("vue");
function exists(id) {
return document.getElementById(id) !== null;
}
let vueMapping = {
'search-system': require('./search'),
'entity-dashboard': require('./entity-search'),
};
Object.keys(vueMapping).forEach(id => {
if (exists(id)) {
let config = vueMapping[id];
config.el = '#' + id;
new Vue(config);
}
});
\ No newline at end of file
......@@ -2,7 +2,7 @@
.anim.fadeIn {
opacity: 0;
animation-name: fadeIn;
animation-duration: 160ms;
animation-duration: 180ms;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
......@@ -127,3 +127,7 @@
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
.anim.selectFade {
transition: background-color ease-in-out 3000ms;
}
\ No newline at end of file
......
......@@ -98,19 +98,36 @@ label {
label.radio, label.checkbox {
font-weight: 400;
user-select: none;
input[type="radio"], input[type="checkbox"] {
margin-right: $-xs;
}
}
label.inline.checkbox {
margin-right: $-m;
}
label + p.small {
margin-bottom: 0.8em;
}
input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
table.form-table {
max-width: 100%;
td {
overflow: hidden;
padding: $-xxs/2 0;
}
}
input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
@extend .input-base;
}
input[type=date] {
width: 190px;
}
.toggle-switch {
display: inline-block;
background-color: #BBB;
......
......@@ -109,6 +109,7 @@
transition-property: right, border;
border-left: 0px solid #FFF;
background-color: #FFF;
max-width: 320px;
&.fixed {
background-color: #FFF;
z-index: 5;
......
......@@ -269,19 +269,31 @@ span.highlight {
/*
* Lists
*/
ul, ol {
overflow: hidden;
p {
margin: 0;
}
}
ul {
padding-left: $-m * 1.3;
list-style: disc;
overflow: hidden;
ul {
list-style: circle;
margin-top: 0;
margin-bottom: 0;
}
label {
margin: 0;
}
}
ol {
list-style: decimal;
padding-left: $-m * 2;
overflow: hidden;
}
li.checkbox-item {
li.checkbox-item, li.task-list-item {
list-style: none;
margin-left: - ($-m * 1.3);
input[type="checkbox"] {
......
......@@ -7,8 +7,8 @@
@import "grid";
@import "blocks";
@import "buttons";
@import "forms";
@import "tables";
@import "forms";
@import "animations";
@import "tinymce";
@import "highlightjs";
......@@ -17,7 +17,11 @@
@import "lists";
@import "pages";
[v-cloak], [v-show] {display: none;}
[v-cloak], [v-show] {
display: none; opacity: 0;
animation-name: none !important;
}
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
......@@ -272,8 +276,3 @@ $btt-size: 40px;
......
......@@ -43,18 +43,9 @@ return [
* Search
*/
'search_results' => 'Suchergebnisse',
'search_results_page' => 'Seiten-Suchergebnisse',
'search_results_chapter' => 'Kapitel-Suchergebnisse',
'search_results_book' => 'Buch-Suchergebnisse',
'search_clear' => 'Suche zur&uuml;cksetzen',
'search_view_pages' => 'Zeige alle passenden Seiten',
'search_view_chapters' => 'Zeige alle passenden Kapitel',
'search_view_books' => 'Zeige alle passenden B&uuml;cher',
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
'search_for_term' => 'Suche nach :term',
'search_page_for_term' => 'Suche nach :term in Seiten',
'search_chapter_for_term' => 'Suche nach :term in Kapiteln',
'search_book_for_term' => 'Suche nach :term in B&uuml;chern',
/**
* Books
......
......@@ -33,6 +33,7 @@ return [
'search_clear' => 'Clear Search',
'reset' => 'Reset',
'remove' => 'Remove',
'add' => 'Add',
/**
......
......@@ -14,6 +14,7 @@ return [
'recent_activity' => 'Recent Activity',
'create_now' => 'Create one now',
'revisions' => 'Revisions',
'meta_revision' => 'Revision #:revisionCount',
'meta_created' => 'Created :timeLength',
'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength',
......@@ -43,18 +44,26 @@ return [
* Search
*/
'search_results' => 'Search Results',
'search_results_page' => 'Page Search Results',
'search_results_chapter' => 'Chapter Search Results',
'search_results_book' => 'Book Search Results',
'search_total_results_found' => ':count result found|:count total results found',
'search_clear' => 'Clear Search',
'search_view_pages' => 'View all matches pages',
'search_view_chapters' => 'View all matches chapters',
'search_view_books' => 'View all matches books',
'search_no_pages' => 'No pages matched this search',
'search_for_term' => 'Search for :term',
'search_page_for_term' => 'Page search for :term',
'search_chapter_for_term' => 'Chapter search for :term',
'search_book_for_term' => 'Books search for :term',
'search_more' => 'More Results',
'search_filters' => 'Search Filters',
'search_content_type' => 'Content Type',
'search_exact_matches' => 'Exact Matches',
'search_tags' => 'Tag Searches',
'search_viewed_by_me' => 'Viewed by me',
'search_not_viewed_by_me' => 'Not viewed by me',
'search_permissions_set' => 'Permissions set',
'search_created_by_me' => 'Created by me',
'search_updated_by_me' => 'Updated by me',
'search_updated_before' => 'Updated before',
'search_updated_after' => 'Updated after',
'search_created_before' => 'Created before',
'search_created_after' => 'Created after',
'search_set_date' => 'Set Date',
'search_update' => 'Update Search',
/**
* Books
......@@ -112,6 +121,7 @@ return [
'chapters_empty' => 'No pages are currently in this chapter.',
'chapters_permissions_active' => 'Chapter Permissions Active',
'chapters_permissions_success' => 'Chapter Permissions Updated',
'chapters_search_this' => 'Search this chapter',
/**
* Pages
......@@ -159,6 +169,7 @@ return [
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
......
......@@ -120,6 +120,7 @@ return [
'fr' => 'Français',
'nl' => 'Nederlands',
'pt_BR' => 'Português do Brasil',
'sk' => 'Slovensky',
]
///////////////////////////////////
];
......
......@@ -11,7 +11,7 @@ return [
|
*/
'failed' => 'Las credenciales no concuerdan con nuestros registros.',
'throttle' => 'Demasiados intentos fallidos de conexiÃn. Por favor intente nuevamente en :seconds segundos.',
'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
/**
* Login & Register
......
......@@ -18,7 +18,7 @@ return [
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas',
'image_preview' => 'Preview de la imagen',
'image_upload_success' => 'Imagen subida exitosamente',
'image_upload_success' => 'Imagen subida éxitosamente',
'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
'image_delete_success' => 'Imagen borrada exitosamente'
];
......
......@@ -4,7 +4,7 @@ return [
/**
* Shared
*/
'recently_created' => 'Recientemente creadod',
'recently_created' => 'Recientemente creado',
'recently_created_pages' => 'Páginas recientemente creadas',
'recently_updated_pages' => 'Páginas recientemente actualizadas',
'recently_created_chapters' => 'Capítulos recientemente creados',
......@@ -43,18 +43,9 @@ return [
* Search
*/
'search_results' => 'Buscar resultados',
'search_results_page' => 'resultados de búsqueda en página',
'search_results_chapter' => 'Resultados de búsqueda en capítulo ',
'search_results_book' => 'Resultados de búsqueda en libro',
'search_clear' => 'Limpiar resultados',
'search_view_pages' => 'Ver todas las páginas que concuerdan',
'search_view_chapters' => 'Ver todos los capítulos que concuerdan',
'search_view_books' => 'Ver todos los libros que concuerdan',
'search_no_pages' => 'Ninguna página encontrada para la búsqueda',
'search_for_term' => 'Busqueda por :term',
'search_page_for_term' => 'Búsqueda de página por :term',
'search_chapter_for_term' => 'Búsqueda por capítulo de :term',
'search_book_for_term' => 'Búsqueda en libro de :term',
/**
* Books
......@@ -148,79 +139,79 @@ return [
'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Insertar Imagen',
'pages_md_insert_link' => 'Insert Entity Link',
'pages_not_in_chapter' => 'Page is not in a chapter',
'pages_move' => 'Move Page',
'pages_move_success' => 'Page moved to ":parentName"',
'pages_permissions' => 'Page Permissions',
'pages_permissions_success' => 'Page permissions updated',
'pages_revisions' => 'Page Revisions',
'pages_revisions_named' => 'Page Revisions for :pageName',
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_md_insert_link' => 'Insertar link de entidad',
'pages_not_in_chapter' => 'La página no esá en el caítulo',
'pages_move' => 'Mover página',
'pages_move_success' => 'Página movida a ":parentName"',
'pages_permissions' => 'Permisos de página',
'pages_permissions_success' => 'Permisos de página actualizados',
'pages_revisions' => 'Revisiones de página',
'pages_revisions_named' => 'Revisiones de página para :pageName',
'pages_revision_named' => 'Revisión de ágina para :pageName',
'pages_revisions_created_by' => 'Creado por',
'pages_revisions_date' => 'Fecha de revisión',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
'pages_revisions_changes' => 'Cambios',
'pages_revisions_current' => 'Versión actual',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restore',
'pages_revisions_none' => 'This page has no revisions',
'pages_copy_link' => 'Copy Link',
'pages_permissions_active' => 'Page Permissions Active',
'pages_initial_revision' => 'Initial publish',
'pages_initial_name' => 'New Page',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Esta página no tiene revisiones',
'pages_copy_link' => 'Copiar Link',
'pages_permissions_active' => 'Permisos de página activos',
'pages_initial_revision' => 'Publicación inicial',
'pages_initial_name' => 'Página nueva',
'pages_editing_draft_notification' => 'Ud. está actualmente editando un borrador que fue guardado porúltima vez el :timeDiff.',
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',
'time_a' => 'since the pages was last updated',
'time_b' => 'in the last :minCount minutes',
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
'start_a' => ':count usuarios han comenzado a editar esta página',
'start_b' => ':userName ha comenzado a editar esta página',
'time_a' => 'desde que las página fue actualizada',
'time_b' => 'en los Ãltimos :minCount minutos',
'message' => ':start :time. Ten cuidado de no sobreescribir los cambios del otro usuario',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
/**
* Editor sidebar
*/
'page_tags' => 'Page Tags',
'tag' => 'Tag',
'tags' => '',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
'tags_add' => 'Add another tag',
'attachments' => 'Attachments',
'attachments_explain' => 'Upload some files or attach some link to display on your page. These are visible in the page sidebar.',
'attachments_explain_instant_save' => 'Changes here are saved instantly.',
'attachments_items' => 'Attached Items',
'attachments_upload' => 'Upload File',
'attachments_link' => 'Attach Link',
'attachments_set_link' => 'Set Link',
'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.',
'attachments_dropzone' => 'Drop files or click here to attach a file',
'attachments_no_files' => 'No files have been uploaded',
'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',
'attachments_link_name' => 'Link Name',
'attachment_link' => 'Attachment link',
'attachments_link_url' => 'Link to file',
'attachments_link_url_hint' => 'Url of site or file',
'attach' => 'Attach',
'attachments_edit_file' => 'Edit File',
'attachments_edit_file_name' => 'File Name',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
'attachments_order_updated' => 'Attachment order updated',
'attachments_updated_success' => 'Attachment details updated',
'attachments_deleted' => 'Attachment deleted',
'attachments_file_uploaded' => 'File successfully uploaded',
'attachments_file_updated' => 'File successfully updated',
'attachments_link_attached' => 'Link successfully attached to page',
'page_tags' => 'Etiquetas de página',
'tag' => 'Etiqueta',
'tags' => 'Etiquetas',
'tag_value' => 'Valor de la etiqueta (Opcional)',
'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Ud. puede asignar un valor a una etiqueta para una organizacón a mayor detalle.",
'tags_add' => 'Agregar otra etiqueta',
'attachments' => 'Adjuntos',
'attachments_explain' => 'Subir ficheros o agregar links para mostrar en la página. Estos son visibles en la barra lateral de la página.',
'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
'attachments_items' => 'Items adjuntados',
'attachments_upload' => 'Fichero adjuntado',
'attachments_link' => 'Adjuntar Link',
'attachments_set_link' => 'Setear Link',
'attachments_delete_confirm' => 'Haga click en borrar nuevamente para confirmar que quiere borrar este adjunto.',
'attachments_dropzone' => 'Arrastre ficheros aquío haga click aquípara adjuntar un fichero',
'attachments_no_files' => 'NingÃn fichero ha sido adjuntado',
'attachments_explain_link' => 'Ud. puede agregar un link o si lo prefiere puede agregar un fichero. Esto puede ser un link a otra página o un link a un fichero en la nube.',
'attachments_link_name' => 'Nombre de Link',
'attachment_link' => 'Link adjunto',
'attachments_link_url' => 'Link a fichero',
'attachments_link_url_hint' => 'Url del sitio o fichero',
'attach' => 'Adjuntar',
'attachments_edit_file' => 'Editar fichero',
'attachments_edit_file_name' => 'Nombre del fichero',
'attachments_edit_drop_upload' => 'Arrastre a los ficheros o haga click aquípara subir o sobreescribir',
'attachments_order_updated' => 'Orden de adjuntos actualizado',
'attachments_updated_success' => 'Detalles de adjuntos actualizados',
'attachments_deleted' => 'Adjunto borrado',
'attachments_file_uploaded' => 'Fichero subido éxitosamente',
'attachments_file_updated' => 'Fichero actualizado éxitosamente',
'attachments_link_attached' => 'Link agregado éxitosamente a la ágina',
/**
* Profile View
*/
'profile_user_for_x' => 'User for :time',
'profile_created_content' => 'Created Content',
'profile_not_created_pages' => ':userName has not created any pages',
'profile_not_created_chapters' => ':userName has not created any chapters',
'profile_not_created_books' => ':userName has not created any books',
'profile_user_for_x' => 'Usuario para :time',
'profile_created_content' => 'Contenido creado',
'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName no ha creado ningún libro',
];
......
......@@ -7,64 +7,64 @@ return [
*/
// Permissions
'permission' => 'You do not have permission to access the requested page.',
'permissionJson' => 'You do not have permission to perform the requested action.',
'permission' => 'Ud. no tiene permisos para visualizar la página solicitada.',
'permissionJson' => 'Ud. no tiene permisos para ejecutar la acción solicitada.',
// Auth
'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'No action defined',
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
'social_account_existing' => 'This :socialAccount is already attached to your profile.',
'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
'error_user_exists_different_creds' => 'Un usuario con el email :email ya existe pero con credenciales diferentes.',
'email_already_confirmed' => 'El email ya ha sido confirmado, Intente loguearse en la aplicación.',
'email_confirmation_invalid' => 'Este token de confirmación no e válido o ya ha sido usado,Intente registrar uno nuevamente.',
'email_confirmation_expired' => 'El token de confirmación ha expirado, Un nuevo email de confirmacón ha sido enviado.',
'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',
'ldap_fail_authed' => 'El acceso LDAP usando el dn & password detallados',
'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',
'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',
'social_no_action_defined' => 'Acción no definida',
'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',
'social_account_email_in_use' => 'El email :email ya se encuentra en uso. Si ud. ya dispone de una cuenta puede loguearse a través de su cuenta :socialAccount desde la configuración de perfil.',
'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',
'social_account_already_used_existing' => 'La cuenta :socialAccount ya se encuentra usada por otro usuario.',
'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjuntela a su configuración de perfil. ',
'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
'social_driver_not_found' => 'Driver social no encontrado',
'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Cannot get image from :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'image_upload_error' => 'An error occurred uploading the image',
'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',
'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
'attachment_page_mismatch' => 'Página no coincidente durante la subida del adjunto ',
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',
// Entities
'entity_not_found' => 'Entity not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
'selected_book_not_found' => 'The selected book was not found',
'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
'guests_cannot_save_drafts' => 'Guests cannot save drafts',
'entity_not_found' => 'Entidad no encontrada',
'book_not_found' => 'Libro no encontrado',
'page_not_found' => 'Página no encontrada',
'chapter_not_found' => 'Capítulo no encontrado',
'selected_book_not_found' => 'El libro seleccionado no fue encontrado',
'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',
'guests_cannot_save_drafts' => 'Los invitados no pueden guardar los borradores',
// Users
'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
'users_cannot_delete_guest' => 'You cannot delete the guest user',
'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',
'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',
// Roles
'role_cannot_be_edited' => 'This role cannot be edited',
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
'role_cannot_be_edited' => 'Este rol no puede ser editado',
'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',
'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de registro',
// Error pages
'404_page_not_found' => 'Page Not Found',
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
'return_home' => 'Return to home',
'error_occurred' => 'An Error Occurred',
'app_down' => ':appName is down right now',
'back_soon' => 'It will be back up soon.',
'404_page_not_found' => 'Página no encontrada',
'sorry_page_not_found' => 'Lo sentimos, la página que intenta acceder no pudo ser encontrada.',
'return_home' => 'Volver al home',
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'Volverá a estar operativa en corto tiempo.',
];
......
......@@ -13,67 +13,67 @@ return [
|
*/
'accepted' => 'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'accepted' => 'El :attribute debe ser aceptado.',
'active_url' => 'El :attribute no es una URl válida.',
'after' => 'El :attribute debe ser una fecha posterior :date.',
'alpha' => 'El :attribute solo puede contener letras.',
'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
'alpha_num' => 'El :attribute solo puede contener letras y número.',
'array' => 'El :attribute debe de ser un array.',
'before' => 'El :attribute debe ser una fecha anterior a :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
'numeric' => 'El :attribute debe estar entre :min y :max.',
'file' => 'El :attribute debe estar entre :min y :max kilobytes.',
'string' => 'El :attribute debe estar entre :min y :max carácteres.',
'array' => 'El :attribute debe estar entre :min y :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'boolean' => 'El campo :attribute debe ser true o false.',
'confirmed' => 'La confirmación de :attribute no concuerda.',
'date' => 'El :attribute no es una fecha válida.',
'date_format' => 'El :attribute no coincide con el formato :format.',
'different' => ':attribute y :other deben ser diferentes.',
'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => ':attribute debe ser una dirección álida.',
'filled' => 'El campo :attribute es requerido.',
'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'El :attribute debe ser una imagen.',
'in' => 'El selected :attribute es inválio.',
'integer' => 'El :attribute debe ser un entero.',
'ip' => 'El :attribute debe ser una dirección IP álida.',
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
'numeric' => ':attribute no puede ser mayor que :max.',
'file' => ':attribute no puede ser mayor que :max kilobytes.',
'string' => ':attribute no puede ser mayor que :max carácteres.',
'array' => ':attribute no puede contener más de :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimes' => ':attribute debe ser un fichero de tipo: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
'numeric' => ':attribute debe ser al menos de :min.',
'file' => ':attribute debe ser al menos :min kilobytes.',
'string' => ':attribute debe ser al menos :min caracteres.',
'array' => ':attribute debe tener como mínimo :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'not_in' => ':attribute seleccionado es inválio.',
'numeric' => ':attribute debe ser numérico.',
'regex' => ':attribute con formato inválido',
'required' => ':attribute es requerido.',
'required_if' => ':attribute es requerido cuando :other vale :value.',
'required_with' => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',
'required_with_all' => 'El campo :attribute es requerido cuando los valores sean :values.',
'required_without' => ':attribute es requerido cuando no se encuentre entre los valores :values.',
'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
'same' => ':attribute y :other deben coincidir.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
'numeric' => ':attribute debe ser :size.',
'file' => ':attribute debe ser :size kilobytes.',
'string' => ':attribute debe ser :size caracteres.',
'array' => ':attribute debe contener :size items.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',
'string' => 'El atributo :attribute debe ser una cadena.',
'timezone' => 'El atributo :attribute debe ser una zona válida.',
'unique' => 'El atributo :attribute ya ha sido tomado.',
'url' => 'El atributo :attribute tiene un formato inválid.',
/*
|--------------------------------------------------------------------------
......@@ -88,7 +88,7 @@ return [
'custom' => [
'password-confirm' => [
'required_with' => 'Password confirmation required',
'required_with' => 'Confirmación de Password requerida',
],
],
......
......@@ -43,18 +43,9 @@ return [
* Search
*/
'search_results' => 'Résultats de recherche',
'search_results_page' => 'Résultats de recherche des pages',
'search_results_chapter' => 'Résultats de recherche des chapitres',
'search_results_book' => 'Résultats de recherche des livres',
'search_clear' => 'Réinitialiser la recherche',
'search_view_pages' => 'Voir toutes les pages correspondantes',
'search_view_chapters' => 'Voir tous les chapitres correspondants',
'search_view_books' => 'Voir tous les livres correspondants',
'search_no_pages' => 'Aucune page correspondant à cette recherche',
'search_for_term' => 'recherche pour :term',
'search_page_for_term' => 'Recherche de page pour :term',
'search_chapter_for_term' => 'Recherche de chapitre pour :term',
'search_book_for_term' => 'Recherche de livres pour :term',
/**
* Books
......
......@@ -43,18 +43,9 @@ return [
* Search
*/
'search_results' => 'Zoekresultaten',
'search_results_page' => 'Pagina Zoekresultaten',
'search_results_chapter' => 'Hoofdstuk Zoekresultaten',
'search_results_book' => 'Boek Zoekresultaten',
'search_clear' => 'Zoekopdracht wissen',
'search_view_pages' => 'Bekijk alle gevonden pagina\'s',
'search_view_chapters' => 'Bekijk alle gevonden hoofdstukken',
'search_view_books' => 'Bekijk alle gevonden boeken',
'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
'search_for_term' => 'Zoeken op :term',
'search_page_for_term' => 'Pagina doorzoeken op :term',
'search_chapter_for_term' => 'Hoofdstuk doorzoeken op :term',
'search_book_for_term' => 'Boeken doorzoeken op :term',
/**
* Books
......
......@@ -43,18 +43,9 @@ return [
* Search
*/
'search_results' => 'Resultado(s) da Pesquisa',
'search_results_page' => 'Resultado(s) de Pesquisa de Página',
'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo',
'search_results_book' => 'Resultado(s) de Pesquisa de Livro',
'search_clear' => 'Limpar Pesquisa',
'search_view_pages' => 'Visualizar todas as páginas correspondentes',
'search_view_chapters' => 'Visualizar todos os capítulos correspondentes',
'search_view_books' => 'Visualizar todos os livros correspondentes',
'search_no_pages' => 'Nenhuma página corresponde à pesquisa',
'search_for_term' => 'Pesquisar por :term',
'search_page_for_term' => 'Pesquisar Página por :term',
'search_chapter_for_term' => 'Pesquisar Capítulo por :term',
'search_book_for_term' => 'Pesquisar Livros por :term',
/**
* Books
......
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'vytvoril stránku',
'page_create_notification' => 'Stránka úspešne vytvorená',
'page_update' => 'aktualizoval stránku',
'page_update_notification' => 'Stránka úspešne aktualizovaná',
'page_delete' => 'odstránil stránku',
'page_delete_notification' => 'Stránka úspešne odstránená',
'page_restore' => 'obnovil stránku',
'page_restore_notification' => 'Stránka úspešne obnovená',
'page_move' => 'presunul stránku',
// Chapters
'chapter_create' => 'vytvoril kapitolu',
'chapter_create_notification' => 'Kapitola úspešne vytvorená',
'chapter_update' => 'aktualizoval kapitolu',
'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',
'chapter_delete' => 'odstránil kapitolu',
'chapter_delete_notification' => 'Kapitola úspešne odstránená',
'chapter_move' => 'presunul kapitolu',
// Books
'book_create' => 'vytvoril knihu',
'book_create_notification' => 'Kniha úspešne vytvorená',
'book_update' => 'aktualizoval knihu',
'book_update_notification' => 'Kniha úspešne aktualizovaná',
'book_delete' => 'odstránil knihu',
'book_delete_notification' => 'Kniha úspešne odstránená',
'book_sort' => 'zoradil knihu',
'book_sort_notification' => 'Kniha úspešne znovu zoradená',
];
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Tieto údaje nesedia s našimi záznamami.',
'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.',
/**
* Login & Register
*/
'sign_up' => 'Registrácia',
'log_in' => 'Prihlásenie',
'log_in_with' => 'Prihlásiť sa cez :socialDriver',
'sign_up_with' => 'Registrovať sa cez :socialDriver',
'logout' => 'Odhlásenie',
'name' => 'Meno',
'username' => 'Používateľské meno',
'email' => 'Email',
'password' => 'Heslo',
'password_confirm' => 'Potvrdiť heslo',
'password_hint' => 'Musí mať viac ako 5 znakov',
'forgot_password' => 'Zabudli ste heslo?',
'remember_me' => 'Zapamätať si ma',
'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
'create_account' => 'Vytvoriť účet',
'social_login' => 'Sociálne prihlásenie',
'social_registration' => 'Sociálna registrácia',
'social_registration_text' => 'Registrovať sa a prihlásiť sa použitím inej služby.',
'register_thanks' => 'Ďakujeme zaregistráciu!',
'register_confirm' => 'Skontrolujte prosím svoj email a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',
'registrations_disabled' => 'Registrácie sú momentálne zablokované',
'registration_email_domain_invalid' => 'Táto emailová doména nemá prístup k tejto aplikácii',
'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.',
/**
* Password Reset
*/
'reset_password' => 'Reset hesla',
'reset_password_send_instructions' => 'Zadajte svoj email nižšie a bude Vám odoslaný email s odkazom pre reset hesla.',
'reset_password_send_button' => 'Poslať odkaz na reset hesla',
'reset_password_sent_success' => 'Odkaz na reset hesla bol poslaný na :email.',
'reset_password_success' => 'Vaše heslo bolo úspešne resetované.',
'email_reset_subject' => 'Reset Vášho :appName hesla',
'email_reset_text' => 'Tento email Ste dostali pretože sme dostali požiadavku na reset hesla pre Váš účet.',
'email_reset_not_requested' => 'Ak ste nepožiadali o reset hesla, nemusíte nič robiť.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Potvrdiť email na :appName',
'email_confirm_greeting' => 'Ďakujeme za pridanie sa k :appName!',
'email_confirm_text' => 'Prosím potvrďte Vašu emailovú adresu kliknutím na tlačidlo nižšie:',
'email_confirm_action' => 'Potvrdiť email',
'email_confirm_send_error' => 'Je požadované overenie emailu, ale systém nemohol odoslať email. Kontaktujte administrátora by ste sa uistili, že email je nastavený správne.',
'email_confirm_success' => 'Váš email bol overený!',
'email_confirm_resent' => 'Potvrdzujúci email bol poslaný znovu, skontrolujte prosím svoju emailovú schránku.',
'email_not_confirmed' => 'Emailová adresa nebola overená',
'email_not_confirmed_text' => 'Vaša emailová adresa nebola zatiaľ overená.',
'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v emaili, ktorý bol poslaný krátko po Vašej registrácii.',
'email_not_confirmed_resend' => 'Ak nemôžete násť email, môžete znova odoslať overovací email odoslaním doleuvedeného formulára.',
'email_not_confirmed_resend_button' => 'Znova odoslať overovací email',
];
<?php
return [
/**
* Buttons
*/
'cancel' => 'Zrušiť',
'confirm' => 'Potvrdiť',
'back' => 'Späť',
'save' => 'Uložiť',
'continue' => 'Pokračovať',
'select' => 'Vybrať',
/**
* Form Labels
*/
'name' => 'Meno',
'description' => 'Popis',
'role' => 'Rola',
/**
* Actions
*/
'actions' => 'Akcie',
'view' => 'Zobraziť',
'create' => 'Vytvoriť',
'update' => 'Aktualizovať',
'edit' => 'Editovať',
'sort' => 'Zoradiť',
'move' => 'Presunúť',
'delete' => 'Zmazať',
'search' => 'Hľadť',
'search_clear' => 'Vyčistiť hľadanie',
'reset' => 'Reset',
'remove' => 'Odstrániť',
/**
* Misc
*/
'deleted_user' => 'Odstránený používateľ',
'no_activity' => 'Žiadna aktivita na zobrazenie',
'no_items' => 'Žiadne položky nie sú dostupné',
'back_to_top' => 'Späť nahor',
'toggle_details' => 'Prepnúť detaily',
/**
* Header
*/
'view_profile' => 'Zobraziť profil',
'edit_profile' => 'Upraviť profil',
/**
* Email Content
*/
'email_action_help' => 'Ak máte problém klinkúť na tlačidlo ":actionText", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:',
'email_rights' => 'Všetky práva vyhradené',
];
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Vybrať obrázok',
'image_all' => 'Všetko',
'image_all_title' => 'Zobraziť všetky obrázky',
'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy',
'image_page_title' => 'Zobraziť obrázky nahrané do tejto stránky',
'image_search_hint' => 'Hľadať obrázok podľa názvu',
'image_uploaded' => 'Nahrané :uploadedDate',
'image_load_more' => 'Načítať viac',
'image_image_name' => 'Názov obrázka',
'image_delete_confirm' => 'Tento obrázok je použitý na stránkach uvedených nižšie, kliknite znova na zmazať pre potvrdenie zmazania tohto obrázka.',
'image_select_image' => 'Vybrať obrázok',
'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',
'images_deleted' => 'Obrázky zmazané',
'image_preview' => 'Náhľad obrázka',
'image_upload_success' => 'Obrázok úspešne nahraný',
'image_update_success' => 'Detaily obrázka úspešne aktualizované',
'image_delete_success' => 'Obrázok úspešne zmazaný'
];
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'Nemáte oprávnenie pre prístup k požadovanej stránke.',
'permissionJson' => 'Nemáte oprávnenie pre vykonanie požadovaného úkonu.',
// Auth
'error_user_exists_different_creds' => 'Používateľ s emailom :email už existuje, ale s inými údajmi.',
'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.',
'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.',
'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.',
'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'Nebola definovaná žiadna akcia',
'social_account_in_use' => 'Tento :socialAccount účet sa už používa, skúste sa prihlásiť pomocou možnosti :socialAccount.',
'social_account_email_in_use' => 'Email :email sa už používa. Ak už máte účet, môžete pripojiť svoj :socialAccount účet v nastaveniach profilu.',
'social_account_existing' => 'Tento :socialAccount účet je už spojený s Vaším profilom.',
'social_account_already_used_existing' => 'Tento :socialAccount účet už používa iný používateľ.',
'social_account_not_used' => 'Tento :socialAccount účet nie je spojený so žiadnym používateľom. Pripojte ho prosím v nastaveniach Vášho profilu. ',
'social_account_register_instructions' => 'Ak zatiaľ nemáte účet, môžete sa registrovať pomocou možnosti :socialAccount.',
'social_driver_not_found' => 'Ovládač socialnych sietí nebol nájdený',
'social_driver_not_configured' => 'Nastavenia Vášho :socialAccount účtu nie sú správne.',
// System
'path_not_writable' => 'Do cesty :filePath sa nedá nahrávať. Uistite sa, že je zapisovateľná serverom.',
'cannot_get_image_from_url' => 'Nedá sa získať obrázok z :url',
'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.',
'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',
'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
// Pages
'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky',
// Entities
'entity_not_found' => 'Entita nenájdená',
'book_not_found' => 'Kniha nenájdená',
'page_not_found' => 'Stránka nenájdená',
'chapter_not_found' => 'Kapitola nenájdená',
'selected_book_not_found' => 'Vybraná kniha nebola nájdená',
'selected_book_chapter_not_found' => 'Vybraná kniha alebo kapitola nebola nájdená',
'guests_cannot_save_drafts' => 'Hosť nemôže ukladať koncepty',
// Users
'users_cannot_delete_only_admin' => 'Nemôžete zmazať posledného správcu',
'users_cannot_delete_guest' => 'Nemôžete zmazať hosťa',
// Roles
'role_cannot_be_edited' => 'Táto rola nemôže byť upravovaná',
'role_system_cannot_be_deleted' => 'Táto rola je systémová rola a nemôže byť zmazaná',
'role_registration_default_cannot_delete' => 'Táto rola nemôže byť zmazaná, pretože je nastavená ako prednastavená rola pri registrácii',
// Error pages
'404_page_not_found' => 'Stránka nenájdená',
'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.',
'return_home' => 'Vrátiť sa domov',
'error_occurred' => 'Nastala chyba',
'app_down' => ':appName je momentálne nedostupná',
'back_soon' => 'Čoskoro bude opäť dostupná.',
];
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Predchádzajúca',
'next' => 'Ďalšia &raquo;',
];
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Heslo musí obsahovať aspoň šesť znakov a musí byť rovnaké ako potvrdzujúce.',
'user' => "Nenašli sme používateľa s takou emailovou adresou.",
'token' => 'Tento token pre reset hesla je neplatný.',
'sent' => 'Poslali sme Vám email s odkazom na reset hesla!',
'reset' => 'Vaše heslo bolo resetované!',
];
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Nastavenia',
'settings_save' => 'Uložiť nastavenia',
'settings_save_success' => 'Nastavenia uložené',
/**
* App settings
*/
'app_settings' => 'Nastavenia aplikácie',
'app_name' => 'Názov aplikácia',
'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.',
'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?',
'app_public_viewing' => 'Povoliť verejné zobrazenie?',
'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',
'app_secure_images_desc' => 'Kvôli výkonu sú všetky obrázky verejné. Táto možnosť pridá pred URL obrázka náhodný, ťažko uhádnuteľný reťazec. Aby ste zabránili jednoduchému prístupu, uistite sa, že indexy priečinkov nie sú povolené.',
'app_editor' => 'Editor stránky',
'app_editor_desc' => 'Vyberte editor, ktorý bude používaný všetkými používateľmi na editáciu stránok.',
'app_custom_html' => 'Vlastný HTML obsah hlavičky',
'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok <head> sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.',
'app_logo' => 'Logo aplikácie',
'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku. <br>Veľké obrázky budú preškálované na menší rozmer.',
'app_primary_color' => 'Primárna farba pre aplikáciu',
'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare. <br>Nechajte prázdne ak chcete použiť prednastavenú farbu.',
/**
* Registration settings
*/
'reg_settings' => 'Nastavenia registrácie',
'reg_allow' => 'Povoliť registráciu?',
'reg_default_role' => 'Prednastavená používateľská rola po registrácii',
'reg_confirm_email' => 'Vyžadovať overenie emailu?',
'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.',
'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu',
'reg_confirm_restrict_domain_desc' => 'Zadajte zoznam domén, pre ktoré chcete povoliť registráciu oddelených čiarkou. Používatelia dostanú email kvôli overeniu adresy predtým ako im bude dovolené používať aplikáciu. <br> Používatelia si budú môcť po úspešnej registrácii zmeniť svoju emailovú adresu.',
'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia',
/**
* Role settings
*/
'roles' => 'Roly',
'role_user_roles' => 'Používateľské roly',
'role_create' => 'Vytvoriť novú rolu',
'role_create_success' => 'Rola úspešne vytvorená',
'role_delete' => 'Zmazať rolu',
'role_delete_confirm' => 'Toto zmaže rolu menom \':roleName\'.',
'role_delete_users_assigned' => 'Túto rolu má priradenú :userCount používateľov. Ak chcete premigrovať používateľov z tejto roly, vyberte novú rolu nižšie.',
'role_delete_no_migration' => "Nemigrovať používateľov",
'role_delete_sure' => 'Ste si istý, že chcete zmazať túto rolu?',
'role_delete_success' => 'Rola úspešne zmazaná',
'role_edit' => 'Upraviť rolu',
'role_details' => 'Detaily roly',
'role_name' => 'Názov roly',
'role_desc' => 'Krátky popis roly',
'role_system' => 'Systémové oprávnenia',
'role_manage_users' => 'Spravovať používateľov',
'role_manage_roles' => 'Spravovať role a oprávnenia rolí',
'role_manage_entity_permissions' => 'Spravovať všetky oprávnenia kníh, kapitol a stránok',
'role_manage_own_entity_permissions' => 'Spravovať oprávnenia vlastných kníh, kapitol a stránok',
'role_manage_settings' => 'Spravovať nastavenia aplikácie',
'role_asset' => 'Oprávnenia majetku',
'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.',
'role_all' => 'Všetko',
'role_own' => 'Vlastné',
'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané',
'role_save' => 'Uložiť rolu',
'role_update_success' => 'Roly úspešne aktualizované',
'role_users' => 'Používatelia s touto rolou',
'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu',
/**
* Users
*/
'users' => 'Používatelia',
'user_profile' => 'Profil používateľa',
'users_add_new' => 'Pridať nového používateľa',
'users_search' => 'Hľadať medzi používateľmi',
'users_role' => 'Používateľské roly',
'users_external_auth_id' => 'Externé autentifikačné ID',
'users_password_warning' => 'Pole nižšie vyplňte iba ak chcete zmeniť heslo:',
'users_system_public' => 'Tento účet reprezentuje každého hosťovského používateľa, ktorý navštívi Vašu inštanciu. Nedá sa pomocou neho prihlásiť a je priradený automaticky.',
'users_delete' => 'Zmazať používateľa',
'users_delete_named' => 'Zmazať používateľa :userName',
'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.',
'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?',
'users_delete_success' => 'Používateľ úspešne zmazaný',
'users_edit' => 'Upraviť používateľa',
'users_edit_profile' => 'Upraviť profil',
'users_edit_success' => 'Používateľ úspešne upravený',
'users_avatar' => 'Avatar používateľa',
'users_avatar_desc' => 'Tento obrázok by mal byť štvorec s rozmerom približne 256px.',
'users_preferred_language' => 'Preferovaný jazyk',
'users_social_accounts' => 'Sociálne účty',
'users_social_accounts_info' => 'Tu si môžete pripojiť iné účty pre rýchlejšie a jednoduchšie prihlásenie. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Pripojiť účet',
'users_social_disconnect' => 'Odpojiť účet',
'users_social_connected' => ':socialAccount účet bol úspešne pripojený k Vášmu profilu.',
'users_social_disconnected' => ':socialAccount účet bol úspešne odpojený od Vášho profilu.',
];
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute musí byť akceptovaný.',
'active_url' => ':attribute nie je platná URL.',
'after' => ':attribute musí byť dátum po :date.',
'alpha' => ':attribute môže obsahovať iba písmená.',
'alpha_dash' => ':attribute môže obsahovať iba písmená, čísla a pomlčky.',
'alpha_num' => ':attribute môže obsahovať iba písmená a čísla.',
'array' => ':attribute musí byť pole.',
'before' => ':attribute musí byť dátum pred :date.',
'between' => [
'numeric' => ':attribute musí byť medzi :min a :max.',
'file' => ':attribute musí byť medzi :min a :max kilobajtmi.',
'string' => ':attribute musí byť medzi :min a :max znakmi.',
'array' => ':attribute musí byť medzi :min a :max položkami.',
],
'boolean' => ':attribute pole musí byť true alebo false.',
'confirmed' => ':attribute potvrdenie nesedí.',
'date' => ':attribute nie je platný dátum.',
'date_format' => ':attribute nesedí s formátom :format.',
'different' => ':attribute a :other musia byť rozdielne.',
'digits' => ':attribute musí mať :digits číslic.',
'digits_between' => ':attribute musí mať medzi :min a :max číslicami.',
'email' => ':attribute musí byť platná emailová adresa.',
'filled' => 'Políčko :attribute je povinné.',
'exists' => 'Vybraný :attribute nie je platný.',
'image' => ':attribute musí byť obrázok.',
'in' => 'Vybraný :attribute je neplatný.',
'integer' => ':attribute musí byť celé číslo.',
'ip' => ':attribute musí byť platná IP adresa.',
'max' => [
'numeric' => ':attribute nesmie byť väčší ako :max.',
'file' => ':attribute nesmie byť väčší ako :max kilobajtov.',
'string' => ':attribute nesmie byť dlhší ako :max znakov.',
'array' => ':attribute nesmie mať viac ako :max položiek.',
],
'mimes' => ':attribute musí byť súbor typu: :values.',
'min' => [
'numeric' => ':attribute musí byť aspoň :min.',
'file' => ':attribute musí mať aspoň :min kilobajtov.',
'string' => ':attribute musí mať aspoň :min znakov.',
'array' => ':attribute musí mať aspoň :min položiek.',
],
'not_in' => 'Vybraný :attribute je neplatný.',
'numeric' => ':attribute musí byť číslo.',
'regex' => ':attribute formát je neplatný.',
'required' => 'Políčko :attribute je povinné.',
'required_if' => 'Políčko :attribute je povinné ak :other je :value.',
'required_with' => 'Políčko :attribute je povinné ak :values existuje.',
'required_with_all' => 'Políčko :attribute je povinné ak :values existuje.',
'required_without' => 'Políčko :attribute je povinné aj :values neexistuje.',
'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.',
'same' => ':attribute a :other musia byť rovnaké.',
'size' => [
'numeric' => ':attribute musí byť :size.',
'file' => ':attribute musí mať :size kilobajtov.',
'string' => ':attribute musí mať :size znakov.',
'array' => ':attribute musí obsahovať :size položiek.',
],
'string' => ':attribute musí byť reťazec.',
'timezone' => ':attribute musí byť plantá časová zóna.',
'unique' => ':attribute je už použité.',
'url' => ':attribute formát je neplatný.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'Vyžaduje sa potvrdenie hesla',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];
......@@ -47,7 +47,7 @@
</a>
</div>
<div class="col-lg-4 col-sm-3 text-center">
<form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box">
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
<input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
<button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
</form>
......
......@@ -50,15 +50,15 @@
</div>
<div class="container" id="book-dashboard" ng-controller="BookShowController" book-id="{{ $book->id }}">
<div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
<div class="row">
<div class="col-md-7">
<h1>{{$book->name}}</h1>
<div class="book-content" ng-show="!searching">
<p class="text-muted" ng-non-bindable>{{$book->description}}</p>
<div class="book-content" v-if="!searching">
<p class="text-muted" v-pre>{{$book->description}}</p>
<div class="page-list" ng-non-bindable>
<div class="page-list" v-pre>
<hr>
@if(count($bookChildren) > 0)
@foreach($bookChildren as $childElement)
......@@ -81,12 +81,12 @@
@include('partials.entity-meta', ['entity' => $book])
</div>
</div>
<div class="search-results" ng-cloak ng-show="searching">
<h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div ng-if="!searchResults">
<div class="search-results" v-cloak v-if="searching">
<h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div v-if="!searchResults">
@include('partials/loading-icon')
</div>
<div ng-bind-html="searchResults"></div>
<div v-html="searchResults"></div>
</div>
......@@ -94,6 +94,7 @@
<div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book))
......@@ -103,14 +104,16 @@
@endif
</p>
@endif
<div class="search-box">
<form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
<form v-on:submit="searchBook">
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button>
<button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
</form>
</div>
<div class="activity anim fadeIn">
<div class="activity">
<h3>{{ trans('entities.recent_activity') }}</h3>
@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
</div>
......
<div class="breadcrumbs">
@if (userCan('view', $chapter->book))
<a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
@endif
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>
\ No newline at end of file
......
......@@ -47,10 +47,11 @@
</div>
<div class="container" ng-non-bindable>
<div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
<div class="row">
<div class="col-md-8">
<div class="col-md-7">
<h1>{{ $chapter->name }}</h1>
<div class="chapter-content" v-if="!searching">
<p class="text-muted">{{ $chapter->description }}</p>
@if(count($pages) > 0)
......@@ -80,7 +81,16 @@
@include('partials.entity-meta', ['entity' => $chapter])
</div>
<div class="col-md-3 col-md-offset-1">
<div class="search-results" v-cloak v-if="searching">
<h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div v-if="!searchResults">
@include('partials/loading-icon')
</div>
<div v-html="searchResults"></div>
</div>
</div>
<div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted || $chapter->restricted)
<div class="text-muted">
......@@ -105,7 +115,16 @@
</div>
@endif
<div class="search-box">
<form v-on:submit="searchBook">
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button>
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
</form>
</div>
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div>
</div>
</div>
......
<div class="breadcrumbs">
@if (userCan('view', $page->book))
<a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
@if($page->hasChapter())
<span class="sep">&raquo;</span>
@endif
@if($page->hasChapter() && userCan('view', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i>
{{ $page->chapter->getShortName() }}
</a>
@endif
<span class="sep">&raquo;</span>
@endif
<a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
\ No newline at end of file
......
......@@ -19,6 +19,7 @@
<table class="table">
<tr>
<th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
<th width="23%">{{ trans('entities.pages_name') }}</th>
<th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
<th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
......@@ -27,6 +28,7 @@
</tr>
@foreach($page->revisions as $index => $revision)
<tr>
<td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
<td>{{ $revision->name }}</td>
<td style="line-height: 0;">
@if($revision->createdBy)
......
......@@ -3,13 +3,13 @@
@if(isset($page) && $page->tags->count() > 0)
<div class="tag-display">
<h6 class="text-muted">Page Tags</h6>
<h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
<table>
<tbody>
@foreach($page->tags as $tag)
<tr class="tag">
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
</tr>
@endforeach
</tbody>
......@@ -39,8 +39,10 @@
<h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6>
<ul class="sidebar-page-list menu">
<li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
@if (userCan('view', $book))
<li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
@endif
@foreach($sidebarTree as $bookChild)
<li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
......
<p class="text-muted small">
@if ($entity->isA('page')) {{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @endif
@if ($entity->createdBy)
{!! trans('entities.meta_created_name', ['timeLength' => $entity->created_at->diffForHumans(), 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"]) !!}
{!! trans('entities.meta_created_name', [
'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"
]) !!}
@else
{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}
<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
@endif
<br>
@if ($entity->updatedBy)
{!! trans('entities.meta_updated_name', ['timeLength' => $entity->updated_at->diffForHumans(), 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"]) !!}
{!! trans('entities.meta_updated_name', [
'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"
]) !!}
@else
{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}
<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
@endif
</p>
\ No newline at end of file
......
......@@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/link/{id}', 'PageController@redirectFromLink');
// Search
Route::get('/search/all', 'SearchController@searchAll');
Route::get('/search/pages', 'SearchController@searchPages');
Route::get('/search/books', 'SearchController@searchBooks');
Route::get('/search/chapters', 'SearchController@searchChapters');
Route::get('/search', 'SearchController@search');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
// Other Pages
Route::get('/', 'HomeController@index');
......
......@@ -22,6 +22,12 @@ abstract class BrowserKitTest extends TestCase
private $admin;
private $editor;
public function tearDown()
{
\DB::disconnect();
parent::tearDown();
}
/**
* Creates the application.
*
......
<?php namespace Tests;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
class EntityTest extends BrowserKitTest
{
......@@ -18,7 +24,7 @@ class EntityTest extends BrowserKitTest
$this->bookDelete($book);
}
public function bookDelete(\BookStack\Book $book)
public function bookDelete(Book $book)
{
$this->asAdmin()
->visit($book->getUrl())
......@@ -32,7 +38,7 @@ class EntityTest extends BrowserKitTest
->notSeeInDatabase('books', ['id' => $book->id]);
}
public function bookUpdate(\BookStack\Book $book)
public function bookUpdate(Book $book)
{
$newName = $book->name . ' Updated';
$this->asAdmin()
......@@ -46,12 +52,12 @@ class EntityTest extends BrowserKitTest
->seePageIs($book->getUrl() . '-updated')
->see($newName);
return \BookStack\Book::find($book->id);
return Book::find($book->id);
}
public function test_book_sort_page_shows()
{
$books = \BookStack\Book::all();
$books = Book::all();
$bookToSort = $books[0];
$this->asAdmin()
->visit($bookToSort->getUrl())
......@@ -65,7 +71,7 @@ class EntityTest extends BrowserKitTest
public function test_book_sort_item_returns_book_content()
{
$books = \BookStack\Book::all();
$books = Book::all();
$bookToSort = $books[0];
$firstPage = $bookToSort->pages[0];
$firstChapter = $bookToSort->chapters[0];
......@@ -79,7 +85,7 @@ class EntityTest extends BrowserKitTest
public function pageCreation($chapter)
{
$page = factory(\BookStack\Page::class)->make([
$page = factory(Page::class)->make([
'name' => 'My First Page'
]);
......@@ -88,7 +94,7 @@ class EntityTest extends BrowserKitTest
->visit($chapter->getUrl())
->click('New Page');
$draftPage = \BookStack\Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
$draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
$this->seePageIs($draftPage->getUrl())
// Fill out form
......@@ -99,13 +105,13 @@ class EntityTest extends BrowserKitTest
->seePageIs($chapter->book->getUrl() . '/page/my-first-page')
->see($page->name);
$page = \BookStack\Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
$page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
return $page;
}
public function chapterCreation(\BookStack\Book $book)
public function chapterCreation(Book $book)
{
$chapter = factory(\BookStack\Chapter::class)->make([
$chapter = factory(Chapter::class)->make([
'name' => 'My First Chapter'
]);
......@@ -122,13 +128,13 @@ class EntityTest extends BrowserKitTest
->seePageIs($book->getUrl() . '/chapter/my-first-chapter')
->see($chapter->name)->see($chapter->description);
$chapter = \BookStack\Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
$chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
return $chapter;
}
public function bookCreation()
{
$book = factory(\BookStack\Book::class)->make([
$book = factory(Book::class)->make([
'name' => 'My First Book'
]);
$this->asAdmin()
......@@ -154,7 +160,7 @@ class EntityTest extends BrowserKitTest
$expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
$this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
$book = \BookStack\Book::where('slug', '=', 'my-first-book')->first();
$book = Book::where('slug', '=', 'my-first-book')->first();
return $book;
}
......@@ -165,8 +171,8 @@ class EntityTest extends BrowserKitTest
$updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($creator);
app('BookStack\Repos\UserRepo')->destroy($creator);
app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']);
app(UserRepo::class)->destroy($creator);
app(EntityRepo::class)->savePageRevision($entities['page']);
$this->checkEntitiesViewable($entities);
}
......@@ -178,8 +184,8 @@ class EntityTest extends BrowserKitTest
$updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($updater);
app('BookStack\Repos\UserRepo')->destroy($updater);
app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']);
app(UserRepo::class)->destroy($updater);
app(EntityRepo::class)->savePageRevision($entities['page']);
$this->checkEntitiesViewable($entities);
}
......@@ -216,7 +222,7 @@ class EntityTest extends BrowserKitTest
public function test_old_page_slugs_redirect_to_new_pages()
{
$page = \BookStack\Page::first();
$page = Page::first();
$pageUrl = $page->getUrl();
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
// Need to save twice since revisions are not generated in seeder.
......@@ -225,7 +231,7 @@ class EntityTest extends BrowserKitTest
->type('super test', '#name')
->press('Save Page');
$page = \BookStack\Page::first();
$page = Page::first();
$pageUrl = $page->getUrl();
// Second Save
......@@ -242,7 +248,7 @@ class EntityTest extends BrowserKitTest
public function test_recently_updated_pages_on_home()
{
$page = \BookStack\Page::orderBy('updated_at', 'asc')->first();
$page = Page::orderBy('updated_at', 'asc')->first();
$this->asAdmin()->visit('/')
->dontSeeInElement('#recently-updated-pages', $page->name);
$this->visit($page->getUrl() . '/edit')
......
<?php namespace Entity;
use BookStack\Page;
use Tests\TestCase;
class PageRevisionTest extends TestCase
{
public function test_page_revision_count_increments_on_update()
{
$page = Page::first();
$startCount = $page->revision_count;
$resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$resp->assertStatus(302);
$this->assertTrue(Page::find($page->id)->revision_count === $startCount+1);
}
public function test_revision_count_shown_in_page_meta()
{
$page = Page::first();
$this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$page = Page::find($page->id);
$pageView = $this->get($page->getUrl());
$pageView->assertSee('Revision #' . $page->revision_count);
}
}
\ No newline at end of file
......@@ -522,4 +522,21 @@ class RestrictionsTest extends BrowserKitTest
->see('Delete Chapter');
}
public function test_page_visible_if_has_permissions_when_book_not_visible()
{
$book = \BookStack\Book::first();
$bookChapter = $book->chapters->first();
$bookPage = $bookChapter->pages->first();
$this->setEntityRestrictions($book, []);
$this->setEntityRestrictions($bookPage, ['view']);
$this->actingAs($this->viewer);
$this->get($bookPage->getUrl());
$this->assertResponseOk();
$this->see($bookPage->name);
$this->dontSee(substr($book->name, 0, 15));
$this->dontSee(substr($bookChapter->name, 0, 15));
}
}
......
......@@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase
public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
}
/**
* Create and return a new test page
* @param array $input
* @return Chapter
*/
public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
$book = Book::first();
$entityRepo = $this->app[EntityRepo::class];
$draftPage = $entityRepo->getDraftPage($book);
return $entityRepo->publishPageDraft($draftPage, $input);
}
}
\ No newline at end of file
......
v0.15.3
v0.16
......