1
0
mirror of https://github.com/flarum/core.git synced 2025-08-16 13:24:11 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Sami Mazouz
13f997d784 feat: ItemList component 2023-11-10 14:15:53 +01:00
692 changed files with 6025 additions and 10074 deletions

View File

@@ -31,8 +31,7 @@ on:
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
type: string
required: false
# Keep PHP versions synced with build-install-packages.yml
default: '["8.1", "8.2", "8.3"]'
default: '["8.1", "8.2"]'
php_extensions:
description: PHP extensions to install.
@@ -52,16 +51,10 @@ on:
required: false
default: error_reporting=E_ALL
secrets:
composer_auth:
description: The Composer auth tokens to use for private packages.
required: false
env:
COMPOSER_ROOT_VERSION: dev-main
# `inputs.composer_directory` defaults to `inputs.backend_directory`
FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp
COMPOSER_AUTH: ${{ secrets.composer_auth }}
jobs:
test:

View File

@@ -90,15 +90,11 @@ on:
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 }}
jobs:
build:

View File

@@ -1,29 +0,0 @@
name: Build Install Packages
on:
release:
types: [released]
env:
VERSION: ${{ github.event.release.tag_name }}
PHP_VERSIONS: '8.1 8.2 8.3'
INSTALL_PACKAGES_INPUTS: '{ "flarum_version": "{0}", "php_versions": "{1}" }'
jobs:
delay:
name: Wait for packagist to publish new packages
runs-on: ubuntu-latest
steps:
- run: sleep 30m
build:
name: Build Installation Packages
runs-on: ubuntu-latest
steps:
- name: Trigger build in flarum/installation-packages
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Build Flarum Install Packages
repo: flarum/installation-packages
token: ${{ secrets.PACKAGES_BUILD_TOKEN }}
inputs: ${{ format(env.INSTALL_PACKAGES_INPUTS, env.VERSION, env.PHP_VERSIONS) }}

View File

@@ -38,4 +38,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",
@@ -70,7 +70,7 @@
"Flarum\\Lock\\Tests\\": "extensions/lock/tests",
"Flarum\\Mentions\\Tests\\": "extensions/mentions/tests",
"Flarum\\Nicknames\\Tests\\": "extensions/nicknames/tests",
"Flarum\\ExtensionManager\\Tests\\": "extensions/package-manager/tests",
"Flarum\\PackageManager\\Tests\\": "extensions/package-manager/tests",
"Flarum\\Pusher\\Tests\\": "extensions/pusher/tests",
"Flarum\\Statistics\\Tests\\": "extensions/statistics/tests",
"Flarum\\Sticky\\Tests\\": "extensions/sticky/tests",
@@ -94,7 +94,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",
@@ -112,7 +112,6 @@
"dflydev/fig-cookies": "^3.0",
"doctrine/dbal": "^3.6.2",
"dragonmantank/cron-expression": "^3.3",
"fakerphp/faker": "^1.9.1",
"franzl/whoops-middleware": "2.0",
"guzzlehttp/guzzle": "*",
"illuminate/bus": "^10.0",
@@ -131,7 +130,7 @@
"illuminate/support": "^10.0",
"illuminate/validation": "^10.0",
"illuminate/view": "^10.0",
"intervention/image": "^3.2",
"intervention/image": "^2.7.2",
"jenssegers/agent": "^2.6",
"laminas/laminas-diactoros": "^3.0",
"laminas/laminas-httphandlerrunner": "^2.6",
@@ -169,7 +168,7 @@
"mockery/mockery": "^1.5",
"phpunit/phpunit": "^9.0",
"phpstan/phpstan": "^1.10.0",
"larastan/larastan": "^2.7",
"nunomaduro/larastan": "^2.6",
"symfony/var-dumper": "^6.3"
},
"config": {

View File

@@ -47,7 +47,7 @@ class Akismet
$client = new Client();
return $client->request('POST', "$this->apiUrl/$type", [
'headers' => [
'headers' => [
'User-Agent' => "Flarum/$this->flarumVersion | Akismet/$this->extensionVersion",
],
'form_params' => $this->params,

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
@@ -21,42 +20,23 @@ class ApproveContent
$events->listen(Saving::class, $this->approvePost(...));
}
/**
* @throws PermissionDeniedException
*/
public function approvePost(Saving $event): void
{
$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));
}
}
}

View File

@@ -10,23 +10,19 @@
namespace Flarum\Approval\Tests\integration;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\User\User;
trait InteractsWithUnapprovedContent
{
protected function prepareUnapprovedDatabaseContent()
{
$this->prepareDatabase([
User::class => [
'users' => [
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
$this->normalUser(),
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
@@ -35,7 +31,7 @@ trait InteractsWithUnapprovedContent
['id' => 6, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 6, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
['id' => 7, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 7, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
],
Post::class => [
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 2, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
['id' => 3, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
@@ -49,7 +45,7 @@ trait InteractsWithUnapprovedContent
['id' => 10, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 4],
['id' => 11, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 1, 'is_approved' => 0, 'number' => 5],
],
Group::class => [
'groups' => [
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0]
],
'group_user' => [

View File

@@ -50,7 +50,7 @@
padding: 15px 15px;
.scrolled & {
box-shadow: 0 2px 6px var(--shadow-color);
.box-shadow(0 2px 6px var(--shadow-color));
}
}

View File

@@ -0,0 +1,6 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

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,47 +1,47 @@
import { extend } from 'flarum/common/extend';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
import Tooltip from 'flarum/common/components/Tooltip';
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getEmojiIconCode from './helpers/getEmojiIconCode';
import cdn from './cdn';
export default function addComposerAutocomplete() {
const $container = $('<div class="ComposerBody-emojiDropdownContainer"></div>');
const dropdown = new AutocompleteDropdown();
let emojiMap = null;
extend('flarum/common/components/TextEditor', 'oninit', function () {
this._loaders.push(async () => await import('./emojiMap').then((m) => (emojiMap = m.default)));
// prettier-ignore
this.commonEmoji = [
'😀', '😁', '😂', '😃', '😄', '😅', '😆', '😇', '😈', '😉', '😊', '😋', '😌', '😍', '😎', '😏', '😐️', '😑', '😒',
'😓', '😔', '😕', '😖', '😗', '😘', '😙', '😚', '😛', '😜', '😝', '😞', '😟', '😠', '😡', '😢', '😣', '😤', '😥',
'😦', '😧', '😨', '😩', '😪', '😫', '😬', '😭', '😮', '😮‍💨', '😯', '😰', '😱', '😲', '😳', '😴', '😵', '😵‍💫',
'😶', '😶‍🌫️', '😷', '😸', '😹', '😺', '😻', '😼', '😽', '😾', '😿', '🙀', '🙁', '🙂', '🙃', '🙄',
];
});
extend('flarum/common/components/TextEditor', 'onbuild', function () {
this.emojiDropdown = new AutocompleteDropdown();
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-emojiWrapper"></div>');
this.navigator = new KeyboardNavigatable();
this.navigator
.when(() => this.emojiDropdown.active)
.onUp(() => this.emojiDropdown.navigate(-1))
.onDown(() => this.emojiDropdown.navigate(1))
.onSelect(this.emojiDropdown.complete.bind(this.emojiDropdown))
.onCancel(this.emojiDropdown.hide.bind(this.emojiDropdown))
.when(() => dropdown.active)
.onUp(() => dropdown.navigate(-1))
.onDown(() => dropdown.navigate(1))
.onSelect(dropdown.complete.bind(dropdown))
.onCancel(dropdown.hide.bind(dropdown))
.bindTo($editor);
$editor.after($('<div class="ComposerBody-emojiDropdownContainer"></div>'));
$editor.after($container);
});
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
const emojiKeys = Object.keys(emojiMap);
const autocompleteReader = new AutocompleteReader(':');
let relEmojiStart;
let absEmojiStart;
let typed;
const applySuggestion = (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(absEmojiStart - 1, replacement + ' ');
dropdown.hide();
};
params.inputListeners.push(() => {
const selection = this.attrs.composer.editor.getSelectionRange();
@@ -50,34 +50,42 @@ export default function addComposerAutocomplete() {
if (selection[1] - cursor > 0) return;
// Search backwards from the cursor for an ':' symbol. If we find
// one and followed by a whitespace, we will want to show the
// autocomplete dropdown!
const lastChunk = this.attrs.composer.editor.getLastNChars(15);
const autocompleting = autocompleteReader.check(lastChunk, cursor, /[a-z0-9]|\+|\-|_|\:/);
absEmojiStart = 0;
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
// check what user typed, emoji names only contains alphanumeric,
// underline, '+' and '-'
if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break;
// make sure ':' preceded by a whitespace or newline
if (character === ':' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relEmojiStart = i + 1;
absEmojiStart = cursor - lastChunk.length + i + 1;
break;
}
}
this.emojiDropdown.hide();
this.emojiDropdown.active = false;
dropdown.hide();
dropdown.active = false;
if (autocompleting) {
const typed = autocompleting.typed;
const emojiDropdown = this.emojiDropdown;
const applySuggestion = (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
this.emojiDropdown.hide();
};
if (absEmojiStart) {
typed = lastChunk.substring(relEmojiStart).toLowerCase();
const makeSuggestion = function ({ emoji, name, code }) {
return (
<Tooltip text={name}>
<button
key={emoji}
onclick={() => applySuggestion(emoji)}
onmouseenter={function () {
emojiDropdown.setIndex($(this).parent().index() - 1);
}}
>
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} title={name} />
</button>
</Tooltip>
<button
key={emoji}
onclick={() => applySuggestion(emoji)}
onmouseenter={function () {
dropdown.setIndex($(this).parent().index() - 1);
}}
>
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
{name}
</button>
);
};
@@ -91,7 +99,7 @@ export default function addComposerAutocomplete() {
};
const regTyped = fuzzyRegexp(typed);
let maxSuggestions = 40;
let maxSuggestions = 7;
const findMatchingEmojis = (matcher) => {
for (let i = 0; i < emojiKeys.length && maxSuggestions > 0; i++) {
@@ -100,7 +108,7 @@ export default function addComposerAutocomplete() {
if (similarEmoji.indexOf(curEmoji) === -1) {
const names = emojiMap[curEmoji];
for (let name of names) {
if (matcher(name, curEmoji)) {
if (matcher(name)) {
--maxSuggestions;
similarEmoji.push(curEmoji);
break;
@@ -111,17 +119,10 @@ export default function addComposerAutocomplete() {
};
// First, try to find all emojis starting with the given string
findMatchingEmojis((emojiName, emoji) => {
// If no input is provided yet, match the most common emojis.
if (!typed) {
return this.commonEmoji?.includes(emoji);
}
return emojiName.indexOf(typed) === 0;
});
findMatchingEmojis((emoji) => emoji.indexOf(typed) === 0);
// If there are still suggestions left, try for some fuzzy matches
findMatchingEmojis((emojiName) => regTyped.test(emojiName));
findMatchingEmojis((emoji) => regTyped.test(emoji));
const suggestions = similarEmoji
.map((emoji) => ({
@@ -132,14 +133,14 @@ export default function addComposerAutocomplete() {
.map(makeSuggestion);
if (suggestions.length) {
this.emojiDropdown.items = suggestions;
m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());
dropdown.items = suggestions;
m.render($container[0], dropdown.render());
this.emojiDropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
const width = this.emojiDropdown.$().outerWidth();
const height = this.emojiDropdown.$().outerHeight();
const parent = this.emojiDropdown.$().offsetParent();
dropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absEmojiStart);
const width = dropdown.$().outerWidth();
const height = dropdown.$().outerHeight();
const parent = dropdown.$().offsetParent();
let left = coordinates.left;
let top = coordinates.top + 15;
@@ -155,15 +156,15 @@ export default function addComposerAutocomplete() {
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
left = Math.max(-parent.offset().left, left);
this.emojiDropdown.show(left, top);
dropdown.show(left, top);
}
};
buildSuggestions();
this.emojiDropdown.setIndex(0);
this.emojiDropdown.$().scrollTop(0);
this.emojiDropdown.active = true;
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
dropdown.active = true;
}
});
});

View File

@@ -7,28 +7,29 @@ img.emoji {
.EmojiDropdown {
max-width: 500px;
max-height: 200px;
overflow: visible;
overflow: auto;
position: absolute;
padding: 8px;
margin: 5px 0 !important;
> li {
display: inline-block;
> li > button {
color: var(--text-color);
font-weight: bold;
padding-top: 6px;
padding-bottom: 6px;
padding-left: 45px;
> button {
color: var(--text-color);
font-weight: bold;
padding: 8px;
border-radius: var(--border-radius);
.emoji {
float: left;
margin-left: -30px;
}
}
> .Dropdown-header {
display: block;
.Dropdown-header {
color: var(--muted-more-color);
text-transform: none;
font-weight: normal;
padding: 4px 8px;
margin: 0 0 4px 0;
padding-bottom: 5px;
font-size: 11px;
}
}

View File

@@ -8,5 +8,4 @@ export interface IFlagListAttrs extends ComponentAttrs {
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
view(): JSX.Element;
content(state: FlagListState): JSX.Element[][] | null;
}

View File

@@ -1,5 +1,5 @@
/// <reference types="flarum/@types/translator-icu-rich" />
export default class FlagPostModal extends FormModal<import("flarum/common/components/FormModal").IFormModalAttrs, undefined> {
export default class FlagPostModal extends Modal<import("flarum/common/components/Modal").IInternalModalAttrs, undefined> {
constructor();
oninit(vnode: any): void;
success: boolean | undefined;
@@ -10,6 +10,6 @@ export default class FlagPostModal extends FormModal<import("flarum/common/compo
flagReasons(): ItemList<any>;
onsubmit(e: any): void;
}
import FormModal from "flarum/common/components/FormModal";
import Modal from "flarum/common/components/Modal";
import Stream from "flarum/common/utils/Stream";
import ItemList from "flarum/common/utils/ItemList";

View File

@@ -1,13 +1,15 @@
import type ForumApplication from 'flarum/forum/ForumApplication';
import type Flag from '../models/Flag';
import PaginatedListState from 'flarum/common/states/PaginatedListState';
export default class FlagListState extends PaginatedListState<Flag> {
import type Post from 'flarum/common/models/Post';
export default class FlagListState {
app: ForumApplication;
loading: boolean;
cache: Flag[] | null;
index: Post | false | null;
constructor(app: ForumApplication);
get type(): string;
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load(): Promise<void>;
load(): void;
}

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

@@ -54,7 +54,7 @@ export default function () {
const controls = PostControls.destructiveControls(this.attrs.post);
Object.keys(controls.toObject()).forEach((k) => {
Object.keys(controls.items).forEach((k) => {
const attrs = controls.get(k).attrs;
attrs.className = 'Button';

View File

@@ -8,8 +8,6 @@ import HeaderListItem from 'flarum/forum/components/HeaderListItem';
import type Mithril from 'mithril';
import type Post from 'flarum/common/models/Post';
import type FlagListState from '../states/FlagListState';
import type Flag from '../models/Flag';
import { Page } from 'flarum/common/states/PaginatedListState';
export interface IFlagListAttrs extends ComponentAttrs {
state: FlagListState;
@@ -18,55 +16,49 @@ export interface IFlagListAttrs extends ComponentAttrs {
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
const state = this.attrs.state;
const flags = this.state.cache || [];
return (
<HeaderList
className="FlagList"
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
hasItems={state.hasItems()}
loading={state.isLoading()}
hasItems={flags.length}
loading={this.state.loading}
emptyText={app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}
loadMore={() => state.hasNext() && !state.isLoadingNext() && state.loadNext()}
>
<ul className="HeaderListGroup-content">{this.content(state)}</ul>
<ul className="HeaderListGroup-content">
{!this.state.loading &&
flags.map((flag) => {
const post = flag.post() as Post;
return (
<li>
<HeaderListItem
className="Flag"
avatar={<Avatar user={post.user() || null} />}
icon="fas fa-flag"
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
excerpt={post.contentPlain()}
datetime={flag.createdAt()}
href={app.route.post(post)}
onclick={(e: MouseEvent) => {
app.flags.index = post;
e.redraw = false;
}}
/>
</li>
);
})}
</ul>
</HeaderList>
);
}
content(state: FlagListState) {
if (!state.isLoading() && state.hasItems()) {
return state.getPages().map((page: Page<Flag>) => {
return page.items.map((flag: Flag) => {
const post = flag.post() as Post;
return (
<li>
<HeaderListItem
className="Flag"
avatar={<Avatar user={post.user() || null} />}
icon="fas fa-flag"
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
excerpt={post.contentPlain()}
datetime={flag.createdAt()}
href={app.route.post(post)}
onclick={(e: MouseEvent) => {
e.redraw = false;
}}
/>
</li>
);
});
});
}
return null;
}
}

View File

@@ -1,5 +1,5 @@
import app from 'flarum/forum/app';
import FormModal from 'flarum/common/components/FormModal';
import Modal from 'flarum/common/components/Modal';
import Form from 'flarum/common/components/Form';
import Button from 'flarum/common/components/Button';
@@ -7,7 +7,7 @@ import Stream from 'flarum/common/utils/Stream';
import withAttr from 'flarum/common/utils/withAttr';
import ItemList from 'flarum/common/utils/ItemList';
export default class FlagPostModal extends FormModal {
export default class FlagPostModal extends Modal {
oninit(vnode) {
super.oninit(vnode);

View File

@@ -25,7 +25,7 @@ export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFl
}
getUnreadCount() {
return app.forum.attribute<number>('flagCount');
return app.flags.cache ? app.flags.cache.length : app.forum.attribute<number>('flagCount');
}
getNewCount() {

View File

@@ -1,33 +1,39 @@
import type ForumApplication from 'flarum/forum/ForumApplication';
import type Flag from '../models/Flag';
import PaginatedListState from 'flarum/common/states/PaginatedListState';
import type Post from 'flarum/common/models/Post';
export default class FlagListState extends PaginatedListState<Flag> {
export default class FlagListState {
public app: ForumApplication;
public loading = false;
public cache: Flag[] | null = null;
public index: Post | false | null = null;
constructor(app: ForumApplication) {
super({}, 1, null);
this.app = app;
}
get type(): string {
return 'flags';
}
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load(): Promise<void> {
if (this.app.session.user?.attribute<number>('newFlagCount')) {
this.pages = [];
this.location = { page: 1 };
load() {
if (this.cache && !this.app.session.user!.attribute<number>('newFlagCount')) {
return;
}
if (this.pages.length > 0) {
return Promise.resolve();
}
this.loading = true;
m.redraw();
return super.loadNext();
this.app.store
.find<Flag[]>('flags')
.then((flags) => {
this.app.session.user!.pushAttributes({ newFlagCount: 0 });
this.cache = flags.sort((a, b) => b.createdAt()!.getTime() - a.createdAt()!.getTime());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@@ -32,7 +32,6 @@ class AddCanFlagAttribute
// If $actor is the post author, check to see if the setting is enabled
return (bool) $this->settings->get('flarum-flags.can_flag_own');
}
// $actor is not the post author
return true;
}

View File

@@ -14,7 +14,6 @@ use Flarum\Api\Controller\AbstractListController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Flag;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -29,52 +28,26 @@ class ListFlagsController extends AbstractListController
'post.discussion'
];
public function __construct(
protected UrlGenerator $url
) {
}
protected function data(ServerRequestInterface $request, Document $document): iterable
{
$actor = RequestUtil::getActor($request);
$include = $this->extractInclude($request);
$actor->assertRegistered();
$actor->read_flags_at = Carbon::now();
$actor->save();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
$flags = Flag::whereVisibleTo($actor)
->latest('flags.created_at')
->groupBy('post_id')
->get();
if (in_array('post.user', $include)) {
$include[] = 'post.user.groups';
}
$flags = Flag::whereVisibleTo($actor)
->latest('flags.created_at')
->groupBy('post_id')
->limit($limit + 1)
->offset($offset)
->get();
$this->loadRelations($flags, $include, $request);
$flags = $flags->all();
$areMoreResults = false;
if (count($flags) > $limit) {
array_pop($flags);
$areMoreResults = true;
}
$this->addPaginationData(
$document,
$request,
$this->url->to('api')->route('flags.index'),
$areMoreResults ? null : 0
);
$this->loadRelations($flags, $include);
return $flags;
}

View File

@@ -29,10 +29,10 @@ class FlagSerializer extends AbstractSerializer
}
return [
'type' => $model->type,
'reason' => $model->reason,
'type' => $model->type,
'reason' => $model->reason,
'reasonDetail' => $model->reason_detail,
'createdAt' => $this->formatDate($model->created_at),
'createdAt' => $this->formatDate($model->created_at),
];
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Flags\Command;
use Flarum\Flags\Event\Deleting;
use Flarum\Flags\Event\FlagsWillBeDeleted;
use Flarum\Post\Post;
use Flarum\Post\PostRepository;
use Illuminate\Events\Dispatcher;
@@ -30,6 +31,9 @@ class DeleteFlagsHandler
$actor->assertCan('viewFlags', $post->discussion);
// Deprecated, removed v2.0
$this->events->dispatch(new FlagsWillBeDeleted($post, $actor, $command->data));
foreach ($post->flags as $flag) {
$this->events->dispatch(new Deleting($flag, $actor, $command->data));
}

View File

@@ -0,0 +1,27 @@
<?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\Flags\Event;
use Flarum\Post\Post;
use Flarum\User\User;
/**
* @deprecated v2.0
* Listen for Flarum\Flags\Event\Deleting instead
*/
class FlagsWillBeDeleted
{
public function __construct(
public Post $post,
public User $actor,
public array $data = []
) {
}
}

View File

@@ -9,12 +9,9 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use Illuminate\Support\Arr;
class ListTest extends TestCase
@@ -31,7 +28,7 @@ class ListTest extends TestCase
$this->extension('flarum-flags');
$this->prepareDatabase([
User::class => [
'users' => [
$this->normalUser(),
[
'id' => 3,
@@ -47,10 +44,10 @@ class ListTest extends TestCase
'group_permission' => [
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
],
Discussion::class => [
'discussions' => [
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
],
Post::class => [
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],

View File

@@ -9,13 +9,9 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\Post;
use Flarum\Tags\Tag;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use Illuminate\Support\Arr;
class ListWithTagsTest extends TestCase
@@ -33,13 +29,13 @@ class ListWithTagsTest extends TestCase
$this->extension('flarum-tags');
$this->prepareDatabase([
Tag::class => [
'tags' => [
['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null],
['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
],
User::class => [
'users' => [
$this->normalUser(),
[
'id' => 3,
@@ -59,7 +55,7 @@ class ListWithTagsTest extends TestCase
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
],
Discussion::class => [
'discussions' => [
['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1],
['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1],
['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1],
@@ -72,7 +68,7 @@ class ListWithTagsTest extends TestCase
['discussion_id' => 4, 'tag_id' => 4],
['discussion_id' => 5, 'tag_id' => 1],
],
Post::class => [
'posts' => [
// From regular ListTest
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],

View File

@@ -19,10 +19,9 @@ use Flarum\Likes\Event\PostWasUnliked;
use Flarum\Likes\Notification\PostLikedBlueprint;
use Flarum\Likes\Query\LikedByFilter;
use Flarum\Likes\Query\LikedFilter;
use Flarum\Post\Filter\PostSearcher;
use Flarum\Post\Filter\PostFilterer;
use Flarum\Post\Post;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\User\Search\UserSearcher;
use Flarum\User\Filter\UserFilterer;
use Flarum\User\User;
return [
@@ -77,9 +76,11 @@ return [
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
->subscribe(Listener\SaveLikesToDatabase::class),
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->addFilter(PostSearcher::class, LikedByFilter::class)
->addFilter(UserSearcher::class, LikedFilter::class),
(new Extend\Filter(PostFilterer::class))
->addFilter(LikedByFilter::class),
(new Extend\Filter(UserFilterer::class))
->addFilter(LikedFilter::class),
(new Extend\Settings())
->default('flarum-likes.like_own_post', true),

View File

@@ -9,14 +9,10 @@
namespace Flarum\Likes\Query;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
use Flarum\Search\ValidateFilterTrait;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
use Flarum\Filter\ValidateFilterTrait;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class LikedByFilter implements FilterInterface
{
use ValidateFilterTrait;
@@ -26,11 +22,11 @@ class LikedByFilter implements FilterInterface
return 'likedBy';
}
public function filter(SearchState $state, string|array $value, bool $negate): void
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
{
$likedId = $this->asInt($value);
$likedId = $this->asInt($filterValue);
$state
$filterState
->getQuery()
->whereIn('id', function ($query) use ($likedId, $negate) {
$query->select('post_id')

View File

@@ -9,14 +9,10 @@
namespace Flarum\Likes\Query;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
use Flarum\Search\ValidateFilterTrait;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
use Flarum\Filter\ValidateFilterTrait;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class LikedFilter implements FilterInterface
{
use ValidateFilterTrait;
@@ -26,11 +22,11 @@ class LikedFilter implements FilterInterface
return 'liked';
}
public function filter(SearchState $state, string|array $value, bool $negate): void
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
{
$likedId = $this->asString($value);
$likedId = $this->asString($filterValue);
$state
$filterState
->getQuery()
->whereIn('id', function ($query) use ($likedId) {
$query->select('user_id')

View File

@@ -10,13 +10,9 @@
namespace Flarum\Likes\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use Psr\Http\Message\ResponseInterface;
class LikePostTest extends TestCase
@@ -30,21 +26,21 @@ class LikePostTest extends TestCase
$this->extension('flarum-likes');
$this->prepareDatabase([
User::class => [
'users' => [
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
$this->normalUser(),
['id' => 3, 'username' => 'Acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 2],
],
Post::class => [
'posts' => [
['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
['id' => 3, 'number' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
['id' => 5, 'number' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'discussionRenamed', 'content' => '<t><p>something</p></t>'],
['id' => 6, 'number' => 4, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
],
Group::class => [
'groups' => [
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
['id' => 6, 'name_singular' => 'Acme1', 'name_plural' => 'Acme1', 'is_hidden' => 0]
],

View File

@@ -10,13 +10,10 @@
namespace Flarum\Likes\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Likes\Api\LoadLikesRelationship;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use Illuminate\Support\Arr;
class ListPostsTest extends TestCase
@@ -33,13 +30,13 @@ class ListPostsTest extends TestCase
$this->extension('flarum-likes');
$this->prepareDatabase([
Discussion::class => [
'discussions' => [
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
],
Post::class => [
'posts' => [
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
],
User::class => [
'users' => [
$this->normalUser(),
['id' => 102, 'username' => 'user102', 'email' => '102@machine.local', 'is_email_confirmed' => 1],
['id' => 103, 'username' => 'user103', 'email' => '103@machine.local', 'is_email_confirmed' => 1],

View File

@@ -11,16 +11,16 @@ use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Saving;
use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Extend;
use Flarum\Lock\Access;
use Flarum\Lock\Event\DiscussionWasLocked;
use Flarum\Lock\Event\DiscussionWasUnlocked;
use Flarum\Lock\Filter\LockedFilter;
use Flarum\Lock\Listener;
use Flarum\Lock\Notification\DiscussionLockedBlueprint;
use Flarum\Lock\Post\DiscussionLockedPost;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\Lock\Query\LockedFilterGambit;
return [
(new Extend\Frontend('forum'))
@@ -57,6 +57,9 @@ return [
(new Extend\Policy())
->modelPolicy(Discussion::class, Access\DiscussionPolicy::class),
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->addFilter(DiscussionSearcher::class, LockedFilter::class),
(new Extend\Filter(DiscussionFilterer::class))
->addFilter(LockedFilterGambit::class),
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
->addGambit(LockedFilterGambit::class),
];

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

@@ -1,2 +1,2 @@
(()=>{var e={n:r=>{var o=r&&r.__esModule?()=>r.default:()=>r;return e.d(o,{a:o}),o},d:(r,o)=>{for(var a in o)e.o(o,a)&&!e.o(r,a)&&Object.defineProperty(r,a,{enumerable:!0,get:o[a]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{extend:()=>m});const o=flarum.reg.get("core","admin/app");var a=e.n(o);const t=flarum.reg.get("core","common/extenders");var s=e.n(t);const n=flarum.reg.get("core","common/query/IGambit"),l=flarum.reg.get("core","common/app");var i=e.n(l);class c extends n.BooleanGambit{key(){return i().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",c);const m=[(new(s().Search)).gambit("discussions",c)];a().initializers.add("lock",(()=>{a().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:a().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=r})();
(()=>{var e={n:o=>{var r=o&&o.__esModule?()=>o.default:()=>o;return e.d(r,{a:r}),r},d:(o,r)=>{for(var a in r)e.o(r,a)&&!e.o(o,a)&&Object.defineProperty(o,a,{enumerable:!0,get:r[a]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},o={};(()=>{"use strict";e.r(o);const r=flarum.reg.get("core","admin/app");var a=e.n(r);a().initializers.add("lock",(()=>{a().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:a().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=o})();
//# sourceMappingURL=admin.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,qDCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,oB,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,wBCAtD,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,c,aCE7C,MAAMW,UAAqB,EAAAC,cACxCjB,MACE,OAAO,qBAAqB,iDAAkD,CAAC,GAAG,EACpF,CACAkB,YACE,MAAO,QACT,EAEFJ,OAAOC,IAAII,IAAI,cAAe,wCAAyCH,GCRvE,UAAgB,IAAI,aACnBI,OAAO,cAAeJ,ICDvB,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeK,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/query/IGambit')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/app')\"","webpack://@flarum/lock/./src/common/query/discussions/LockedGambit.ts","webpack://@flarum/lock/./src/common/extend.ts","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/query/IGambit');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/app');","import { BooleanGambit } from 'flarum/common/query/IGambit';\nimport app from 'flarum/common/app';\nexport default class LockedGambit extends BooleanGambit {\n key() {\n return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);\n }\n filterKey() {\n return 'locked';\n }\n}\nflarum.reg.add('flarum-lock', 'common/query/discussions/LockedGambit', LockedGambit);","import Extend from 'flarum/common/extenders';\nimport LockedGambit from './query/discussions/LockedGambit';\nexport default [new Extend.Search() //\n.gambit('discussions', LockedGambit)];","import app from 'flarum/admin/app';\nexport { default as extend } from './extend';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","LockedGambit","BooleanGambit","filterKey","add","gambit","registerPermission","icon","label","permission"],"sourceRoot":""}
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCC5D,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeW,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","import app from 'flarum/admin/app';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","registerPermission","icon","label","permission"],"sourceRoot":""}

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

@@ -1,2 +1,2 @@
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>D});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var l=o.n(i);const u=flarum.reg.get("core","common/components/Badge");var d=o.n(u);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var y=o.n(b);const _=flarum.reg.get("core","common/extenders");var v=o.n(_);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=flarum.reg.get("core","common/query/IGambit"),S=flarum.reg.get("core","common/app");var j=o.n(S);class w extends P.BooleanGambit{key(){return j().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",w);const D=[(new(v().Search)).gambit("discussions",w),(new(v().PostTypes)).add("discussionLocked",h),new(v().Model)(l()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,t.extend)(l().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(k(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(y(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
(()=>{var o={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return o.d(e,{a:e}),e},d:(t,e)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},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:()=>P});const e=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var u=o.n(i);const l=flarum.reg.get("core","common/components/Badge");var d=o.n(l);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var _=o.n(b);const v=flarum.reg.get("core","common/extenders");var y=o.n(v);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=[(new(y().PostTypes)).add("discussionLocked",h),new(y().Model)(u()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,e.extend)(u().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,e.extend)(k(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(_(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(t.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,e.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().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 +0,0 @@
export { default as default } from '../common/extend';

View File

@@ -1,7 +1,5 @@
import app from 'flarum/admin/app';
export { default as extend } from './extend';
app.initializers.add('lock', () => {
app.extensionData.for('flarum-lock').registerPermission(
{

View File

@@ -1,7 +0,0 @@
import Extend from 'flarum/common/extenders';
import LockedGambit from './query/discussions/LockedGambit';
export default [
new Extend.Search() //
.gambit('discussions', LockedGambit),
];

View File

@@ -1,12 +0,0 @@
import { BooleanGambit } from 'flarum/common/query/IGambit';
import app from 'flarum/common/app';
export default class LockedGambit extends BooleanGambit {
key(): string {
return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);
}
filterKey(): string {
return 'locked';
}
}

View File

@@ -2,11 +2,7 @@ import Extend from 'flarum/common/extenders';
import Discussion from 'flarum/common/models/Discussion';
import DiscussionLockedPost from './components/DiscussionLockedPost';
import commonExtend from '../common/extend';
export default [
...commonExtend,
new Extend.PostTypes() //
.add('discussionLocked', DiscussionLockedPost),

View File

@@ -35,12 +35,3 @@ flarum-lock:
# These translations are used in the Settings page.
settings:
notify_discussion_locked_label: Someone locks a discussion I started
# Translations in this namespace are used by the forum and admin interfaces.
lib:
# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
gambits:
discussions:
locked:
key: locked

View File

@@ -1,36 +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\Lock\Filter;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
use Illuminate\Database\Query\Builder;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class LockedFilter implements FilterInterface
{
public function getFilterKey(): string
{
return 'locked';
}
public function filter(SearchState $state, string|array $value, bool $negate): void
{
$this->constrain($state->getQuery(), $negate);
}
protected function constrain(Builder $query, bool $negate): void
{
$query->where('is_locked', ! $negate);
}
}

View File

@@ -0,0 +1,44 @@
<?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\Lock\Query;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
use Flarum\Search\AbstractRegexGambit;
use Flarum\Search\SearchState;
use Illuminate\Database\Query\Builder;
class LockedFilterGambit extends AbstractRegexGambit implements FilterInterface
{
protected function getGambitPattern(): string
{
return 'is:locked';
}
protected function conditions(SearchState $search, array $matches, bool $negate): void
{
$this->constrain($search->getQuery(), $negate);
}
public function getFilterKey(): string
{
return 'locked';
}
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
{
$this->constrain($filterState->getQuery(), $negate);
}
protected function constrain(Builder $query, bool $negate): void
{
$query->where('is_locked', ! $negate);
}
}

View File

@@ -24,9 +24,8 @@ use Flarum\Post\Event\Hidden;
use Flarum\Post\Event\Posted;
use Flarum\Post\Event\Restored;
use Flarum\Post\Event\Revised;
use Flarum\Post\Filter\PostSearcher;
use Flarum\Post\Filter\PostFilterer;
use Flarum\Post\Post;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\Tags\Api\Serializer\TagSerializer;
use Flarum\User\User;
@@ -115,9 +114,9 @@ return [
->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class)
->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class),
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
(new Extend\Filter(PostFilterer::class))
->addFilter(Filter\MentionedFilter::class)
->addFilter(Filter\MentionedPostFilter::class),
(new Extend\ApiSerializer(CurrentUserSerializer::class))
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,33 +2,45 @@ import app from 'flarum/forum/app';
import { extend } from 'flarum/common/extend';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
import { throttle } from 'flarum/common/utils/throttleDebounce';
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import MentionableModels from './mentionables/MentionableModels';
export default function addComposerAutocomplete() {
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
const dropdown = new AutocompleteDropdown();
extend('flarum/common/components/TextEditor', 'onbuild', function () {
this.mentionsDropdown = new AutocompleteDropdown();
this.searchMentions = throttle(250, (mentionables, buildSuggestions) => mentionables.search().then(buildSuggestions));
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
this.navigator = new KeyboardNavigatable();
this.navigator
.when(() => this.mentionsDropdown.active)
.onUp(() => this.mentionsDropdown.navigate(-1))
.onDown(() => this.mentionsDropdown.navigate(1))
.onSelect(this.mentionsDropdown.complete.bind(this.mentionsDropdown))
.onCancel(this.mentionsDropdown.hide.bind(this.mentionsDropdown))
.when(() => dropdown.active)
.onUp(() => dropdown.navigate(-1))
.onDown(() => dropdown.navigate(1))
.onSelect(dropdown.complete.bind(dropdown))
.onCancel(dropdown.hide.bind(dropdown))
.bindTo($editor);
$editor.after($('<div class="ComposerBody-mentionsDropdownContainer"></div>'));
$editor.after($container);
});
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
let relMentionStart;
let absMentionStart;
let matchTyped;
let mentionables = new MentionableModels({
onmouseenter: function () {
dropdown.setIndex($(this).parent().index());
},
onclick: (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
dropdown.hide();
},
});
const suggestionsInputListener = () => {
const selection = this.attrs.composer.editor.getSelectionRange();
@@ -36,27 +48,30 @@ export default function addComposerAutocomplete() {
if (selection[1] - cursor > 0) return;
// Search backwards from the cursor for a mention triggering symbol. If we find one,
// we will want to show the correct autocomplete dropdown!
// Check classes implementing the IMentionableModel interface to see triggering symbols.
const lastChunk = this.attrs.composer.editor.getLastNChars(30);
absMentionStart = 0;
let activeFormat = null;
const autocompleteReader = new AutocompleteReader((character) => !!(activeFormat = app.mentionFormats.get(character)));
const autocompleting = autocompleteReader.check(this.attrs.composer.editor.getLastNChars(30), cursor, /\S+/);
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
activeFormat = app.mentionFormats.get(character);
const mentionsDropdown = this.mentionsDropdown;
let mentionables = new MentionableModels({
onmouseenter: function () {
mentionsDropdown.setIndex($(this).parent().index());
},
onclick: (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
this.mentionsDropdown.hide();
},
});
if (activeFormat && (i === 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relMentionStart = i + 1;
absMentionStart = cursor - lastChunk.length + i + 1;
mentionables.init(activeFormat.makeMentionables());
break;
}
}
this.mentionsDropdown.hide();
this.mentionsDropdown.active = false;
dropdown.hide();
dropdown.active = false;
if (autocompleting) {
mentionables.init(activeFormat.makeMentionables());
matchTyped = activeFormat.queryFromTyped(autocompleting.typed);
if (absMentionStart) {
const typed = lastChunk.substring(relMentionStart).toLowerCase();
matchTyped = activeFormat.queryFromTyped(typed);
if (!matchTyped) return;
@@ -68,14 +83,14 @@ export default function addComposerAutocomplete() {
const suggestions = mentionables.buildSuggestions();
if (suggestions.length) {
this.mentionsDropdown.items = suggestions;
m.render(this.$('.ComposerBody-mentionsDropdownContainer')[0], this.mentionsDropdown.render());
dropdown.items = suggestions;
m.render($container[0], dropdown.render());
this.mentionsDropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
const width = this.mentionsDropdown.$().outerWidth();
const height = this.mentionsDropdown.$().outerHeight();
const parent = this.mentionsDropdown.$().offsetParent();
dropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absMentionStart);
const width = dropdown.$().outerWidth();
const height = dropdown.$().outerHeight();
const parent = dropdown.$().offsetParent();
let left = coordinates.left;
let top = coordinates.top + 15;
@@ -91,21 +106,21 @@ export default function addComposerAutocomplete() {
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
left = Math.max(-parent.offset().left, left);
this.mentionsDropdown.show(left, top);
dropdown.show(left, top);
} else {
this.mentionsDropdown.active = false;
this.mentionsDropdown.hide();
dropdown.active = false;
dropdown.hide();
}
};
this.mentionsDropdown.active = true;
dropdown.active = true;
buildSuggestions();
this.mentionsDropdown.setIndex(0);
this.mentionsDropdown.$().scrollTop(0);
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
this.searchMentions(mentionables, buildSuggestions);
mentionables.search()?.then(buildSuggestions);
}
};

View File

@@ -5,6 +5,7 @@ import './components/UserMentionedNotification';
import './fragments/AutocompleteDropdown';
import './fragments/PostQuoteButton';
import './utils/getCleanDisplayName';
import './utils/getMentionText';
import './utils/reply';
import './utils/selectedText';
import './utils/textFormatter';

View File

@@ -2,6 +2,7 @@ import type MentionableModel from './MentionableModel';
import type Model from 'flarum/common/Model';
import type Mithril from 'mithril';
import MentionsDropdownItem from '../components/MentionsDropdownItem';
import { throttle } from 'flarum/common/utils/throttleDebounce';
export default class MentionableModels {
protected mentionables?: MentionableModel[];
@@ -32,7 +33,7 @@ export default class MentionableModels {
* Don't send API calls searching for models until at least 2 characters have been typed.
* This focuses the mention results on models already loaded.
*/
public readonly search = async (): Promise<void> => {
public readonly search = throttle(250, async (): Promise<void> => {
if (!this.typed || this.typed.length <= 1) return;
const typedLower = this.typed.toLowerCase();
@@ -50,7 +51,7 @@ export default class MentionableModels {
this.searched.push(typedLower);
return Promise.resolve();
};
});
public matches(mentionable: MentionableModel, model: Model): boolean {
return mentionable.matches(model, this.typed?.toLowerCase() || '');

View File

@@ -0,0 +1,21 @@
import app from 'flarum/forum/app';
/**
* Fetches the mention text for a specified user (and optionally a post ID for replies or group).
*
* Automatically determines which mention syntax to be used based on the option in the
* admin dashboard. Also performs display name clean-up automatically.
*
* @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.
*/
export default function getMentionText(user, postId, group) {
if (user !== undefined && postId === undefined) {
return app.mentionables.get('user').replacement(user);
} else if (user !== undefined && postId !== undefined) {
return app.mentionables.get('post').replacement(app.store.getById('posts', postId));
} else if (group !== undefined) {
return app.mentionables.get('group').replacement(group);
}
throw 'No parameters were passed';
}

View File

@@ -9,14 +9,10 @@
namespace Flarum\Mentions\Filter;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
use Flarum\Search\ValidateFilterTrait;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
use Flarum\Filter\ValidateFilterTrait;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class MentionedFilter implements FilterInterface
{
use ValidateFilterTrait;
@@ -26,11 +22,11 @@ class MentionedFilter implements FilterInterface
return 'mentioned';
}
public function filter(SearchState $state, string|array $value, bool $negate): void
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
{
$mentionedId = $this->asInt($value);
$mentionedId = $this->asInt($filterValue);
$state
$filterState
->getQuery()
->join('post_mentions_user', 'posts.id', '=', 'post_mentions_user.post_id')
->where('post_mentions_user.mentions_user_id', $negate ? '!=' : '=', $mentionedId);

View File

@@ -9,13 +9,9 @@
namespace Flarum\Mentions\Filter;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class MentionedPostFilter implements FilterInterface
{
public function getFilterKey(): string
@@ -23,11 +19,11 @@ class MentionedPostFilter implements FilterInterface
return 'mentionedPost';
}
public function filter(SearchState $state, string|array $value, bool $negate): void
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
{
$mentionedId = trim($value, '"');
$mentionedId = trim($filterValue, '"');
$state
$filterState
->getQuery()
->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id')
->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId);

View File

@@ -12,7 +12,6 @@ namespace Flarum\Mentions\Formatter;
use Flarum\Discussion\Discussion;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use Psr\Http\Message\ServerRequestInterface as Request;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@@ -25,22 +24,12 @@ class FormatPostMentions
) {
}
/**
* Configure rendering for post mentions.
*
* @param \s9e\TextFormatter\Renderer $renderer
* @param mixed $context
* @param string $xml
* @param \Psr\Http\Message\ServerRequestInterface|null $request
* @return string $xml to be rendered
*/
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
public function __invoke(Renderer $renderer, mixed $context, ?string $xml, Request $request = null): string
{
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;
}

View File

@@ -10,7 +10,6 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use s9e\TextFormatter\Utils;
class UnparsePostMentions
@@ -32,11 +31,10 @@ class UnparsePostMentions
*/
protected function updatePostMentionTags(mixed $context, string $xml): string
{
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;
}

View File

@@ -10,10 +10,8 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
@@ -32,14 +30,14 @@ class GroupMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
User::class => [
'users' => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
Post::class => [
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION groupname="Mods" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
@@ -55,7 +53,7 @@ class GroupMentionsTest extends TestCase
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
['group_id' => 9, 'permission' => 'mentionGroups'],
],
Group::class => [
'groups' => [
['id' => 9, 'name_singular' => 'HasPermissionToMentionGroups', 'name_plural' => 'test'],
['id' => 10, 'name_singular' => 'Hidden', 'name_plural' => 'Ninjas', 'icon' => 'fas fa-wrench', 'color' => '#000', 'is_hidden' => 1],
['id' => 11, 'name_singular' => 'Fresh Name', 'name_plural' => 'Fresh Name', 'color' => '#ccc', 'icon' => 'fas fa-users', 'is_hidden' => 0]

View File

@@ -10,12 +10,9 @@
namespace Flarum\Mentions\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Mentions\Api\LoadMentionedByRelationship;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use Illuminate\Support\Arr;
class ListPostsTest extends TestCase
@@ -32,10 +29,10 @@ class ListPostsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
Discussion::class => [
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
],
Post::class => [
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
@@ -46,7 +43,7 @@ class ListPostsTest extends TestCase
['post_id' => 3, 'mentions_user_id' => 1],
['post_id' => 4, 'mentions_user_id' => 2]
],
User::class => [
'users' => [
$this->normalUser(),
]
]);
@@ -115,10 +112,10 @@ class ListPostsTest extends TestCase
protected function prepareMentionedByData(): void
{
$this->prepareDatabase([
Discussion::class => [
'discussions' => [
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
],
Post::class => [
'posts' => [
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],

View File

@@ -10,11 +10,8 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
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;
@@ -34,16 +31,16 @@ class PostMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
User::class => [
'users' => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
],
Post::class => [
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'],
['id' => 6, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="7" number="5" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p7</POSTMENTION></r>'],
@@ -541,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

View File

@@ -10,14 +10,10 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Tags\Tag;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
class TagMentionsTest extends TestCase
{
@@ -30,20 +26,20 @@ class TagMentionsTest extends TestCase
$this->extension('flarum-tags', 'flarum-mentions');
$this->prepareDatabase([
User::class => [
'users' => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
Post::class => [
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
],
Tag::class => [
'tags' => [
['id' => 1, 'name' => 'Test', 'slug' => 'test', 'is_restricted' => 0],
['id' => 2, 'name' => 'Flarum', 'slug' => 'flarum', 'is_restricted' => 0],
['id' => 3, 'name' => 'Support', 'slug' => 'support', 'is_restricted' => 0],

View File

@@ -10,10 +10,8 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\DisplayName\DriverInterface;
@@ -33,16 +31,16 @@ class UserMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
User::class => [
'users' => [
$this->normalUser(),
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
Post::class => [
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="i_am_a_deleted_user" id="2021" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#2021</USERMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="Bad &quot;#p6 User" id="5">@"Bad "#p6 User"#5</USERMENTION></r>'],

View File

@@ -12,7 +12,6 @@ namespace Flarum\Nicknames;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Extend;
use Flarum\Nicknames\Access\UserPolicy;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\User\Event\Saving;
use Flarum\User\Search\UserSearcher;
use Flarum\User\User;
@@ -45,16 +44,15 @@ return [
->default('flarum-nicknames.set_on_registration', true)
->default('flarum-nicknames.min', 1)
->default('flarum-nicknames.max', 150)
->default('display_name_driver', 'username')
->serializeToForum('displayNameDriver', 'display_name_driver')
->serializeToForum('displayNameDriver', 'display_name_driver', null, 'username')
->serializeToForum('setNicknameOnRegistration', 'flarum-nicknames.set_on_registration', 'boolval')
->serializeToForum('randomizeUsernameOnRegistration', 'flarum-nicknames.random_username', 'boolval'),
(new Extend\Validator(UserValidator::class))
->configure(AddNicknameValidation::class),
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->setFulltext(UserSearcher::class, NicknameFullTextFilter::class),
(new Extend\SimpleFlarumSearch(UserSearcher::class))
->setFullTextGambit(NicknameFullTextGambit::class),
(new Extend\Policy())
->modelPolicy(User::class, UserPolicy::class),

View File

@@ -1,2 +1,2 @@
(()=>{var e={n:n=>{var a=n&&n.__esModule?()=>n.default:()=>n;return e.d(a,{a}),a},d:(n,a)=>{for(var t in a)e.o(a,t)&&!e.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:a[t]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};(()=>{"use strict";e.r(n);const a=flarum.reg.get("core","admin/app");var t=e.n(a);const r=flarum.reg.get("core","common/components/Alert");var s=e.n(r);const i=flarum.reg.get("core","common/components/Link");var l=e.n(i);const o=flarum.reg.get("core","admin/components/BasicsPage");var c=e.n(o);const g=flarum.reg.get("core","common/utils/extractText");var u=e.n(g);const d=flarum.reg.get("core","common/extend");t().initializers.add("flarum/nicknames",(()=>{t().extensionData.for("flarum-nicknames").registerSetting((function(){if("nickname"!==t().data.settings.display_name_driver)return m("div",{className:"Form-group"},m(s(),{dismissible:!1},t().translator.trans("flarum-nicknames.admin.wrong_driver",{a:m(l(),{href:t().route("basics")})})))})).registerSetting({setting:"flarum-nicknames.set_on_registration",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.set_on_registration_label")}).registerSetting({setting:"flarum-nicknames.random_username",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.random_username_label"),help:t().translator.trans("flarum-nicknames.admin.settings.random_username_help")}).registerSetting({setting:"flarum-nicknames.unique",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.unique_label")}).registerSetting({setting:"flarum-nicknames.regex",type:"text",label:t().translator.trans("flarum-nicknames.admin.settings.regex_label")}).registerSetting({setting:"flarum-nicknames.min",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.min_label")}).registerSetting({setting:"flarum-nicknames.max",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.max_label")}).registerPermission({icon:"fas fa-user-tag",label:t().translator.trans("flarum-nicknames.admin.permissions.edit_own_nickname_label"),permission:"user.editOwnNickname"},"start"),(0,d.extend)(c().prototype,"driverLocale",(function(e){e.display_name.nickname=u()(t().translator.trans("flarum-nicknames.admin.basics.display_name_driver_options.nickname"))}))}))})(),module.exports=n})();
(()=>{var e={n:n=>{var a=n&&n.__esModule?()=>n.default:()=>n;return e.d(a,{a}),a},d:(n,a)=>{for(var t in a)e.o(a,t)&&!e.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:a[t]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};(()=>{"use strict";e.r(n);const a=flarum.reg.get("core","admin/app");var t=e.n(a);const r=flarum.reg.get("core","common/components/Alert");var s=e.n(r);const i=flarum.reg.get("core","common/components/Link");var l=e.n(i);t().initializers.add("flarum/nicknames",(()=>{t().extensionData.for("flarum-nicknames").registerSetting((function(){if("nickname"!==t().data.settings.display_name_driver)return m("div",{className:"Form-group"},m(s(),{dismissible:!1},t().translator.trans("flarum-nicknames.admin.wrong_driver",{a:m(l(),{href:t().route("basics")})})))})).registerSetting({setting:"flarum-nicknames.set_on_registration",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.set_on_registration_label")}).registerSetting({setting:"flarum-nicknames.random_username",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.random_username_label"),help:t().translator.trans("flarum-nicknames.admin.settings.random_username_help")}).registerSetting({setting:"flarum-nicknames.unique",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.unique_label")}).registerSetting({setting:"flarum-nicknames.regex",type:"text",label:t().translator.trans("flarum-nicknames.admin.settings.regex_label")}).registerSetting({setting:"flarum-nicknames.min",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.min_label")}).registerSetting({setting:"flarum-nicknames.max",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.max_label")}).registerPermission({icon:"fas fa-user-tag",label:t().translator.trans("flarum-nicknames.admin.permissions.edit_own_nickname_label"),permission:"user.editOwnNickname"},"start")}))})(),module.exports=n})();
//# sourceMappingURL=admin.js.map

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,9 +1,6 @@
import app from 'flarum/admin/app';
import Alert from 'flarum/common/components/Alert';
import Link from 'flarum/common/components/Link';
import BasicsPage from 'flarum/admin/components/BasicsPage';
import extractText from 'flarum/common/utils/extractText';
import { extend } from 'flarum/common/extend';
app.initializers.add('flarum/nicknames', () => {
app.extensionData
@@ -58,8 +55,4 @@ app.initializers.add('flarum/nicknames', () => {
},
'start'
);
extend(BasicsPage.prototype, 'driverLocale', function (locale) {
locale.display_name['nickname'] = extractText(app.translator.trans('flarum-nicknames.admin.basics.display_name_driver_options.nickname'));
});
});

View File

@@ -1,10 +1,10 @@
import app from 'flarum/forum/app';
import FormModal from 'flarum/common/components/FormModal';
import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import Form from '@flarum/core/src/common/components/Form';
export default class NicknameModal extends FormModal {
export default class NicknameModal extends Modal {
oninit(vnode) {
super.oninit(vnode);
this.nickname = Stream(app.session.user.displayName());

View File

@@ -1,8 +1,5 @@
flarum-nicknames:
admin:
basics:
display_name_driver_options:
nickname: Nickname
permissions:
edit_own_nickname_label: Edit own nickname
settings:

View File

@@ -9,16 +9,19 @@
namespace Flarum\Nicknames;
use Flarum\Search\AbstractFulltextFilter;
use Flarum\Search\Database\DatabaseSearchState;
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Search\GambitInterface;
use Flarum\Search\SearchState;
use Flarum\User\UserRepository;
use Illuminate\Database\Eloquent\Builder;
/**
* @extends AbstractFulltextFilter<DatabaseSearchState>
*/
class NicknameFullTextFilter extends AbstractFulltextFilter
class NicknameFullTextGambit implements GambitInterface
{
public function __construct(
protected UserRepository $users
@@ -34,12 +37,14 @@ class NicknameFullTextFilter extends AbstractFulltextFilter
->orWhere('nickname', 'like', "{$searchValue}%");
}
public function search(SearchState $state, string $value): void
public function apply(SearchState $search, string $bit): bool
{
$state->getQuery()
$search->getQuery()
->whereIn(
'id',
$this->getUserSearchSubQuery($value)
$this->getUserSearchSubQuery($bit)
);
return true;
}
}

View File

@@ -27,7 +27,7 @@ class UpdateTest extends TestCase
$this->extension('flarum-nicknames');
$this->prepareDatabase([
User::class => [
'users' => [
$this->normalUser(),
],
]);

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": "^2.0",
"composer/composer": "^2.7"
"composer/composer": "^2.3"
},
"require-dev": {
"flarum/testing": "^2.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,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

@@ -6,7 +6,6 @@ export default class Task extends Model {
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 +0,0 @@
import QueueState from './QueueState';
import ControlSectionState from './ControlSectionState';
export default class ExtensionManagerState {
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,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="Form 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('FormSection', 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

@@ -6,7 +6,6 @@ import { ComponentAttrs } from 'flarum/common/Component';
import Installer from './Installer';
import Updater from './Updater';
import Mithril from 'mithril';
import Form from 'flarum/common/components/Form';
export default class ControlSection extends Component<ComponentAttrs> {
oninit(vnode: Mithril.Vnode<ComponentAttrs, this>) {
@@ -15,22 +14,22 @@ 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'] ? (
<Form>
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
<Installer />
<Updater />
</Form>
</>
) : (
<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>
)}

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