1
0
mirror of https://github.com/flarum/core.git synced 2025-08-17 22:01:44 +02:00

Compare commits

..

5 Commits
v1.8.8 ... main

Author SHA1 Message Date
StyleCI Bot
29da25dec7 Apply fixes from StyleCI 2024-06-21 21:01:38 +00:00
Daniël Klabbers
b94c6f913c chore: fix links and images 2024-06-21 23:01:12 +02:00
flarum-bot
64cd6d1988 Bundled output for commit 51bd7a7e32
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-05-27 09:37:18 +00:00
Darkle
51bd7a7e32 Update the scheduler info link in admin (#3826) 2023-05-27 11:29:56 +02:00
Sami Mazouz
b6a6248dff chore: prepare v2.0 development
Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
2023-05-24 08:52:14 +01:00
315 changed files with 1124 additions and 4559 deletions

View File

@@ -25,7 +25,7 @@ on:
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
type: string
required: false
default: '["7.3", "7.4", "8.0", "8.1", "8.2", "8.3"]'
default: '["7.3", "7.4", "8.0", "8.1", "8.2"]'
php_extensions:
description: PHP extensions to install.
@@ -45,25 +45,13 @@ on:
required: false
default: error_reporting=E_ALL
runner_type:
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
type: string
required: false
default: 'ubuntu-latest'
secrets:
composer_auth:
description: The Composer auth tokens to use for private packages.
required: false
env:
COMPOSER_ROOT_VERSION: dev-main
FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp
COMPOSER_AUTH: ${{ secrets.composer_auth }}
jobs:
test:
runs-on: ${{ inputs.runner_type }}
runs-on: ubuntu-latest
strategy:
matrix:
@@ -103,8 +91,6 @@ jobs:
# Include testing PHP 8.2 with deprecation warnings disabled.
- php: 8.2
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
- php: 8.3
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
# To reduce number of actions, we exclude some PHP versions from running with some DB versions.
exclude:
@@ -172,7 +158,7 @@ jobs:
COMPOSER_PROCESS_TIMEOUT: 600
phpstan:
runs-on: ${{ inputs.runner_type }}
runs-on: ubuntu-latest
strategy:
matrix:

View File

@@ -74,7 +74,7 @@ on:
description: The node version to use for the workflow.
type: number
required: false
default: 20
default: 16
js_package_manager:
description: "Enable TypeScript?"
@@ -86,41 +86,30 @@ on:
type: string
required: false
runner_type:
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
type: string
required: false
default: 'ubuntu-latest'
secrets:
bundlewatch_github_token:
description: The GitHub token to use for Bundlewatch.
required: false
composer_auth:
description: The Composer auth tokens to use for private packages.
required: false
env:
COMPOSER_ROOT_VERSION: dev-main
ci_script: ${{ inputs.js_package_manager == 'yarn' && 'yarn install --immutable' || 'npm ci' }}
cache_dependency_path: ${{ inputs.cache_dependency_path || format(inputs.js_package_manager == 'yarn' && '{0}/yarn.lock' || '{0}/package-lock.json', inputs.frontend_directory) }}
COMPOSER_AUTH: ${{ secrets.composer_auth }}
DISABLE_V8_COMPILE_CACHE: 1
jobs:
build:
name: Checks & Build
runs-on: ${{ inputs.runner_type }}
runs-on: ubuntu-latest
if: >-
((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || github.event_name != 'pull_request')
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node_version }}
cache: ${{ inputs.js_package_manager }}
@@ -143,7 +132,7 @@ jobs:
working-directory: ${{ inputs.frontend_directory }}
- name: JS Checks & Production Build
uses: flarum/action-build@v4
uses: flarum/action-build@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
build_script: ${{ inputs.build_script }}

View File

@@ -1,4 +1,4 @@
name: Extension Manager PHP
name: Package Manager PHP
on: [workflow_dispatch, push, pull_request]

View File

@@ -10,7 +10,7 @@ jobs:
backend_directory: ./
js_package_manager: yarn
cache_dependency_path: ./yarn.lock
main_git_branch: 1.x
main_git_branch: main
enable_tests: true
# @TODO: fix bundlewatch
enable_bundlewatch: false

View File

@@ -1,63 +1,10 @@
# Changelog
## [v1.8.7](https://github.com/flarum/framework/compare/v1.8.6...v1.8.7)
### Fixed
* BasicsPage not viewable if only one language pack enabled, and/or `flarum/nicknames` not enabled (https://github.com/flarum/framework/pull/4062)
## [v1.8.6](https://github.com/flarum/framework/compare/v1.8.5...v1.8.6)
### Fixed
* reset admin page save button in catch handler (https://github.com/flarum/framework/pull/3963)
* suspended users can remove avatar (https://github.com/flarum/framework/pull/3998)
* return null if content left empty in formatter (https://github.com/flarum/framework/pull/4059)
### Changed
* allow DiscussionsSearchSource to be extended (https://github.com/flarum/framework/pull/4025)
* allow modifying the discussion title on PostsUserPage (https://github.com/flarum/framework/pull/4031)
* make it easier to modify AppearancePage, BasicsPage, MailPage (https://github.com/flarum/framework/pull/4037)
* point fontawesome links at v5 free (https://github.com/flarum/framework/pull/4038)
* make WelcomeHero extensible (https://github.com/flarum/framework/pull/4039)
* make PostMeta extensible (https://github.com/flarum/framework/pull/4040)
* extensible TagHero (https://github.com/flarum/framework/pull/4041)
* allow extending PostPreview content (https://github.com/flarum/framework/pull/4043)
* allow classes that extends AbstractJob to be placed on a specified queue (https://github.com/flarum/framework/pull/4026)
* use common component for ip address display (https://github.com/flarum/framework/pull/4042)
* make it easier to add content after the first post (https://github.com/flarum/framework/pull/4050)
* improve extensibility of IndexPage (https://github.com/flarum/framework/pull/4045)
* improve extensibility of DiscussionPage (https://github.com/flarum/framework/pull/4046)
* backport & improve extensibility of DiscussionListItem (https://github.com/flarum/framework/pull/4048)
* improve & use extensibility of CommentPost & Post (https://github.com/flarum/framework/pull/4047)
* allow labels of PostStreamScrubber to be customized (https://github.com/flarum/framework/pull/4049)
* allow to customize time formats through translations (https://github.com/flarum/framework/pull/4053)
### Added
* Export all missing modules in compat (https://github.com/flarum/framework/pull/4044)
* Add (some) missing shims (https://github.com/flarum/framework/pull/4027)
* provide an 'actions' dropdown for extensions to add their additional buttons to the admin UserListPage (https://github.com/flarum/framework/pull/4054)
## [v1.8.5](https://github.com/flarum/framework/compare/v1.8.4...v1.8.5)
### Fixed
* Logout controller allows open redirects [#3948]
## [v1.8.4](https://github.com/flarum/framework/compare/v1.8.3...v1.8.4)
### Fixed
* `s9e/textformatter` 2.15 has breaking changes [#3946]
## [v1.8.3](https://github.com/flarum/framework/compare/v1.8.2...v1.8.3)
### Fixed
* Console extender does not accept ::class [#3900]
* Conditional extender instantiation [#3898]
## [v1.8.2](https://github.com/flarum/framework/compare/v1.8.1...v1.8.2)
### Fixed
* suspended users can abuse avatar upload [#3890]
* missing compat exports [#3888]
## [v1.8.1](https://github.com/flarum/framework/compare/v1.8.0...v1.8.1)
### Fixed
* recover temporary solution for html entities in browser title (e72541e35de4f71f9d870bbd9bb46ddf586bdf1d)
* custom contrast color affected by parents (577890d89c593ae5b6cb96083fab69e2f1ae600c)
* reply placeholder wrong positioning (253a3d281dbf5ce3fa712b629b80587cf67e7dbe)
* (mentions) missed post mentions UI changes with lazy loading [#3832]
* (mentions) cannot use newly introduced mentionables extender [#3849]
* (mentions) missing slug from post mention links ([5a4bb7c](5a4bb7ccf226f66dd44816cb69b3d7cfe4ad7f7c))
## [v1.8.0](https://github.com/flarum/framework/compare/v1.7.1...v1.8.0)
### Fixed

View File

@@ -20,7 +20,7 @@
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarums architecture is amazingly flexible, with a powerful Extension API.
![Screenshot of a Flarum instance, showing multiple discussions and tags.](https://flarum.org/assets/img/home-screenshot.png)
![Screenshot of a Flarum instance, showing multiple discussions and tags.](https://flarum.org/assets/flarum/home-screenshot.png)
## Installation
@@ -37,4 +37,3 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
## License
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).

View File

@@ -46,7 +46,7 @@
"Flarum\\Lock\\": "extensions/lock/src",
"Flarum\\Mentions\\": "extensions/mentions/src",
"Flarum\\Nicknames\\": "extensions/nicknames/src",
"Flarum\\ExtensionManager\\": "extensions/package-manager/src",
"Flarum\\PackageManager\\": "extensions/package-manager/src",
"Flarum\\Pusher\\": "extensions/pusher/src",
"Flarum\\Statistics\\": "extensions/statistics/src",
"Flarum\\Sticky\\": "extensions/sticky/src",
@@ -74,7 +74,7 @@
"flarum/markdown": "self.version",
"flarum/mentions": "self.version",
"flarum/nicknames": "self.version",
"flarum/extension-manager": "self.version",
"flarum/package-manager": "self.version",
"flarum/pusher": "self.version",
"flarum/statistics": "self.version",
"flarum/sticky": "self.version",
@@ -127,7 +127,7 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"pusher/pusher-php-server": "^2.2",
"s9e/text-formatter": ">=2.3.6 <2.15",
"s9e/text-formatter": "^2.3.6",
"staudenmeir/eloquent-eager-limit": "^1.0",
"sycho/json-api": "^0.5.0",
"sycho/sourcemap": "^2.0.0",

View File

@@ -15,7 +15,6 @@
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/flags/*": ["../../flags/js/dist-typings/*"]
}
}

View File

@@ -1,15 +0,0 @@
import 'flarum/common/models/Discussion';
import 'flarum/common/models/Post';
declare module 'flarum/common/models/Discussion' {
export default interface Discussion {
isApproved(): boolean;
}
}
declare module 'flarum/common/models/Post' {
export default interface Post {
isApproved(): boolean;
canApprove(): boolean;
}
}

View File

@@ -11,7 +11,6 @@ namespace Flarum\Approval\Listener;
use Flarum\Approval\Event\PostWasApproved;
use Flarum\Post\Event\Saving;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Contracts\Events\Dispatcher;
class ApproveContent
@@ -24,42 +23,23 @@ class ApproveContent
$events->listen(Saving::class, [$this, 'approvePost']);
}
/**
* @throws PermissionDeniedException
*/
public function approvePost(Saving $event)
{
$attributes = $event->data['attributes'];
$post = $event->post;
// Nothing to do if it is already approved.
if ($post->is_approved) {
return;
}
/*
* We approve a post in one of two cases:
* - The post was unapproved and the allowed action is approving it. We trigger an event.
* - The post was unapproved and the allowed actor is hiding or un-hiding it.
* We approve it silently if the action is unhiding.
*/
$approvingSilently = false;
if (isset($attributes['isApproved'])) {
$event->actor->assertCan('approve', $post);
$isApproved = (bool) $attributes['isApproved'];
} elseif (isset($attributes['isHidden']) && $event->actor->can('approve', $post)) {
} elseif (! empty($attributes['isHidden']) && $event->actor->can('approve', $post)) {
$isApproved = true;
$approvingSilently = $attributes['isHidden'];
}
if (! empty($isApproved)) {
$post->is_approved = true;
if (! $approvingSilently) {
$post->raise(new PostWasApproved($post, $event->actor));
}
$post->raise(new PostWasApproved($post, $event->actor));
}
}
}

2
extensions/emoji/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getEmojiIconCode from './helpers/getEmojiIconCode';
export default {
'emoji/fragments/AutocompleteDropdown': AutocompleteDropdown,
'emoji/helpers/getEmojiIconCode': getEmojiIconCode,
};

View File

@@ -11,9 +11,3 @@ app.initializers.add('flarum-emoji', () => {
// render emoji as image in Posts content and title.
renderEmoji();
});
// Expose compat API
import emojiCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, emojiCompat);

View File

@@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^1.8.6"
"flarum/core": "^1.8"
},
"autoload": {
"psr-4": {

View File

@@ -1,7 +1,3 @@
import 'flarum/common/models/Post';
import 'flarum/forum/ForumApplication';
import 'flarum/forum/components/Post';
import Flag from '../forum/models/Flag';
import FlagListState from '../forum/states/FlagListState';
import Mithril from 'mithril';

View File

@@ -7,7 +7,6 @@ declare const _default: {
'flags/components/FlagPostModal': typeof FlagPostModal;
'flags/components/FlagsPage': typeof FlagsPage;
'flags/components/FlagsDropdown': typeof FlagsDropdown;
'flags/states/FlagListState': typeof FlagListState;
};
export default _default;
import addFlagsToPosts from "./addFlagsToPosts";
@@ -18,4 +17,3 @@ import FlagList from "./components/FlagList";
import FlagPostModal from "./components/FlagPostModal";
import FlagsPage from "./components/FlagsPage";
import FlagsDropdown from "./components/FlagsDropdown";
import FlagListState from "./states/FlagListState";

View File

@@ -1,2 +1,2 @@
declare const _default: (import("flarum/common/extenders/Routes").default | import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Model").default)[];
declare const _default: (import("flarum/common/extenders/Model").default | import("flarum/common/extenders/Routes").default | import("flarum/common/extenders/Store").default)[];
export default _default;

2
extensions/flags/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,3 @@
import 'flarum/common/models/Post';
import 'flarum/forum/ForumApplication';
import 'flarum/forum/components/Post';
import Flag from '../forum/models/Flag';
import FlagListState from '../forum/states/FlagListState';
import Mithril from 'mithril';

View File

@@ -75,7 +75,7 @@ export default function () {
return items;
};
extend(Post.prototype, 'viewItems', function (items) {
extend(Post.prototype, 'content', function (vdom) {
const post = this.attrs.post;
const flags = post.flags();
@@ -83,8 +83,7 @@ export default function () {
if (post.isHidden()) this.revealContent = true;
items.add(
'flagged',
vdom.unshift(
<div className="Post-flagged">
<div className="Post-flagged-flags">
{flags.map((flag) => (
@@ -92,8 +91,7 @@ export default function () {
))}
</div>
<div className="Post-flagged-actions">{this.flagActionItems().toArray()}</div>
</div>,
110
</div>
);
});

View File

@@ -6,7 +6,6 @@ import FlagList from './components/FlagList';
import FlagPostModal from './components/FlagPostModal';
import FlagsPage from './components/FlagsPage';
import FlagsDropdown from './components/FlagsDropdown';
import FlagListState from './states/FlagListState';
export default {
'flags/addFlagsToPosts': addFlagsToPosts,
@@ -17,5 +16,4 @@ export default {
'flags/components/FlagPostModal': FlagPostModal,
'flags/components/FlagsPage': FlagsPage,
'flags/components/FlagsDropdown': FlagsDropdown,
'flags/states/FlagListState': FlagListState,
};

2
extensions/likes/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,9 @@
import 'flarum/common/models/Post';
import Post from 'flarum/common/models/Post';
import User from 'flarum/common/models/User';
declare module 'flarum/common/models/Post' {
export default interface Post {
likes(): User[];
likesCount(): number;
canLike(): boolean;
}
}

View File

@@ -1,11 +0,0 @@
import LikesUserPage from './components/LikesUserPage';
import PostLikedNotification from './components/PostLikedNotification';
import PostLikesModal from './components/PostLikesModal';
import PostLikesModalState from './states/PostLikesModalState';
export default {
'likes/components/LikesUserPage': LikesUserPage,
'likes/components/PostLikedNotification': PostLikedNotification,
'likes/components/PostLikesModal': PostLikesModal,
'likes/states/PostLikesModalState': PostLikesModalState,
};

View File

@@ -24,9 +24,3 @@ app.initializers.add('flarum-likes', () => {
});
});
});
// Expose compat API
import likesCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, likesCompat);

2
extensions/lock/js/dist/forum.js generated vendored
View File

@@ -1,2 +1,2 @@
(()=>{var o={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return o.d(n,{a:n}),n},d:(t,n)=>{for(var c in n)o.o(n,c)&&!o.o(t,c)&&Object.defineProperty(t,c,{enumerable:!0,get:n[c]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>j});const n=flarum.core.compat["common/extend"],c=flarum.core.compat["forum/app"];var e=o.n(c);const r=flarum.core.compat["forum/components/NotificationGrid"];var s=o.n(r);function a(o,t){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(o,t){return o.__proto__=t,o},a(o,t)}function i(o,t){o.prototype=Object.create(t.prototype),o.prototype.constructor=o,a(o,t)}const u=flarum.core.compat["forum/components/Notification"];var l=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return"fas fa-lock"},n.href=function(){var o=this.attrs.notification;return e().route.discussion(o.subject(),o.content().postNumber)},n.content=function(){return e().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})},t}(o.n(u)());const f=flarum.core.compat["common/models/Discussion"];var d=o.n(f);const p=flarum.core.compat["common/components/Badge"];var k=o.n(p);const y=flarum.core.compat["forum/utils/DiscussionControls"];var b=o.n(y);const _=flarum.core.compat["forum/components/DiscussionPage"];var v=o.n(_);const h=flarum.core.compat["common/components/Button"];var L=o.n(h);const g=flarum.core.compat["common/extenders"];var O=o.n(g);const x=flarum.core.compat["forum/components/EventPost"];var P=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"},n.descriptionKey=function(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"},t}(o.n(x)());const j=[(new(O().PostTypes)).add("discussionLocked",P),new(O().Model)(d()).attribute("isLocked").attribute("canLock")],D={"lock/components/DiscussionLockedNotification":l,"lock/components/DiscussionLockedPost":P},S=flarum.core;e().initializers.add("flarum-lock",(function(){e().notificationComponents.discussionLocked=l,(0,n.extend)(d().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(k(),{type:"locked",label:e().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,n.extend)(b(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(L(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},e().translator.trans("flarum-lock.forum.discussion_controls."+(t.isLocked()?"unlock":"lock")+"_button")))})),b().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((function(){e().current.matches(v())&&e().current.get("stream").update(),m.redraw()}))},(0,n.extend)(s().prototype,"notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:e().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))})),Object.assign(S.compat,D)})(),module.exports=t})();
(()=>{var o={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return o.d(n,{a:n}),n},d:(t,n)=>{for(var e in n)o.o(n,e)&&!o.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:n[e]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>j});const n=flarum.core.compat["common/extend"],e=flarum.core.compat["forum/app"];var c=o.n(e);const r=flarum.core.compat["forum/components/NotificationGrid"];var s=o.n(r);function a(o,t){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(o,t){return o.__proto__=t,o},a(o,t)}function i(o,t){o.prototype=Object.create(t.prototype),o.prototype.constructor=o,a(o,t)}const u=flarum.core.compat["forum/components/Notification"];var f=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return"fas fa-lock"},n.href=function(){var o=this.attrs.notification;return c().route.discussion(o.subject(),o.content().postNumber)},n.content=function(){return c().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})},t}(o.n(u)());const l=flarum.core.compat["common/models/Discussion"];var d=o.n(l);const p=flarum.core.compat["common/components/Badge"];var k=o.n(p);const y=flarum.core.compat["forum/utils/DiscussionControls"];var b=o.n(y);const _=flarum.core.compat["forum/components/DiscussionPage"];var v=o.n(_);const h=flarum.core.compat["common/components/Button"];var g=o.n(h);const L=flarum.core.compat["common/extenders"];var x=o.n(L);const O=flarum.core.compat["forum/components/EventPost"];var P=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"},n.descriptionKey=function(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"},t}(o.n(O)());const j=[(new(x().PostTypes)).add("discussionLocked",P),new(x().Model)(d()).attribute("isLocked").attribute("canLock")];c().initializers.add("flarum-lock",(function(){c().notificationComponents.discussionLocked=f,(0,n.extend)(d().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(k(),{type:"locked",label:c().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,n.extend)(b(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},c().translator.trans("flarum-lock.forum.discussion_controls."+(t.isLocked()?"unlock":"lock")+"_button")))})),b().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((function(){c().current.matches(v())&&c().current.get("stream").update(),m.redraw()}))},(0,n.extend)(s().prototype,"notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:c().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=t})();
//# sourceMappingURL=forum.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
import 'flarum/common/models/Discussion';
declare module 'flarum/common/models/Discussion' {
export default interface Discussion {
isLocked(): boolean;
canLock(): boolean;
}
}

View File

@@ -1,7 +0,0 @@
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
import DiscussionLockedPost from './components/DiscussionLockedPost';
export default {
'lock/components/DiscussionLockedNotification': DiscussionLockedNotification,
'lock/components/DiscussionLockedPost': DiscussionLockedPost,
};

View File

@@ -22,9 +22,3 @@ app.initializers.add('flarum-lock', () => {
});
});
});
// Expose compat API
import lockCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, lockCompat);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
import commonCompat from '../common/compat';
export default {
...commonCompat,
};

View File

@@ -2,9 +2,3 @@ import app from 'flarum/admin/app';
import { initialize } from '../common/index';
app.initializers.add('flarum-markdown', initialize);
// Expose compat API
import markdownCompat from './compat';
import { compat } from '@flarum/core/admin';
Object.assign(compat, markdownCompat);

View File

@@ -1,7 +0,0 @@
import MarkdownButton from './components/MarkdownButton';
import MarkdownToolbar from './components/MarkdownToolbar';
export default {
'markdown/components/MarkdownButton': MarkdownButton,
'markdown/components/MarkdownToolbar': MarkdownToolbar,
};

View File

@@ -1,5 +0,0 @@
import commonCompat from '../common/compat';
export default {
...commonCompat,
};

View File

@@ -2,9 +2,3 @@ import app from 'flarum/forum/app';
import { initialize } from '../common/index';
app.initializers.add('flarum-markdown', initialize);
// Expose compat API
import markdownCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, markdownCompat);

View File

@@ -78,17 +78,15 @@ return [
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
->load([
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
'posts.mentionsGroups'
])
->loadWhere('posts.mentionedBy', [LoadMentionedByRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadMentionedByRelationship::class, 'countRelation']),
(new Extend\ApiController(Controller\ListDiscussionsController::class))
->load([
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', 'firstPost.mentionsGroups',
'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups',
]),
(new Extend\ApiController(Controller\ShowPostController::class))
@@ -100,7 +98,7 @@ return [
(new Extend\ApiController(Controller\ListPostsController::class))
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups'])
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsGroups'])
->loadWhere('mentionedBy', [LoadMentionedByRelationship::class, 'mutateRelation'])
->prepareDataForSerialization([LoadMentionedByRelationship::class, 'countRelation']),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,3 @@
import 'flarum/forum/ForumApplication';
import 'flarum/common/models/User';
import 'flarum/common/models/Post';
import MentionFormats from '../forum/mentionables/formats/MentionFormats';
import type BasePost from 'flarum/common/models/Post';

View File

@@ -5,9 +5,12 @@ import TextEditorButton from 'flarum/common/components/TextEditorButton';
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import MentionFormats from './mentionables/formats/MentionFormats';
import MentionableModels from './mentionables/MentionableModels';
export default function addComposerAutocomplete() {
app.mentionFormats = new MentionFormats();
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
const dropdown = new AutocompleteDropdown();

View File

@@ -118,7 +118,7 @@ export default function addMentionedByList() {
});
const limit = 4;
const overLimit = post.mentionedByCount() > limit;
const overLimit = repliers.length > limit;
// Create a list of unique users who have replied. So even if a user has
// replied twice, they will only be in this array once.
@@ -136,7 +136,7 @@ export default function addMentionedByList() {
// others" name to the end of the list. Clicking on it will display a modal
// with a full list of names.
if (overLimit) {
const count = post.mentionedByCount() - names.length;
const count = repliers.length - names.length;
names.push(app.translator.trans('flarum-mentions.forum.post.others_text', { count }));
}

View File

@@ -1,6 +1,4 @@
import GroupMentionedNotification from './components/GroupMentionedNotification';
import MentionedByModal from './components/MentionedByModal';
import MentionsDropdownItem from './components/MentionsDropdownItem';
import MentionsUserPage from './components/MentionsUserPage';
import PostMentionedNotification from './components/PostMentionedNotification';
import UserMentionedNotification from './components/UserMentionedNotification';
@@ -11,24 +9,13 @@ import getMentionText from './utils/getMentionText';
import * as reply from './utils/reply';
import selectedText from './utils/selectedText';
import * as textFormatter from './utils/textFormatter';
import GroupMention from './mentionables/GroupMention';
import MentionableModel from './mentionables/MentionableModel';
import MentionableModels from './mentionables/MentionableModels';
import PostMention from './mentionables/PostMention';
import TagMention from './mentionables/TagMention';
import UserMention from './mentionables/UserMention';
import AtMentionFormat from './mentionables/formats/AtMentionFormat';
import HashMentionFormat from './mentionables/formats/HashMentionFormat';
import MentionFormat from './mentionables/formats/MentionFormat';
import MentionFormats from './mentionables/formats/MentionFormats';
import Mentionables from './extenders/Mentionables';
import MentionedByModalState from './state/MentionedByModalState';
export default {
'mentions/components/MentionsUserPage': MentionsUserPage,
'mentions/components/PostMentionedNotification': PostMentionedNotification,
'mentions/components/MentionedByModal': MentionedByModal,
'mentions/components/MentionsDropdownItem': MentionsDropdownItem,
'mentions/components/UserMentionedNotification': UserMentionedNotification,
'mentions/components/GroupMentionedNotification': GroupMentionedNotification,
'mentions/fragments/AutocompleteDropdown': AutocompleteDropdown,
@@ -38,16 +25,7 @@ export default {
'mentions/utils/reply': reply,
'mentions/utils/selectedText': selectedText,
'mentions/utils/textFormatter': textFormatter,
'mentions/mentionables/GroupMention': GroupMention,
'mentions/mentionables/MentionableModel': MentionableModel,
'mentions/mentionables/MentionableModels': MentionableModels,
'mentions/mentionables/PostMention': PostMention,
'mentions/mentionables/TagMention': TagMention,
'mentions/mentionables/UserMention': UserMention,
'mentions/mentionables/formats/AtMentionFormat': AtMentionFormat,
'mentions/mentionables/formats/HashMentionFormat': HashMentionFormat,
'mentions/mentionables/formats/MentionFormat': MentionFormat,
'mentions/mentionables/formats/MentionFormats': MentionFormats,
'mentions/extenders/Mentionables': Mentionables,
'mentions/state/MentionedByModalState': MentionedByModalState,
};

View File

@@ -5,7 +5,7 @@ import type MentionFormat from '../mentionables/formats/MentionFormat';
export default class Mentionables implements IExtender<ForumApplication> {
protected formats: (new () => MentionFormat)[] = [];
protected mentionables: Record<string, (new (...args: any[]) => MentionableModel)[]> = {};
protected mentionables: Record<string, (new () => MentionableModel)[]> = {};
/**
* Register a new mention format.
@@ -26,7 +26,7 @@ export default class Mentionables implements IExtender<ForumApplication> {
* @param mentionable The mentionable instance to register.
* Must extend MentionableModel.
*/
mentionable(symbol: string, mentionable: new (...args: any[]) => MentionableModel): this {
mentionable(symbol: string, mentionable: new () => MentionableModel): this {
if (!this.mentionables[symbol]) {
this.mentionables[symbol] = [];
}

View File

@@ -13,14 +13,11 @@ import addComposerAutocomplete from './addComposerAutocomplete';
import PostMentionedNotification from './components/PostMentionedNotification';
import UserMentionedNotification from './components/UserMentionedNotification';
import GroupMentionedNotification from './components/GroupMentionedNotification';
import MentionFormats from './mentionables/formats/MentionFormats';
import UserPage from 'flarum/forum/components/UserPage';
import LinkButton from 'flarum/common/components/LinkButton';
import User from 'flarum/common/models/User';
import Model from 'flarum/common/Model';
app.mentionFormats = new MentionFormats();
export { default as extend } from './extend';
app.initializers.add('flarum-mentions', function () {

View File

@@ -4,7 +4,7 @@ import TagMention from '../TagMention';
export default class HashMentionFormat extends MentionFormat {
public mentionables: (new (...args: any[]) => MentionableModel)[] = [TagMention];
protected extendable: boolean = true;
protected extendable: boolean = false;
public trigger(): string {
return '#';

View File

@@ -29,7 +29,7 @@ class LoadMentionedByRelationship
$actor = RequestUtil::getActor($request);
return $query
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsUsers'])
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsUsers'])
->whereVisibleTo($actor)
->oldest()
// Limiting a relationship results is only possible because
@@ -50,16 +50,6 @@ class LoadMentionedByRelationship
$loadable = $data->newCollection($data->posts)->filter(function ($post) {
return $post instanceof Post;
});
// firstPost and lastPost might have been included in the API response,
// so we have to make sure counts are also loaded for them.
if ($data->firstPost) {
$loadable->push($data->firstPost);
}
if ($data->lastPost) {
$loadable->push($data->lastPost);
}
} elseif ($data instanceof Collection) {
$loadable = $data;
} elseif ($data instanceof Post) {

View File

View File

@@ -9,9 +9,6 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Discussion\Discussion;
use Flarum\Http\SlugManager;
use Flarum\Post\Post;
use Psr\Http\Message\ServerRequestInterface as Request;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@@ -24,15 +21,9 @@ class FormatPostMentions
*/
private $translator;
/**
* @var SlugManager
*/
private $slugManager;
public function __construct(TranslatorInterface $translator, SlugManager $slugManager)
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
$this->slugManager = $slugManager;
}
/**
@@ -40,17 +31,16 @@ class FormatPostMentions
*
* @param \s9e\TextFormatter\Renderer $renderer
* @param mixed $context
* @param string $xml
* @param \Psr\Http\Message\ServerRequestInterface|null $request
* @return string $xml to be rendered
* @param string|null $xml
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return string
*/
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
{
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
: Post::find($attributes['id']);
$post = $context;
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
$post = $post->mentionsPosts->find($attributes['id']);
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;
}
@@ -66,12 +56,6 @@ class FormatPostMentions
$attributes['displayname'] = $this->translator->trans('core.lib.username.deleted_text');
}
if ($post) {
$attributes['discussionid'] = $this->slugManager
->forResource(Discussion::class)
->toSlug($post->discussion);
}
return $attributes;
});
}

View File

@@ -9,7 +9,6 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Post\Post;
use s9e\TextFormatter\Utils;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -28,16 +27,12 @@ class UnparsePostMentions
/**
* Configure rendering for user mentions.
*
* @param string|null $xml
* @param string $xml
* @param mixed $context
* @return mixed $xml to be unparsed
* @return string $xml to be unparsed
*/
public function __invoke($context, $xml)
public function __invoke($context, string $xml)
{
if ($xml === null) {
return $xml;
}
$xml = $this->updatePostMentionTags($context, $xml);
$xml = $this->unparsePostMentionTags($xml);
@@ -55,11 +50,8 @@ class UnparsePostMentions
{
$post = $context;
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
: Post::find($attributes['id']);
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
$post = $post->mentionsPosts->find($attributes['id']);
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;
}

View File

@@ -18,16 +18,12 @@ class UnparseTagMentions
/**
* Configure rendering for user mentions.
*
* @param string|null $xml
* @param string $xml
* @param mixed $context
* @return mixed $xml to be unparsed
* @return string $xml to be unparsed
*/
public function __invoke($context, $xml)
public function __invoke($context, string $xml)
{
if ($xml === null) {
return $xml;
}
$xml = $this->updateTagMentionTags($context, $xml);
$xml = $this->unparseTagMentionTags($xml);

View File

@@ -29,16 +29,12 @@ class UnparseUserMentions
/**
* Configure rendering for user mentions.
*
* @param string|null $xml
* @param string $xml
* @param mixed $context
* @return mixed $xml to be unparsed
* @return string $xml to be unparsed
*/
public function __invoke($context, $xml)
public function __invoke($context, string $xml)
{
if ($xml === null) {
return $xml;
}
$xml = $this->updateUserMentionTags($context, $xml);
$xml = $this->unparseUserMentionTags($xml);

View File

@@ -47,8 +47,6 @@ class SendMentionsNotificationsJob extends AbstractJob
public function __construct(CommentPost $post, array $userMentions, array $postMentions, array $groupMentions)
{
parent::__construct();
$this->post = $post;
$this->userMentions = $userMentions;
$this->postMentions = $postMentions;

View File

@@ -1,137 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Mentions\Tests\integration\api;
use Flarum\Extend;
use Flarum\Testing\integration\TestCase;
class CreateDiscussionTest extends TestCase
{
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->extension('flarum-mentions');
$this->extend(
(new Extend\Event())
->listen(\Flarum\Post\Event\Saving::class, function ($event) {
$event->post->content;
})
);
}
/**
* @test
*/
public function cannot_create_discussion_with_empty_string()
{
$response = $this->send(
$this->request('POST', '/api/discussions', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'title' => 'Test post',
'content' => '',
],
],
],
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'The content field is required.',
'source' => ['pointer' => '/data/attributes/content'],
],
],
], json_decode($body, true));
}
/**
* @test
*/
public function cannot_create_discussion_without_content_property()
{
$response = $this->send(
$this->request('POST', '/api/discussions', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'title' => 'Test post',
],
],
],
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'The content field is required.',
'source' => ['pointer' => '/data/attributes/content'],
],
],
], json_decode($body, true));
}
/**
* @test
*/
public function cannot_create_discussion_with_content_set_to_null()
{
$response = $this->send(
$this->request('POST', '/api/discussions', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'title' => 'Test post',
'content' => null,
],
],
],
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'The content field is required.',
'source' => ['pointer' => '/data/attributes/content'],
],
],
], json_decode($body, true));
}
}

View File

@@ -1,110 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Mentions\Tests\integration\api;
use Flarum\Extend;
use Flarum\Testing\integration\TestCase;
class EditPostTest extends TestCase
{
/**
* @inheritDoc
*/
protected function setUp(): void
{
parent::setUp();
$this->extension('flarum-mentions');
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => 'Discussion with post', 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>'],
]
]);
$this->extend(
(new Extend\Event())
->listen(\Flarum\Post\Event\Saving::class, function ($event) {
$event->post->content;
})
);
}
/**
* @test
*/
public function cannot_update_post_with_empty_string()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/1', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '',
],
],
],
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'The content field is required.',
'source' => ['pointer' => '/data/attributes/content'],
],
],
], json_decode($body, true));
}
/**
* @test
*/
public function cannot_update_post_with_invalid_content_type()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/1', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => [],
],
],
],
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'The content field is required.',
'source' => ['pointer' => '/data/attributes/content'],
],
],
], json_decode($body, true));
}
}

View File

@@ -11,9 +11,7 @@ namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Extend;
use Flarum\Formatter\Formatter;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\DisplayName\DriverInterface;
@@ -540,40 +538,6 @@ class PostMentionsTest extends TestCase
$this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(11));
}
/**
* @test
*/
public function rendering_post_mention_with_a_post_context_works()
{
/** @var Formatter $formatter */
$formatter = $this->app()->getContainer()->make(Formatter::class);
$post = Post::find(4);
$user = User::find(1);
$xml = $formatter->parse($post->content, $post, $user);
$renderedHtml = $formatter->render($xml, $post);
$this->assertStringContainsString('TOBY$', $renderedHtml);
}
/**
* @test
*/
public function rendering_post_mention_without_a_context_works()
{
/** @var Formatter $formatter */
$formatter = $this->app()->getContainer()->make(Formatter::class);
$post = Post::find(4);
$user = User::find(1);
$xml = $formatter->parse($post->content, null, $user);
$renderedHtml = $formatter->render($xml);
$this->assertStringContainsString('TOBY$', $renderedHtml);
}
}
class CustomOtherDisplayNameDriver implements DriverInterface

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
import 'flarum/common/models/User';
declare module 'flarum/common/models/User' {
export default interface User {
canEditNickname(): boolean;
}
}

View File

@@ -1,5 +0,0 @@
import NicknameModal from './components/NicknameModal';
export default {
'nicknames/components/NicknameModal': NicknameModal,
};

View File

@@ -110,9 +110,3 @@ app.initializers.add('flarum/nicknames', () => {
}
});
});
// Expose compat API
import nicknamesCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, nicknamesCompat);

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2024 Stichting Flarum (Flarum Foundation)
Copyright (c) Sami Mazouz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,18 +1,5 @@
# Extension Manager
# Package Manager
The extension manager is a tool that allows you to easily install and manage extensions. It runs [composer](https://getcomposer.org/) under the hood.
*An Experiment.*
## Security
If admin access is given to untrustworthy users, they can install malicious extensions. Please be careful.
This extension is optional and can be removed for those who prefer to manually manage installs and updates through the command line interface.
## Troubleshooting
If you have many extensions installed, you may run into memory issues when using the extension manager. If this happens, you can use an asynchronous queue that will run the extension manager in the background.
* Simple database queue guide: https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting
* (Advanced) Redis queue: https://discuss.flarum.org/d/21873-redis-sessions-cache-queues
You can find detailed logs on the extension manager operations in the `storage/logs/composer` directory. Please include the latest log file when reporting issues in the [Flarum support forum](https://discuss.flarum.org/t/support).
Read: https://github.com/flarum/package-manager/wiki

View File

@@ -1,6 +1,6 @@
{
"name": "flarum/extension-manager",
"description": "An extension manager to install, update and remove extension packages from the interface (Wrapper around composer).",
"name": "flarum/package-manager",
"description": "A Flarum Package Manager.",
"keywords": [
"extensions",
"composer",
@@ -18,12 +18,12 @@
}
],
"support": {
"issues": "https://github.com/flarum/framework/issues",
"source": "https://github.com/flarum/extension-manager"
"issues": "https://github.com/flarum/package-manager/issues",
"source": "https://github.com/flarum/package-manager"
},
"require": {
"flarum/core": "^1.8",
"composer/composer": "^2.7"
"composer/composer": "^2.3"
},
"require-dev": {
"flarum/testing": "^1.0.0",
@@ -31,7 +31,7 @@
},
"extra": {
"flarum-extension": {
"title": "Extension Manager",
"title": "Package Manager",
"icon": {
"name": "fas fa-box-open",
"backgroundColor": "#117187",
@@ -69,12 +69,12 @@
},
"autoload": {
"psr-4": {
"Flarum\\ExtensionManager\\": "src/"
"Flarum\\PackageManager\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Flarum\\ExtensionManager\\Tests\\": "tests/"
"Flarum\\PackageManager\\Tests\\": "tests/"
}
},
"scripts": {

View File

@@ -7,26 +7,32 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\ExtensionManager;
namespace Flarum\PackageManager;
use Flarum\Extend;
use Flarum\Foundation\Paths;
use Flarum\Frontend\Document;
use Flarum\PackageManager\Exception\ComposerCommandFailedException;
use Flarum\PackageManager\Exception\ComposerRequireFailedException;
use Flarum\PackageManager\Exception\ComposerUpdateFailedException;
use Flarum\PackageManager\Exception\ExceptionHandler;
use Flarum\PackageManager\Exception\MajorUpdateFailedException;
use Flarum\PackageManager\Settings\LastUpdateCheck;
use Flarum\PackageManager\Settings\LastUpdateRun;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\SyncQueue;
return [
(new Extend\Routes('api'))
->post('/extension-manager/extensions', 'extension-manager.extensions.require', Api\Controller\RequireExtensionController::class)
->patch('/extension-manager/extensions/{id}', 'extension-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
->delete('/extension-manager/extensions/{id}', 'extension-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
->post('/extension-manager/check-for-updates', 'extension-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
->post('/extension-manager/why-not', 'extension-manager.why-not', Api\Controller\WhyNotController::class)
->post('/extension-manager/minor-update', 'extension-manager.minor-update', Api\Controller\MinorUpdateController::class)
->post('/extension-manager/major-update', 'extension-manager.major-update', Api\Controller\MajorUpdateController::class)
->post('/extension-manager/global-update', 'extension-manager.global-update', Api\Controller\GlobalUpdateController::class)
->get('/extension-manager-tasks', 'extension-manager.tasks.index', Api\Controller\ListTasksController::class)
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
->post('/package-manager/extensions', 'package-manager.extensions.require', Api\Controller\RequireExtensionController::class)
->patch('/package-manager/extensions/{id}', 'package-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
->delete('/package-manager/extensions/{id}', 'package-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
->post('/package-manager/check-for-updates', 'package-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
->post('/package-manager/why-not', 'package-manager.why-not', Api\Controller\WhyNotController::class)
->post('/package-manager/minor-update', 'package-manager.minor-update', Api\Controller\MinorUpdateController::class)
->post('/package-manager/major-update', 'package-manager.major-update', Api\Controller\MajorUpdateController::class)
->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class)
->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class),
(new Extend\Frontend('admin'))
->css(__DIR__.'/less/admin.less')
@@ -34,34 +40,31 @@ return [
->content(function (Document $document) {
$paths = resolve(Paths::class);
$document->payload['flarum-extension-manager.writable_dirs'] = is_writable($paths->vendor)
$document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor)
&& is_writable($paths->storage)
&& (! file_exists($paths->storage.'/.composer') || is_writable($paths->storage.'/.composer'))
&& is_writable($paths->base.'/composer.json')
&& is_writable($paths->base.'/composer.lock');
$document->payload['flarum-extension-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
$document->payload['flarum-package-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
}),
new Extend\Locales(__DIR__.'/locale'),
(new Extend\Settings())
->default(Settings\LastUpdateCheck::key(), json_encode(Settings\LastUpdateCheck::default()))
->default(Settings\LastUpdateRun::key(), json_encode(Settings\LastUpdateRun::default()))
->default('flarum-extension-manager.queue_jobs', '0')
->default('flarum-extension-manager.minimum_stability', 'stable')
->default('flarum-extension-manager.task_retention_days', 7),
->default(LastUpdateCheck::key(), json_encode(LastUpdateCheck::default()))
->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default()))
->default('flarum-package-manager.queue_jobs', false),
(new Extend\ServiceProvider)
->register(ExtensionManagerServiceProvider::class),
->register(PackageManagerServiceProvider::class),
(new Extend\ErrorHandling)
->handler(Exception\ComposerCommandFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class)
->handler(ComposerCommandFailedException::class, ExceptionHandler::class)
->handler(ComposerRequireFailedException::class, ExceptionHandler::class)
->handler(ComposerUpdateFailedException::class, ExceptionHandler::class)
->handler(MajorUpdateFailedException::class, ExceptionHandler::class)
->status('extension_already_installed', 409)
->status('extension_not_installed', 409)
->status('no_new_major_version', 409)
->status('extension_not_directly_dependency', 409),
->status('no_new_major_version', 409),
];

View File

@@ -1,49 +0,0 @@
import AuthMethodModal from './components/AuthMethodModal';
import ConfigureAuth from './components/ConfigureAuth';
import ConfigureComposer from './components/ConfigureComposer';
import ConfigureJson from './components/ConfigureJson';
import ControlSection from './components/ControlSection';
import ExtensionItem from './components/ExtensionItem';
import Installer from './components/Installer';
import Label from './components/Label';
import MajorUpdater from './components/MajorUpdater';
import Pagination from './components/Pagination';
import QueueSection from './components/QueueSection';
import RepositoryModal from './components/RepositoryModal';
import SettingsPage from './components/SettingsPage';
import TaskOutputModal from './components/TaskOutputModal';
import Updater from './components/Updater';
import WhyNotModal from './components/WhyNotModal';
import Task from './models/Task';
import ControlSectionState from './states/ControlSectionState';
import ExtensionManagerState from './states/ExtensionManagerState';
import QueueState from './states/QueueState';
import errorHandler from './utils/errorHandler';
import humanDuration from './utils/humanDuration';
import jumpToQueue from './utils/jumpToQueue';
declare const _default: {
'extension-manager/components/AuthMethodModal': typeof AuthMethodModal;
'extension-manager/components/ConfigureAuth': typeof ConfigureAuth;
'extension-manager/components/ConfigureComposer': typeof ConfigureComposer;
'extension-manager/components/ConfigureJson': typeof ConfigureJson;
'extension-manager/components/ControlSection': typeof ControlSection;
'extension-manager/components/ExtensionItem': typeof ExtensionItem;
'extension-manager/components/Installer': typeof Installer;
'extension-manager/components/Label': typeof Label;
'extension-manager/components/MajorUpdater': typeof MajorUpdater;
'extension-manager/components/Pagination': typeof Pagination;
'extension-manager/components/QueueSection': typeof QueueSection;
'extension-manager/components/RepositoryModal': typeof RepositoryModal;
'extension-manager/components/SettingsPage': typeof SettingsPage;
'extension-manager/components/TaskOutputModal': typeof TaskOutputModal;
'extension-manager/components/Updater': typeof Updater;
'extension-manager/components/WhyNotModal': typeof WhyNotModal;
'extension-manager/models/Task': typeof Task;
'extension-manager/states/ControlSectionState': typeof ControlSectionState;
'extension-manager/states/ExtensionManagerState': typeof ExtensionManagerState;
'extension-manager/states/QueueState': typeof QueueState;
'extension-manager/utils/errorHandler': typeof errorHandler;
'extension-manager/utils/humanDuration': typeof humanDuration;
'extension-manager/utils/jumpToQueue': typeof jumpToQueue;
};
export default _default;

View File

@@ -1,19 +0,0 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import Stream from 'flarum/common/utils/Stream';
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
onsubmit: (type: string, host: string, token: string) => void;
type?: string;
host?: string;
token?: string;
}
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type: Stream<string>;
protected host: Stream<string>;
protected token: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
className(): string;
title(): Mithril.Children;
content(): Mithril.Children;
submit(): void;
}

View File

@@ -1,10 +0,0 @@
import type Mithril from 'mithril';
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
protected type: string;
title(): Mithril.Children;
className(): string;
content(): Mithril.Children;
submitButton(): Mithril.Children[];
onchange(oldHost: string | null, type: string, host: string, token: string): void;
}

View File

@@ -1,14 +0,0 @@
import type Mithril from 'mithril';
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
export declare type Repository = {
type: 'composer' | 'vcs' | 'path';
url: string;
};
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
protected type: string;
title(): Mithril.Children;
className(): string;
content(): Mithril.Children;
submitButton(): Mithril.Children[];
onchange(repository: Repository, name: string): void;
}

View File

@@ -1,24 +0,0 @@
import type Mithril from 'mithril';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
import type ItemList from 'flarum/common/utils/ItemList';
import Stream from 'flarum/common/utils/Stream';
export interface IConfigureJson extends ComponentAttrs {
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
}
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>>;
protected initialSettings: Record<string, any> | null;
protected loading: boolean;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
protected abstract type: string;
abstract title(): Mithril.Children;
abstract content(): Mithril.Children;
className(): string;
view(): Mithril.Children;
submitButton(): Mithril.Children[];
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children>;
setting(key: string): Stream<any>;
submit(readOnly: boolean): void;
isDirty(): boolean;
}

View File

@@ -5,10 +5,7 @@ import { UpdatedPackage } from '../states/ControlSectionState';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
updates: UpdatedPackage;
onClickUpdate: CallableFunction | {
soft: CallableFunction;
hard: CallableFunction;
};
onClickUpdate: CallableFunction;
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;

View File

@@ -1,18 +0,0 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import Stream from 'flarum/common/utils/Stream';
import { type Repository } from './ConfigureComposer';
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
onsubmit: (repository: Repository, key: string) => void;
name?: string;
repository?: Repository;
}
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
protected name: Stream<string>;
protected repository: Stream<Repository>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
className(): string;
title(): Mithril.Children;
content(): Mithril.Children;
submit(): void;
}

View File

@@ -2,7 +2,5 @@ import type Mithril from 'mithril';
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
import ItemList from 'flarum/common/utils/ItemList';
export default class SettingsPage extends ExtensionPage {
content(): JSX.Element;
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown>;
onsaved(): void;
}

View File

@@ -1,5 +1,5 @@
/// <reference types="mithril" />
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
/// <reference types="flarum/@types/translator-icu-rich" />
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Task from '../models/Task';
interface TaskOutputModalAttrs extends IInternalModalAttrs {

View File

@@ -1,4 +1,4 @@
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
/// <reference types="flarum/@types/translator-icu-rich" />
import type Mithril from 'mithril';
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
export interface WhyNotModalAttrs extends IInternalModalAttrs {

View File

@@ -1,12 +1,11 @@
import Model from 'flarum/common/Model';
export declare type TaskOperations = 'extension_install' | 'extension_remove' | 'extension_update' | 'update_global' | 'update_minor' | 'update_major' | 'update_check' | 'why_not';
export default class Task extends Model {
status(): "pending" | "running" | "failure" | "success";
status(): "running" | "pending" | "success" | "failure";
operation(): TaskOperations;
command(): string;
package(): string;
output(): string;
guessedCause(): string;
createdAt(): Date | null | undefined;
startedAt(): Date;
finishedAt(): Date;

View File

@@ -9,8 +9,6 @@ export declare type UpdatedPackage = {
'latest-minor': string | null;
'latest-major': string | null;
'latest-status': string;
'required-as': string;
'direct-dependency': boolean;
description: string;
};
export declare type ComposerUpdates = {
@@ -33,7 +31,7 @@ export declare type LastUpdateRun = {
} & {
limitedPackages: () => string[];
};
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action';
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
export declare type CoreUpdate = {
package: UpdatedPackage;
extension: Extension;
@@ -47,17 +45,13 @@ export default class ControlSectionState {
get lastUpdateRun(): LastUpdateRun;
constructor();
isLoading(name?: LoadingTypes): boolean;
hasOperationRunning(): boolean;
isLoadingOtherThan(name: LoadingTypes): boolean;
setLoading(name: LoadingTypes): void;
requirePackage(data: any): void;
checkForUpdates(): void;
updateCoreMinor(): void;
updateExtension(extension: Extension, updateMode: 'soft' | 'hard'): void;
updateExtension(extension: Extension): void;
updateGlobally(): void;
formatExtensionUpdates(lastUpdateCheck: LastUpdateCheck): Extension[];
formatCoreUpdate(lastUpdateCheck: LastUpdateCheck): CoreUpdate | null;
majorUpdate({ dryRun }: {
dryRun: boolean;
}): void;
}
export {};

View File

@@ -1,6 +1,6 @@
import QueueState from './QueueState';
import ControlSectionState from './ControlSectionState';
export default class ExtensionManagerState {
export default class PackageManagerState {
queue: QueueState;
control: ControlSectionState;
}

View File

@@ -1,12 +1,11 @@
import Task from '../models/Task';
import { ApiQueryParamsPlural } from 'flarum/common/Store';
export default class QueueState {
private polling;
private tasks;
private limit;
private offset;
private total;
load(params?: ApiQueryParamsPlural, actionTaken?: boolean): Promise<Task[]>;
load(params?: ApiQueryParamsPlural): Promise<import("flarum/common/Store").ApiResponsePlural<Task>>;
getItems(): Task[] | null;
getTotalPages(): number;
pageNumber(): number;
@@ -14,6 +13,4 @@ export default class QueueState {
hasNext(): boolean;
prev(): void;
next(): void;
pollQueue(actionTaken?: boolean): void;
hasPending(): boolean;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"name": "@flarum/extension-manager",
"name": "@flarum/package-manager",
"version": "0.0.0",
"private": true,
"prettier": "@flarum/prettier-config",

View File

@@ -1,52 +0,0 @@
import AuthMethodModal from './components/AuthMethodModal';
import ConfigureAuth from './components/ConfigureAuth';
import ConfigureComposer from './components/ConfigureComposer';
import ConfigureJson from './components/ConfigureJson';
import ControlSection from './components/ControlSection';
import ExtensionItem from './components/ExtensionItem';
import Installer from './components/Installer';
import Label from './components/Label';
import MajorUpdater from './components/MajorUpdater';
import Pagination from './components/Pagination';
import QueueSection from './components/QueueSection';
import RepositoryModal from './components/RepositoryModal';
import SettingsPage from './components/SettingsPage';
import TaskOutputModal from './components/TaskOutputModal';
import Updater from './components/Updater';
import WhyNotModal from './components/WhyNotModal';
import Task from './models/Task';
import ControlSectionState from './states/ControlSectionState';
import ExtensionManagerState from './states/ExtensionManagerState';
import QueueState from './states/QueueState';
import errorHandler from './utils/errorHandler';
import humanDuration from './utils/humanDuration';
import jumpToQueue from './utils/jumpToQueue';
export default {
'extension-manager/components/AuthMethodModal': AuthMethodModal,
'extension-manager/components/ConfigureAuth': ConfigureAuth,
'extension-manager/components/ConfigureComposer': ConfigureComposer,
'extension-manager/components/ConfigureJson': ConfigureJson,
'extension-manager/components/ControlSection': ControlSection,
'extension-manager/components/ExtensionItem': ExtensionItem,
'extension-manager/components/Installer': Installer,
'extension-manager/components/Label': Label,
'extension-manager/components/MajorUpdater': MajorUpdater,
'extension-manager/components/Pagination': Pagination,
'extension-manager/components/QueueSection': QueueSection,
'extension-manager/components/RepositoryModal': RepositoryModal,
'extension-manager/components/SettingsPage': SettingsPage,
'extension-manager/components/TaskOutputModal': TaskOutputModal,
'extension-manager/components/Updater': Updater,
'extension-manager/components/WhyNotModal': WhyNotModal,
'extension-manager/models/Task': Task,
'extension-manager/states/ControlSectionState': ControlSectionState,
'extension-manager/states/ExtensionManagerState': ExtensionManagerState,
'extension-manager/states/QueueState': QueueState,
'extension-manager/utils/errorHandler': errorHandler,
'extension-manager/utils/humanDuration': humanDuration,
'extension-manager/utils/jumpToQueue': jumpToQueue,
};

View File

@@ -1,88 +0,0 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Select from 'flarum/common/components/Select';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
onsubmit: (type: string, host: string, token: string) => void;
type?: string;
host?: string;
token?: string;
}
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type!: Stream<string>;
protected host!: Stream<string>;
protected token!: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.type = Stream(this.attrs.type || 'bearer');
this.host = Stream(this.attrs.host || '');
this.token = Stream(this.attrs.token || '');
}
className(): string {
return 'AuthMethodModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.host ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.auth_config.${context}_label`);
}
content(): Mithril.Children {
const types = {
'github-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.github-oauth'),
'gitlab-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-oauth'),
'gitlab-token': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-token'),
bearer: app.translator.trans('flarum-extension-manager.admin.auth_config.types.bearer'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.type_label')}</label>
<Select options={types} value={this.type()} onchange={this.type} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_label')}</label>
<input
className="FormControl"
bidi={this.host}
placeholder={app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_placeholder')}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.token_label')}</label>
<textarea
className="FormControl"
oninput={(e: InputEvent) => this.token((e.target as HTMLTextAreaElement).value)}
rows="6"
placeholder={
this.token().startsWith('unchanged:')
? extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.unchanged_token_placeholder'))
: ''
}
>
{this.token().startsWith('unchanged:') ? '' : this.token()}
</textarea>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.type(), this.host(), this.token());
this.hide();
}
}

View File

@@ -1,120 +0,0 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import AuthMethodModal from './AuthMethodModal';
import extractText from 'flarum/common/utils/extractText';
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
protected type = 'auth';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.auth_config.title');
}
className(): string {
return 'ConfigureAuth';
}
content(): Mithril.Children {
const authSettings = Object.keys(this.settings);
const hasAuthSettings =
authSettings.length &&
authSettings.every((type) => {
const data = this.settings[type]();
return Array.isArray(data) ? data.length : Object.keys(data).length;
});
return (
<div className="ExtensionManager-SettingsGroups-content">
{hasAuthSettings ? (
authSettings.map((type) => {
const hosts = this.settings[type]();
return (
<div className="Form-group">
<label>{app.translator.trans(`flarum-extension-manager.admin.auth_config.types.${type}`)}</label>
<div className="ConfigureAuth-hosts">
{Object.keys(hosts).map((host) => {
const data = hosts[host] as string | Record<string, string>;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon="fas fa-key"
onclick={() =>
app.modal.show(AuthMethodModal, {
type,
host,
token: data,
onsubmit: this.onchange.bind(this, host),
})
}
>
{host}
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.auth_config.delete_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.delete_confirmation')))) {
const newType = { ...this.setting(type)() };
delete newType[host];
if (Object.keys(newType).length) {
this.setting(type)(newType);
} else {
delete this.settings[type];
}
}
}}
/>
</div>
);
})}
</div>
</div>
);
})
) : (
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.auth_config.no_auth_methods_configured')}</span>
)}
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button
className="Button"
loading={this.loading}
onclick={() =>
app.modal.show(AuthMethodModal, {
onsubmit: this.onchange.bind(this, null),
})
}
>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_label')}
</Button>
);
return items;
}
onchange(oldHost: string | null, type: string, host: string, token: string) {
const data = { ...this.setting(type)() };
if (oldHost) {
delete data[oldHost];
}
data[host] = token;
this.setting(type)(data);
}
}

View File

@@ -1,115 +0,0 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
import RepositoryModal from './RepositoryModal';
export type Repository = {
type: 'composer' | 'vcs' | 'path';
url: string;
};
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
protected type = 'composer';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.composer.title');
}
className(): string {
return 'ConfigureComposer';
}
content(): Mithril.Children {
return (
<div className="ExtensionManager-SettingsGroups-content">
{this.attrs.buildSettingComponent.call(this, {
setting: 'minimum-stability',
label: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.label'),
help: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.help'),
type: 'select',
options: {
stable: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.stable'),
RC: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.rc'),
beta: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.beta'),
alpha: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.alpha'),
dev: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.dev'),
},
})}
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.label')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.composer.repositories.help')}</div>
<div className="ConfigureComposer-repositories">
{Object.keys(this.setting('repositories')() || {}).map((name) => {
const repository = this.setting('repositories')()[name] as Repository;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon={
{
composer: 'fas fa-cubes',
vcs: 'fas fa-code-branch',
path: 'fas fa-folder',
}[repository.type]
}
onclick={() =>
app.modal.show(RepositoryModal, {
name,
repository,
onsubmit: (repository: Repository, newName: string) => {
const repositories = this.setting('repositories')();
delete repositories[name];
this.setting('repositories')(repositories);
this.onchange(repository, newName);
},
})
}
>
{name} ({repository.type})
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_confirmation')))) {
const repositories = { ...this.setting('repositories')() };
delete repositories[name];
this.setting('repositories')(repositories);
}
}}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button className="Button" onclick={() => app.modal.show(RepositoryModal, { onsubmit: this.onchange.bind(this) })}>
{app.translator.trans('flarum-extension-manager.admin.composer.add_repository_label')}
</Button>
);
return items;
}
onchange(repository: Repository, name: string) {
this.setting('repositories')({
...this.setting('repositories')(),
[name]: repository,
});
}
}

View File

@@ -1,94 +0,0 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
import AdminPage from 'flarum/admin/components/AdminPage';
import type ItemList from 'flarum/common/utils/ItemList';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import classList from 'flarum/common/utils/classList';
export interface IConfigureJson extends ComponentAttrs {
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
}
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>> = {};
protected initialSettings: Record<string, any> | null = null;
protected loading: boolean = false;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.submit(true);
}
protected abstract type: string;
abstract title(): Mithril.Children;
abstract content(): Mithril.Children;
className(): string {
return '';
}
view(): Mithril.Children {
return (
<div className={classList('Form', this.className())}>
<label>{this.title()}</label>
{this.content()}
<div className="Form-group Form--controls">{this.submitButton()}</div>
</div>
);
}
submitButton(): Mithril.Children[] {
return [
<Button className="Button Button--primary" loading={this.loading} onclick={() => this.submit(false)} disabled={!this.isDirty()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>,
];
}
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> {
return AdminPage.prototype.customSettingComponents();
}
setting(key: string) {
return this.settings[key] ?? (this.settings[key] = Stream());
}
submit(readOnly: boolean) {
this.loading = true;
const configuration: any = {};
Object.keys(this.settings).forEach((key) => {
configuration[key] = this.settings[key]();
});
app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/extension-manager/composer',
body: {
type: this.type,
data: readOnly ? null : configuration,
},
})
.then(({ data }: any) => {
Object.keys(data).forEach((key) => {
this.settings[key] = Stream(data[key]);
});
this.initialSettings = Array.isArray(data) ? {} : data;
})
.finally(() => {
this.loading = false;
m.redraw();
});
}
isDirty() {
return JSON.stringify(this.initialSettings) !== JSON.stringify(this.settings);
}
}

View File

@@ -14,14 +14,14 @@ export default class ControlSection extends Component<ComponentAttrs> {
view() {
return (
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-extension-manager.writable_dirs'] ? (
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
<Installer />
<Updater />
@@ -29,7 +29,7 @@ export default class ControlSection extends Component<ComponentAttrs> {
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-extension-manager.admin.file_permissions')}
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
</Alert>
</div>
)}

View File

@@ -10,17 +10,11 @@ import { Extension } from 'flarum/admin/AdminApplication';
import { UpdatedPackage } from '../states/ControlSectionState';
import WhyNotModal from './WhyNotModal';
import Label from './Label';
import Dropdown from 'flarum/common/components/Dropdown';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
updates: UpdatedPackage;
onClickUpdate:
| CallableFunction
| {
soft: CallableFunction;
hard: CallableFunction;
};
onClickUpdate: CallableFunction;
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;
@@ -35,56 +29,43 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
return (
<div
className={classList({
'ExtensionManager-extension': true,
'ExtensionManager-extension--core': isCore,
'ExtensionManager-extension--danger': isDanger,
'PackageManager-extension': true,
'PackageManager-extension--core': isCore,
'PackageManager-extension--danger': isDanger,
})}
>
<div className="ExtensionManager-extension-icon ExtensionIcon" style={extension.icon}>
<div className="PackageManager-extension-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</div>
<div className="ExtensionManager-extension-info">
<div className="ExtensionManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="ExtensionManager-extension-version">
<span className="ExtensionManager-extension-version-current">{this.version(updates['version'])}</span>
<div className="PackageManager-extension-info">
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(updates['version'])}</span>
{latestVersion ? (
<Label className="ExtensionManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
<div className="ExtensionManager-extension-controls">
{onClickUpdate && typeof onClickUpdate === 'function' ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.update')}>
<div className="PackageManager-extension-controls">
{onClickUpdate ? (
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.update')}>
<Button
icon="fas fa-arrow-alt-circle-up"
className="Button Button--icon Button--flat"
onclick={onClickUpdate}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.update')}
/>
</Tooltip>
) : onClickUpdate ? (
<Dropdown
buttonClassName="Button Button--icon Button--flat"
icon="fas fa-arrow-alt-circle-up"
label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.soft}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_soft_label')}
</Button>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.hard} disabled={!updates['direct-dependency']}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_hard_label')}
</Button>
</Dropdown>
) : null}
{whyNotWarning ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}>
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}>
<Button
icon="fas fa-exclamation-circle"
className="Button Button--icon Button--flat Button--danger"
onclick={() => app.modal.show(WhyNotModal, { package: extension.name })}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}
/>
</Tooltip>
) : null}
@@ -94,6 +75,6 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
}
version(v: string): string {
return v.charAt(0) === 'v' ? v.substring(1) : v;
return 'v' + v.replace('v', '');
}
}

View File

@@ -3,6 +3,11 @@ import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
export interface InstallerAttrs extends ComponentAttrs {}
@@ -19,13 +24,11 @@ export default class Installer extends Component<InstallerAttrs> {
view(): Mithril.Children {
return (
<div className="Form-group ExtensionManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-extension-manager.admin.extensions.install')}</label>
<div className="Form-group PackageManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-package-manager.admin.extensions.install')}</label>
<p className="helpText">
{app.translator.trans('flarum-extension-manager.admin.extensions.install_help', {
{app.translator.trans('flarum-package-manager.admin.extensions.install_help', {
extiverse: <a href="https://extiverse.com">extiverse.com</a>,
semantic_link: <a href="https://devhints.io/semver" />,
code: <code />,
})}
</p>
<div className="FormControl-container">
@@ -34,10 +37,10 @@ export default class Installer extends Component<InstallerAttrs> {
className="Button"
icon="fas fa-download"
onclick={this.onsubmit.bind(this)}
loading={app.extensionManager.control.isLoading('extension-install')}
disabled={app.extensionManager.control.hasOperationRunning()}
loading={app.packageManager.control.isLoading('extension-install')}
disabled={app.packageManager.control.isLoadingOtherThan('extension-install')}
>
{app.translator.trans('flarum-extension-manager.admin.extensions.proceed')}
{app.translator.trans('flarum-package-manager.admin.extensions.proceed')}
</Button>
</div>
</div>
@@ -51,6 +54,35 @@ export default class Installer extends Component<InstallerAttrs> {
}
onsubmit(): void {
app.extensionManager.control.requirePackage(this.data());
app.packageManager.control.setLoading('extension-install');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse & { id: number }>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions`,
body: {
data: this.data(),
},
})
.then((response) => {
if (response.processing) {
jumpToQueue();
} else {
const extensionId = response.id;
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_install', { extension: extensionId })
);
window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`;
window.location.reload();
}
})
.catch(errorHandler)
.finally(() => {
app.packageManager.control.setLoading(null);
app.modal.close();
m.redraw();
});
}
}

View File

@@ -3,12 +3,16 @@ import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import Tooltip from 'flarum/common/components/Tooltip';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import Alert from 'flarum/common/components/Alert';
import RequestError from 'flarum/common/utils/RequestError';
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
import errorHandler from '../utils/errorHandler';
import WhyNotModal from './WhyNotModal';
import ExtensionItem from './ExtensionItem';
import classList from 'flarum/common/utils/classList';
import { AsyncBackendResponse } from '../shims';
import jumpToQueue from '../utils/jumpToQueue';
export interface MajorUpdaterAttrs extends ComponentAttrs {
coreUpdate: UpdatedPackage;
@@ -29,39 +33,32 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
view(): Mithril.Children {
// @todo move Form-group--danger class to core for reuse
return (
<div
className={classList('Form-group Form-group--danger ExtensionManager-majorUpdate', {
'ExtensionManager-majorUpdate--failed': this.updateState.status === 'failure',
'ExtensionManager-majorUpdate--incompatibleExtensions': this.updateState.incompatibleExtensions.length,
})}
>
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-extension-manager/flarum.svg'} />
<label>
{app.translator.trans('flarum-extension-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}
</label>
<p className="helpText">{app.translator.trans('flarum-extension-manager.admin.major_updater.description')}</p>
<div className="ExtensionManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run_help')}>
<div className="Form-group Form-group--danger PackageManager-majorUpdate">
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-package-manager/flarum.svg'} />
<label>{app.translator.trans('flarum-package-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.major_updater.description')}</p>
<div className="PackageManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-package-manager.admin.major_updater.dry_run_help')}>
<Button
className="Button"
icon="fas fa-vial"
onclick={this.update.bind(this, true)}
disabled={app.extensionManager.control.hasOperationRunning()}
disabled={app.packageManager.control.isLoadingOtherThan('major-update-dry-run')}
>
{app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run')}
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
</Button>
</Tooltip>
<Button
className="Button Button--danger"
icon="fas fa-play"
onclick={this.update.bind(this, false)}
disabled={app.extensionManager.control.hasOperationRunning()}
disabled={app.packageManager.control.isLoadingOtherThan('major-update')}
>
{app.translator.trans('flarum-extension-manager.admin.major_updater.update')}
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
</Button>
</div>
{this.updateState.incompatibleExtensions.length ? (
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
<div className="PackageManager-majorUpdate-incompatibleExtensions PackageManager-extensions-grid">
{this.updateState.incompatibleExtensions.map((extension: string) => (
<ExtensionItem
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
@@ -75,20 +72,20 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
{this.updateState.status === 'failure' ? (
<Alert
type="error"
className="ExtensionManager-majorUpdate-failure"
className="PackageManager-majorUpdate-failure"
dismissible={false}
controls={[
<Button
className="Button Button--text ExtensionManager-majorUpdate-failure-details"
className="Button Button--text PackageManager-majorUpdate-failure-details"
icon="fas fa-question-circle"
onclick={() => app.modal.show(WhyNotModal, { package: 'flarum/core' })}
>
{app.translator.trans('flarum-extension-manager.admin.major_updater.failure.why')}
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.why')}
</Button>,
]}
>
<p className="ExtensionManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-extension-manager.admin.major_updater.failure.desc')}
<p className="PackageManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.desc')}
</p>
</Alert>
) : null}
@@ -97,6 +94,34 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
}
update(dryRun: boolean) {
app.extensionManager.control.majorUpdate({ dryRun });
app.packageManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`,
body: {
data: { dryRun },
},
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
window.location.reload();
}
})
.catch(errorHandler)
.catch((e: RequestError) => {
app.modal.close();
this.updateState.status = 'failure';
this.updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
})
.finally(() => {
app.packageManager.control.setLoading(null);
m.redraw();
});
}
}

View File

@@ -15,7 +15,7 @@ export default class Pagination extends Component<PaginationAttrs> {
return (
<nav className="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev() || app.extensionManager.control.isLoading()}
disabled={!this.attrs.list.hasPrev()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
@@ -28,7 +28,7 @@ export default class Pagination extends Component<PaginationAttrs> {
})}
</span>
<Button
disabled={!this.attrs.list.hasNext() || app.extensionManager.control.isLoading()}
disabled={!this.attrs.list.hasNext()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"

View File

@@ -8,7 +8,6 @@ import { Extension } from 'flarum/admin/AdminApplication';
import icon from 'flarum/common/helpers/icon';
import ItemList from 'flarum/common/utils/ItemList';
import extractText from 'flarum/common/utils/extractText';
import Link from 'flarum/common/components/Link';
import Label from './Label';
import TaskOutputModal from './TaskOutputModal';
@@ -25,21 +24,20 @@ export default class QueueSection extends Component<{}> {
oninit(vnode: Mithril.Vnode<{}, this>) {
super.oninit(vnode);
app.extensionManager.queue.load();
app.packageManager.queue.load();
}
view() {
return (
<section id="ExtensionManager-queueSection" className="ExtensionPage-permissions ExtensionManager-queueSection">
<div className="ExtensionPage-permissions-header ExtensionManager-queueSection-header">
<section id="PackageManager-queueSection" className="ExtensionPage-permissions PackageManager-queueSection">
<div className="ExtensionPage-permissions-header PackageManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.queue.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}</h2>
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.extensionManager.queue.load()}
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.refresh')}
disabled={app.extensionManager.control.isLoading()}
onclick={() => app.packageManager.queue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
/>
</div>
</div>
@@ -54,12 +52,12 @@ export default class QueueSection extends Component<{}> {
items.add(
'operation',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.operation')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')),
content: (task) => (
<div className="ExtensionManager-queueTable-operation">
<span className="ExtensionManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="ExtensionManager-queueTable-operation-name">
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${task.operation()}`)}
<div className="PackageManager-queueTable-operation">
<span className="PackageManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="PackageManager-queueTable-operation-name">
{app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)}
</span>
</div>
),
@@ -70,20 +68,20 @@ export default class QueueSection extends Component<{}> {
items.add(
'package',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.package')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.package')),
content: (task) => {
const extension: Extension | null = app.data.extensions[task.package()?.replace(/(\/flarum-|\/flarum-ext-|\/)/g, '-')];
return extension ? (
<Link className="ExtensionManager-queueTable-package" href={app.route('extension', { id: extension.id })}>
<div className="ExtensionManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
<div className="PackageManager-queueTable-package">
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{!!extension.icon && icon(extension.icon.name)}
</div>
<div className="ExtensionManager-queueTable-package-details">
<span className="ExtensionManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="ExtensionManager-queueTable-package-name">{task.package()}</span>
<div className="PackageManager-queueTable-package-details">
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="PackageManager-queueTable-package-name">{task.package()}</span>
</div>
</Link>
</div>
) : (
task.package()
);
@@ -95,17 +93,14 @@ export default class QueueSection extends Component<{}> {
items.add(
'status',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.status')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')),
content: (task) => (
<>
<Label
className="ExtensionManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
{['pending', 'running'].includes(task.status()) && <LoadingIndicator size="small" display="inline" />}
</>
<Label
className="PackageManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-package-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
),
},
70
@@ -114,10 +109,10 @@ export default class QueueSection extends Component<{}> {
items.add(
'elapsedTime',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.elapsed_time')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')),
content: (task) =>
!task.startedAt() || !task.finishedAt() ? (
app.translator.trans('flarum-extension-manager.admin.sections.queue.task_just_started')
!task.startedAt() ? (
app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started')
) : (
<Tooltip text={`${dayjs(task.startedAt()).format('LL LTS')} ${dayjs(task.finishedAt()).format('LL LTS')}`}>
<span>{humanDuration(task.startedAt(), task.finishedAt())}</span>
@@ -130,7 +125,7 @@ export default class QueueSection extends Component<{}> {
items.add(
'memoryUsed',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.peak_memory_used')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')),
content: (task) => <span>{task.peakMemoryUsed()}</span>,
},
60
@@ -139,16 +134,15 @@ export default class QueueSection extends Component<{}> {
items.add(
'details',
{
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.details')),
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')),
content: (task) => (
<Button
className="Button Button--icon Table-controls-item"
icon="fas fa-file-alt"
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.details')}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')}
// @todo fix in core
// @ts-ignore
onclick={() => app.modal.show(TaskOutputModal, { task })}
disabled={['pending', 'running'].includes(task.status())}
/>
),
className: 'Table-controls',
@@ -160,21 +154,21 @@ export default class QueueSection extends Component<{}> {
}
queueTable() {
const tasks = app.extensionManager.queue.getItems();
const tasks = app.packageManager.queue.getItems();
if (!tasks) {
return <LoadingIndicator />;
}
if (tasks && !tasks.length) {
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-extension-manager.admin.sections.queue.none')}</h3>;
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-package-manager.admin.sections.queue.none')}</h3>;
}
const columns = this.columns();
return (
<>
<table className="Table ExtensionManager-queueTable">
<table className="Table PackageManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
@@ -199,7 +193,7 @@ export default class QueueSection extends Component<{}> {
</tbody>
</table>
<Pagination list={app.extensionManager.queue} />
<Pagination list={app.packageManager.queue} />
</>
);
}

View File

@@ -1,77 +0,0 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Select from 'flarum/common/components/Select';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import { type Repository } from './ConfigureComposer';
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
onsubmit: (repository: Repository, key: string) => void;
name?: string;
repository?: Repository;
}
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
protected name!: Stream<string>;
protected repository!: Stream<Repository>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.name = Stream(this.attrs.name || '');
this.repository = Stream(this.attrs.repository || { type: 'composer', url: '' });
}
className(): string {
return 'RepositoryModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.repository ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.composer.${context}_repository_label`);
}
content(): Mithril.Children {
const types = {
composer: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.composer'),
vcs: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.vcs'),
path: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.path'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.name_label')}</label>
<input className="FormControl" bidi={this.name} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.type_label')}</label>
<Select
options={types}
value={this.repository().type}
onchange={(value: 'composer' | 'vcs' | 'path') => this.repository({ ...this.repository(), type: value })}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.url')}</label>
<input
className="FormControl"
onchange={(e: Event) => this.repository({ ...this.repository(), url: (e.target as HTMLInputElement).value })}
value={this.repository().url}
/>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.repository(), this.name());
this.hide();
}
}

Some files were not shown because too many files have changed in this diff Show More