diff --git a/framework/core/js/src/admin/components/ExtensionPage.js b/framework/core/js/src/admin/components/ExtensionPage.js
index c4c43c342..de06ea888 100644
--- a/framework/core/js/src/admin/components/ExtensionPage.js
+++ b/framework/core/js/src/admin/components/ExtensionPage.js
@@ -11,6 +11,7 @@ import LoadingModal from './LoadingModal';
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import AdminPage from './AdminPage';
+import ReadmeModal from './ReadmeModal';
export default class ExtensionPage extends AdminPage {
oninit(vnode) {
@@ -196,6 +197,22 @@ export default class ExtensionPage extends AdminPage {
}
});
+ const extension = this.extension;
+ items.add(
+ 'readme',
+ Button.component(
+ {
+ icon: 'fab fa-readme',
+ class: 'Readme-link',
+ onclick() {
+ app.modal.show(ReadmeModal, { extension });
+ },
+ },
+ app.translator.trans('core.admin.extension.readme.button_label')
+ ),
+ 10
+ );
+
return items;
}
diff --git a/framework/core/js/src/admin/components/ReadmeModal.js b/framework/core/js/src/admin/components/ReadmeModal.js
new file mode 100644
index 000000000..100e8e2b3
--- /dev/null
+++ b/framework/core/js/src/admin/components/ReadmeModal.js
@@ -0,0 +1,50 @@
+import app from '../../admin/app';
+import Modal from '../../common/components/Modal';
+import LoadingIndicator from '../../common/components/LoadingIndicator';
+import Placeholder from '../../common/components/Placeholder';
+import ExtensionReadme from '../models/ExtensionReadme';
+
+export default class ReadmeModal extends Modal {
+ oninit(vnode) {
+ super.oninit(vnode);
+
+ app.store.models['extension-readmes'] = ExtensionReadme;
+
+ this.name = this.attrs.extension.id;
+ this.extName = this.attrs.extension.extra['flarum-extension'].title;
+
+ this.loading = true;
+
+ this.loadReadme();
+ }
+
+ className() {
+ return 'ReadmeModal Modal--large';
+ }
+
+ title() {
+ return app.translator.trans('core.admin.extension.readme.title', {
+ extName: this.extName,
+ });
+ }
+
+ content() {
+ const text = app.translator.trans('core.admin.extension.readme.no_readme');
+
+ return (
+
+ {this.loading ? (
+
{LoadingIndicator.component()}
+ ) : (
+
{this.readme.content() ? m.trust(this.readme.content()) : Placeholder.component({ text })}
+ )}
+
+ );
+ }
+
+ async loadReadme() {
+ this.readme = await app.store.find('extension-readmes', this.name);
+ this.loading = false;
+ m.redraw();
+ }
+}
diff --git a/framework/core/js/src/admin/models/ExtensionReadme.js b/framework/core/js/src/admin/models/ExtensionReadme.js
new file mode 100644
index 000000000..91d1c90e1
--- /dev/null
+++ b/framework/core/js/src/admin/models/ExtensionReadme.js
@@ -0,0 +1,5 @@
+import Model from '../../common/Model';
+
+export default class ExtensionReadme extends Model {
+ content = Model.attribute('content');
+}
diff --git a/framework/core/less/admin.less b/framework/core/less/admin.less
index 07f528fc0..ac4d51eb4 100644
--- a/framework/core/less/admin.less
+++ b/framework/core/less/admin.less
@@ -11,4 +11,5 @@
@import "admin/AppearancePage";
@import "admin/MailPage";
@import "admin/NoJs";
-@import "admin/UsersListPage.less";
+@import "admin/ReadmeModal";
+@import "admin/UsersListPage";
diff --git a/framework/core/less/admin/ExtensionPage.less b/framework/core/less/admin/ExtensionPage.less
index 5c10862b0..25e282fc3 100644
--- a/framework/core/less/admin/ExtensionPage.less
+++ b/framework/core/less/admin/ExtensionPage.less
@@ -149,3 +149,20 @@
display: inline-block;
margin-left: 8px;
}
+
+.Readme-link {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: @muted-color;
+}
+
+.ReadmeModal {
+ .Modal-header {
+ background: @control-bg;
+ color: @muted-color
+ }
+ img {
+ max-width: 100%;
+ }
+}
diff --git a/framework/core/less/admin/ReadmeModal.less b/framework/core/less/admin/ReadmeModal.less
new file mode 100644
index 000000000..953b96aba
--- /dev/null
+++ b/framework/core/less/admin/ReadmeModal.less
@@ -0,0 +1,5 @@
+.ReadmeModal {
+ .Placeholder {
+ margin-bottom: 40px;
+ }
+}
diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml
index 3b7772d72..d68a39fce 100644
--- a/framework/core/locale/core.yml
+++ b/framework/core/locale/core.yml
@@ -131,6 +131,10 @@ core:
open_modal: Open Settings
permissions_title: Permissions
purge_button: Purge
+ readme:
+ button_label: README
+ no_readme: This extension does not appear to have a README file
+ title: "{extName} documentation"
# These translations are used in the secondary header.
header:
diff --git a/framework/core/src/Api/Controller/ShowExtensionReadmeController.php b/framework/core/src/Api/Controller/ShowExtensionReadmeController.php
new file mode 100644
index 000000000..4a1e29823
--- /dev/null
+++ b/framework/core/src/Api/Controller/ShowExtensionReadmeController.php
@@ -0,0 +1,47 @@
+extensions = $extensions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function data(ServerRequestInterface $request, Document $document)
+ {
+ $extensionName = Arr::get($request->getQueryParams(), 'name');
+
+ RequestUtil::getActor($request)->assertAdmin();
+
+ return $this->extensions->getExtension($extensionName);
+ }
+}
diff --git a/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php b/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php
new file mode 100644
index 000000000..e59e4f74f
--- /dev/null
+++ b/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php
@@ -0,0 +1,35 @@
+ $extension->getReadme()
+ ];
+
+ return $attributes;
+ }
+
+ public function getId($extension)
+ {
+ return $extension->getId();
+ }
+
+ public function getType($extension)
+ {
+ return 'extension-readmes';
+ }
+}
diff --git a/framework/core/src/Api/routes.php b/framework/core/src/Api/routes.php
index 503a8a0db..5863e4856 100644
--- a/framework/core/src/Api/routes.php
+++ b/framework/core/src/Api/routes.php
@@ -265,6 +265,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\UninstallExtensionController::class)
);
+ // Get readme for an extension
+ $map->get(
+ '/extension-readmes/{name}',
+ 'extension-readmes.show',
+ $route->toController(Controller\ShowExtensionReadmeController::class)
+ );
+
// Update settings
$map->post(
'/settings',
diff --git a/framework/core/src/Extension/Extension.php b/framework/core/src/Extension/Extension.php
index 3398f51b4..0910eba73 100644
--- a/framework/core/src/Extension/Extension.php
+++ b/framework/core/src/Extension/Extension.php
@@ -19,6 +19,7 @@ use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
+use s9e\TextFormatter\Bundles\Fatdown;
use Throwable;
/**
@@ -525,4 +526,28 @@ class Extension implements Arrayable
'links' => $this->getLinks(),
], $this->composerJson);
}
+
+ /**
+ * Gets the rendered contents of the extension README file as a HTML string.
+ *
+ * @return string|null
+ */
+ public function getReadme(): ?string
+ {
+ $content = null;
+
+ if (file_exists($file = "$this->path/README.md")) {
+ $content = file_get_contents($file);
+ } elseif (file_exists($file = "$this->path/README")) {
+ $content = file_get_contents($file);
+ }
+
+ if ($content) {
+ $xml = Fatdown::parse($content);
+
+ return Fatdown::render($xml);
+ }
+
+ return null;
+ }
}