Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/tests/ export-ignore
/.github/ export-ignore
/.husky/ export-ignore
/workbench/ export-ignore
/testbench.yaml export-ignore

# Identify generated files
/resources/assets/*/dist/** linguist-generated=true
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
extensions: gd, pdo_sqlite
tools: composer:v2
- run: composer install --no-interaction --no-ansi --no-progress
- run: php workbench/prepare.php
- run: composer phpstan
notify-slack:
name: Notify Slack
Expand Down
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ tests/_craft/*
vendor/*
resources/assets/**/dist
public/build
workbench/bootstrap/cache
workbench/config/craft
workbench/public/vendor
workbench/public/uploads
workbench/storage
workbench/vendor
.amp/in
.amp/portals
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,22 @@ If you wish to use CKEditor on the front end, the safest option is to [bring you
```

You may need to build up the top-level config object from `field.options`, `field.headingLevels`, and so on to get a complete, functional editor—be mindful that configurations may rely on plugins that are only available in the control panel! Refer to [`CraftCms\Ckeditor\Field::uiControl()`](src/Field.php) for additional steps the CKEditor plugin when normalizing configuration, for control panel editor instances.

## Development workbench

The workbench runs a persistent Craft 6 application with SQLite. It seeds an Articles section with plain text, CKEditor, nested Quote entries, and Photo entries backed by a local image volume.

With PHP 8.5, Composer, and Node installed, run:

```sh
composer install
npm ci
composer workbench:setup
composer serve
```

Open `http://localhost:8125/admin` on the machine running the server. Sign in as `admin` with the generated `WORKBENCH_ADMIN_PASSWORD` in `workbench/.env`. The workbench does not bypass authentication. Do not use it for production data.

`composer workbench:setup` publishes the installed CMS assets and the plugin's committed bundles. After changing frontend source, run `npm exec npm-run-all -- -p 'build:*'` and rerun setup to publish the rebuilt bundles.

The SQLite database, uploads, generated configuration, and credentials are ignored by Git. Repeated setup preserves existing content. Tests use a separate application directory and do not reset the workbench database.
14 changes: 12 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,24 @@
},
"autoload-dev": {
"psr-4": {
"CraftCms\\Ckeditor\\Tests\\": "tests/"
"CraftCms\\Ckeditor\\Tests\\": "tests/",
"Workbench\\App\\": "workbench/app/",
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
}
},
"scripts": {
"check-cs": "pint --test",
"fix-cs": "pint",
"phpstan": "phpstan --memory-limit=1G",
"tests": "./vendor/bin/pest --compact"
"tests": "./vendor/bin/pest --compact",
"workbench:setup": [
"@php workbench/prepare.php",
"@php vendor/bin/testbench workbench:build --no-interaction"
],
"serve": [
"Composer\\Config::disableProcessTimeout",
"@php vendor/bin/testbench serve --host=127.0.0.1 --port=8125"
]
},
"extra": {
"name": "CKEditor",
Expand Down
22 changes: 22 additions & 0 deletions testbench.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
laravel: workbench

providers:
- Workbench\App\Providers\WorkbenchServiceProvider
- Inertia\ServiceProvider
- CraftCms\Aliases\AliasesServiceProvider
- CraftCms\DependencyAwareCache\CacheServiceProvider
- CraftCms\Cms\Providers\CraftServiceProvider

workbench:
start: /admin
install: false
build:
- vendor-publish:
--tag: craftcms-assets
--force: true
- db-seed:
--class: Workbench\Database\Seeders\DatabaseSeeder
--force: true
discovers:
web: true
config: true
17 changes: 17 additions & 0 deletions workbench/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
APP_NAME="CKEditor Workbench"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8125

DB_CONNECTION=sqlite
CACHE_STORE=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
MAIL_MAILER=log

WORKBENCH_ADMIN_EMAIL=admin@example.test
WORKBENCH_ADMIN_USERNAME=admin
WORKBENCH_ADMIN_PASSWORD=

CRAFT_EDITION=pro
13 changes: 13 additions & 0 deletions workbench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.env
!.env.example
/vendor
/bootstrap/cache
/database/*.sqlite*
/config/craft/project
/config/craft/license.key
/public/vendor
/public/assets
/storage
/cms
/public/cpresources
/public/uploads
30 changes: 30 additions & 0 deletions workbench/app/Providers/WorkbenchServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Workbench\App\Providers;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;

class WorkbenchServiceProvider extends ServiceProvider
{
public function register(): void
{
// Testbench creates a console request even for its web server. Plugins need the real path at boot.
if (! $this->app->runningInConsole()) {
$this->app->instance('request', Request::capture());
}

$files = new Filesystem;
$root = dirname(__DIR__, levels: 3);
$composer = json_decode($files->get($root.'/composer.json'), associative: true, flags: JSON_THROW_ON_ERROR);
$path = $this->app->basePath('vendor/craftcms/plugins.php');
$plugins = $files->exists($path) ? (require $path) : [];
$plugins[$composer['name']] = [...$composer['extra'], 'version' => '6.0.0', 'basePath' => $root.'/src'];

$files->ensureDirectoryExists(dirname($path));
$files->put($path, '<?php return '.var_export($plugins, return: true).';');
}
}
31 changes: 31 additions & 0 deletions workbench/bootstrap/app.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

use Dotenv\Dotenv;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\ApplicationBuilder;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Env;

use function Orchestra\Testbench\default_skeleton_path;

// Feature tests clean project config and runtime files in their application directory.
$basePath = Env::get('APP_ENV') === 'testing' ? default_skeleton_path() : dirname(__DIR__);

if ($basePath === dirname(__DIR__)) {
// Testbench caches framework defaults while preparing its vendor symlink.
Dotenv::create(Env::getRepository(), $basePath)->safeLoad();
}

$app = new Application($basePath);
// Keep Testbench's kernels so Laravel does not bootstrap the configuration a second time.
$app->singleton(Kernel::class, Orchestra\Testbench\Console\Kernel::class);
$app->singleton(Illuminate\Contracts\Http\Kernel::class, Orchestra\Testbench\Http\Kernel::class);

return new ApplicationBuilder($app)
->withProviders()
->withMiddleware(static fn (Middleware $middleware) => $middleware->trustProxies(at: ['127.0.0.1', '::1']))
->withCommands()
->create();
9 changes: 9 additions & 0 deletions workbench/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "craftcms/ckeditor-workbench",
"type": "project",
"autoload": {
"psr-4": {
"Workbench\\App\\": "app/"
}
}
}
14 changes: 14 additions & 0 deletions workbench/config/filesystems.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

return [
'disks' => [
'images' => [
'driver' => 'local',
'root' => public_path('uploads'),
'url' => rtrim(env('APP_URL', 'http://localhost:8125'), '/').'/uploads',
'visibility' => 'public',
],
],
];
9 changes: 9 additions & 0 deletions workbench/config/workbench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

return [
'admin_email' => env('WORKBENCH_ADMIN_EMAIL', default: 'admin@example.test'),
'admin_username' => env('WORKBENCH_ADMIN_USERNAME', default: 'admin'),
'admin_password' => env('WORKBENCH_ADMIN_PASSWORD'),
];
59 changes: 59 additions & 0 deletions workbench/database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Workbench\Database\Seeders;

use CraftCms\Cms\Cms;
use CraftCms\Cms\Support\Facades\Plugins;
use CraftCms\Cms\Support\Facades\Sites;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Context;
use RuntimeException;

class DatabaseSeeder extends Seeder
{
public function run(): void
{
if (! Cms::isInstalled()) {
$status = $this->command->call('craft:install', [
'--email' => config('workbench.admin_email'),
'--username' => config('workbench.admin_username'),
'--password' => config('workbench.admin_password'),
'--siteName' => 'CKEditor Workbench',
'--siteUrl' => config('app.url'),
'--language' => 'en-US',
'--timezone' => 'UTC',
'--no-interaction' => true,
]);

if ($status !== 0) {
throw new RuntimeException('Craft installation failed.');
}

Context::forgetHidden('craft.info');
Context::forgetHidden('craft.isInstalled');
}

$site = Sites::getPrimarySite();

if ($site->getBaseUrl(false) !== '$APP_URL') {
$site->baseUrl = '$APP_URL';

if (! Sites::saveSite($site)) {
throw new RuntimeException('Failed to update the Workbench site URL.');
}
}

if (! Plugins::isPluginInstalled('ckeditor')) {
Plugins::installPlugin('ckeditor');
}

if (! Plugins::isPluginEnabled('ckeditor')) {
Plugins::enablePlugin('ckeditor');
}

Plugins::createPlugin('ckeditor')->publishFrontendAssets();
$this->call(EditorSeeder::class);
}
}
97 changes: 97 additions & 0 deletions workbench/database/seeders/EditorSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace Workbench\Database\Seeders;

use CraftCms\Ckeditor\Field;
use CraftCms\Cms\Asset\Data\Volume;
use CraftCms\Cms\Entry\Data\EntryType;
use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Field\Assets;
use CraftCms\Cms\Field\PlainText;
use CraftCms\Cms\FieldLayout\FieldLayout;
use CraftCms\Cms\FieldLayout\LayoutElements\CustomField;
use CraftCms\Cms\FieldLayout\LayoutElements\Entries\EntryTitleField;
use CraftCms\Cms\Section\Data\Section;
use CraftCms\Cms\Section\Data\SectionSiteSettings;
use CraftCms\Cms\Section\Enums\SectionType;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\EntryTypes;
use CraftCms\Cms\Support\Facades\Fields;
use CraftCms\Cms\Support\Facades\Sections;
use CraftCms\Cms\Support\Facades\Sites;
use CraftCms\Cms\Support\Facades\Volumes;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
use RuntimeException;

class EditorSeeder extends Seeder
{
public function run(): void
{
if (Sections::getSectionByHandle('articles')) {
return;
}

$save = static function (bool $saved): void {
if (! $saved) {
throw new RuntimeException('Could not save editor fixture.');
}
};
$volume = new Volume(['name' => 'Images', 'handle' => 'images', 'fsHandle' => 'images', 'hasUrls' => true]);
$save(Volumes::saveVolume($volume));

$plain = Fields::createField(['type' => PlainText::class, 'name' => 'Summary', 'handle' => 'summary']);
$image = Fields::createField(['type' => Assets::class, 'name' => 'Image', 'handle' => 'image', 'sources' => ['volume:'.$volume->uid], 'defaultUploadLocationSource' => 'volume:'.$volume->uid]);
$save(Fields::saveField($plain));
$save(Fields::saveField($image));

$type = static function (string $name, string $handle, array $fields) use ($save): EntryType {
$type = new EntryType(['name' => $name, 'handle' => $handle]);
$type->setFieldLayout(FieldLayout::createFromConfig([
'type' => Entry::class,
'tabs' => [['name' => 'Content', 'elements' => [
['type' => EntryTitleField::class, 'uid' => (string) Str::uuid()],
...array_map(static fn ($field) => ['type' => CustomField::class, 'uid' => (string) Str::uuid(), 'fieldUid' => $field->uid], $fields),
]]],
]));
$save(EntryTypes::saveEntryType($type));

return EntryTypes::getEntryTypeById($type->id);
};
$quote = $type('Quote', 'quote', [$plain]);
$photo = $type('Photo', 'photo', [$image, $plain]);
$body = Fields::createField([
'type' => Field::class, 'name' => 'Body', 'handle' => 'body',
'toolbar' => ['heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'insertImage', 'createEntry', 'sourceEditing', 'undo', 'redo'],
'entryTypes' => [['uid' => $quote->uid]],
'defaultUploadLocationVolume' => $volume->uid,
]);
$save(Fields::saveField($body));
$gallery = Fields::createField([
'type' => Field::class, 'name' => 'Photo story', 'handle' => 'photoStory',
'toolbar' => ['bold', 'insertImage', 'createEntry', 'undo', 'redo'],
'imageMode' => 'entries',
'entryTypes' => [['uid' => $photo->uid]],
'imageEntryTypeUid' => $photo->uid,
'imageFieldUid' => $photo->getFieldLayout()->getCustomFieldElements()[0]->uid,
'defaultUploadLocationVolume' => $volume->uid,
]);
$save(Fields::saveField($gallery));
$article = $type('Article', 'article', [$plain, $body, $gallery]);
$section = new Section([
'name' => 'Articles', 'handle' => 'articles', 'type' => SectionType::Channel,
'entryTypes' => [$article->id],
'siteSettings' => [new SectionSiteSettings(['siteId' => Sites::getPrimarySite()->id])],
]);
$save(Sections::saveSection($section));
$entry = new Entry(['sectionId' => $section->id, 'typeId' => $article->id, 'title' => 'Autumn in Brussels']);
$entry->setFieldValues([
'summary' => 'A weekend walk through the city.',
'body' => '<h2>A walk through Brussels</h2><p>The autumn market opens on Saturday.</p>',
'photoStory' => '<p>Photographs from the walk.</p>',
]);
$save(Elements::saveElement($entry));
}
}
Loading
Loading