mirror of
https://github.com/flarum/core.git
synced 2025-08-24 09:03:05 +02:00
Compare commits
313 Commits
as/support
...
mithril-2-
Author | SHA1 | Date | |
---|---|---|---|
|
dd17fa3342 | ||
|
db8755d5d9 | ||
|
5c2a233b74 | ||
|
4dd0361ccf | ||
|
ccd2b0ee0b | ||
|
b954e79ec8 | ||
|
bdc64f98a5 | ||
|
f9e5aa4193 | ||
|
801220cc10 | ||
|
bd52aaaa61 | ||
|
4a5d12626b | ||
|
1e4a7f3282 | ||
|
cbcb0429d0 | ||
|
14b12f8ae8 | ||
|
37d7068569 | ||
|
6e1a9193ac | ||
|
ea6aea409d | ||
|
d572200cfb | ||
|
e10ebf0489 | ||
|
b251ff0469 | ||
|
4a0947db4b | ||
|
32767d6321 | ||
|
27017b7181 | ||
|
bdd30ceecc | ||
|
abc98645a0 | ||
|
5d538adc33 | ||
|
6f11567f91 | ||
|
537e2690e8 | ||
|
0e715a8c40 | ||
|
383a7e559f | ||
|
aeaa9a4b73 | ||
|
85f8fc52b7 | ||
|
174f3aba90 | ||
|
47ce93d2fd | ||
|
24935eacaf | ||
|
0f99e7c015 | ||
|
691ae85e50 | ||
|
fb83f8c59c | ||
|
a6e6c54972 | ||
|
6b5bdb5c41 | ||
|
d7ef260c54 | ||
|
571ed8d8e5 | ||
|
42ad490096 | ||
|
09bead3ba2 | ||
|
abb896d430 | ||
|
6e7c86ac50 | ||
|
2673dd2ee3 | ||
|
cbd9c8dd4f | ||
|
29d995de45 | ||
|
a5b2768836 | ||
|
398951f282 | ||
|
07f04c7ba7 | ||
|
c8d5ca51bb | ||
|
60dbd3f26c | ||
|
e7f6e37799 | ||
|
27ffeb204e | ||
|
884a5cf3b9 | ||
|
e895ca738d | ||
|
f41aec1043 | ||
|
4c73d76668 | ||
|
f67194484b | ||
|
c8f47d519d | ||
|
e9b267a33a | ||
|
554c72c6db | ||
|
fb2b0a1d3e | ||
|
2ac18a39ed | ||
|
6ed3cb56d4 | ||
|
038744f092 | ||
|
2edbd4508a | ||
|
a6d4658dff | ||
|
955c8121d3 | ||
|
fd2dcd38d6 | ||
|
520c7e7d0f | ||
|
5ec9c52b04 | ||
|
0341e64057 | ||
|
f1a480d3d7 | ||
|
aceac88013 | ||
|
8cc9e18990 | ||
|
94d3bea53e | ||
|
9e6cfcf05a | ||
|
b8abd2522e | ||
|
376a00f24f | ||
|
43bfaa7400 | ||
|
7ba8a7122b | ||
|
88bc291c86 | ||
|
e699ada1cc | ||
|
2186584878 | ||
|
7beeae6269 | ||
|
e0e3d6ecae | ||
|
f8bc58fd1a | ||
|
764f50f469 | ||
|
e8485db484 | ||
|
f017d7afbe | ||
|
44c1e91f05 | ||
|
4b05e0073a | ||
|
3764abee51 | ||
|
015cedb29d | ||
|
32e9fa1f0b | ||
|
2bdf0d7096 | ||
|
b889fa1bbf | ||
|
76c3494f9b | ||
|
6bcb76b914 | ||
|
63984b43f9 | ||
|
389bc59745 | ||
|
2eb28ea396 | ||
|
169b0fbd9b | ||
|
6b178b8204 | ||
|
1d6e985107 | ||
|
f1fc0fecb7 | ||
|
94d8f7e726 | ||
|
b0891c42da | ||
|
7d5bebb70a | ||
|
b78db0268a | ||
|
fcda092558 | ||
|
f6dd87f72f | ||
|
d8d43d95e0 | ||
|
51c7b17305 | ||
|
b592ceb199 | ||
|
a083876b5f | ||
|
b0cbe277c2 | ||
|
b1d948becc | ||
|
a48568b17a | ||
|
e5cebd85ed | ||
|
fbd5f6245b | ||
|
3ce63bc035 | ||
|
bac5e7c94c | ||
|
97b0e61f61 | ||
|
604989be72 | ||
|
f9b1dfe499 | ||
|
f611a44a08 | ||
|
911f1fd5c9 | ||
|
0087b956ef | ||
|
99b119f1fa | ||
|
6be37fd376 | ||
|
46741f63fe | ||
|
cec00c0dd6 | ||
|
5f2c9da2f5 | ||
|
92de05e911 | ||
|
68aa6e26da | ||
|
8b7fa012c7 | ||
|
61231debd3 | ||
|
e771ec90c4 | ||
|
ab85b49845 | ||
|
04e5d5884f | ||
|
ddc1141106 | ||
|
a48cc19814 | ||
|
6d18b700ec | ||
|
b2bc427b3f | ||
|
1f94ffc842 | ||
|
3d91268493 | ||
|
81fd986881 | ||
|
d1b0030292 | ||
|
2c395a781c | ||
|
21861f231b | ||
|
537f5e833e | ||
|
4d45aaa9ae | ||
|
529f8e5f32 | ||
|
991d90bf4a | ||
|
38fed603f8 | ||
|
9691a6ab92 | ||
|
e8e4b64d7d | ||
|
35d76515d3 | ||
|
5d34124a02 | ||
|
16a6f82e8f | ||
|
8a0c241a8e | ||
|
a376c0e596 | ||
|
5ccf9d420e | ||
|
742f89f660 | ||
|
916cf4b546 | ||
|
f127e67fd4 | ||
|
3e79c3e3ff | ||
|
0172dfd79c | ||
|
eb627544fa | ||
|
73c0a90da7 | ||
|
ca0f8f2d72 | ||
|
090df13e7f | ||
|
d1a1277f88 | ||
|
31b2ab1b2b | ||
|
2590073a50 | ||
|
4402dc81ac | ||
|
f93a255a2f | ||
|
ddb0a9f1ce | ||
|
f44caf1600 | ||
|
89b6847710 | ||
|
2fb885175a | ||
|
7d9db2f4ae | ||
|
5d073941c9 | ||
|
f664fa5be7 | ||
|
aa4b58d7aa | ||
|
3120eb6f63 | ||
|
aac54a1d28 | ||
|
0d0841d019 | ||
|
83d2dbd290 | ||
|
7e5b40c532 | ||
|
6c9971eeba | ||
|
37a690833a | ||
|
fa2301b5c1 | ||
|
edca7b93ec | ||
|
095dce9a3e | ||
|
479f655bb3 | ||
|
41d6e91318 | ||
|
87414995b6 | ||
|
2c93b5f801 | ||
|
7498f5e506 | ||
|
79f5291f04 | ||
|
ed3b923f58 | ||
|
1ce06611ce | ||
|
27bacd779b | ||
|
02acacfdcb | ||
|
23d95a7566 | ||
|
43164df79e | ||
|
46e704b27b | ||
|
e55867acb4 | ||
|
3596425bde | ||
|
b43452223f | ||
|
70697be8c0 | ||
|
c20ae678f5 | ||
|
30a61b8b42 | ||
|
824fe95346 | ||
|
8475d176e0 | ||
|
95f4dc771d | ||
|
68caf45f33 | ||
|
f72d118bec | ||
|
652d961907 | ||
|
71178245fc | ||
|
cfd1f01299 | ||
|
674f55e91d | ||
|
f897b58f29 | ||
|
5c49b71c02 | ||
|
b9ba5b63f1 | ||
|
6e88dfb2cb | ||
|
c899e11070 | ||
|
784b5cc03c | ||
|
c17d7cd23f | ||
|
4af34265cc | ||
|
1616d8f1c7 | ||
|
57b85f501a | ||
|
91609b8a71 | ||
|
9d8b466d57 | ||
|
02154d05e5 | ||
|
a454b185e9 | ||
|
84c5248872 | ||
|
2a784009fb | ||
|
070865f825 | ||
|
9615fd3e39 | ||
|
dde9c9c51b | ||
|
6547290472 | ||
|
74f6a3e6ce | ||
|
98740472a8 | ||
|
1e9825de4f | ||
|
d14ca79dfa | ||
|
5cd5f27769 | ||
|
d05b63eddd | ||
|
48c7354c08 | ||
|
0fe635d32c | ||
|
2c2a42030a | ||
|
37d2902cd1 | ||
|
66c2f6b76a | ||
|
4d68636544 | ||
|
73891b751b | ||
|
fe011bf285 | ||
|
524dde31d4 | ||
|
b69fc01ab3 | ||
|
20e69e6351 | ||
|
d097e7ed4b | ||
|
63e07b2044 | ||
|
60c3f23667 | ||
|
9793c10610 | ||
|
29af3e6d8b | ||
|
703c3442da | ||
|
e0d3a8c733 | ||
|
527748ff66 | ||
|
eb440bb9b6 | ||
|
d71f77d592 | ||
|
8b891abb2b | ||
|
5f0dcc71ba | ||
|
4fcafe3b2f | ||
|
29065e1ee9 | ||
|
2b39bd7a0d | ||
|
94fe3236d7 | ||
|
3b6e5a0caf | ||
|
9ac8f1543a | ||
|
439e3a5a9a | ||
|
883e1a9d6a | ||
|
2ac2edbbad | ||
|
18148141c3 | ||
|
10f4223028 | ||
|
e10220ae47 | ||
|
dcd14821c2 | ||
|
edeaa5855c | ||
|
cc91244f1a | ||
|
5faab8bdbd | ||
|
99d79e7571 | ||
|
5606eae0f1 | ||
|
62cb71d4e1 | ||
|
e28ba4acff | ||
|
5b3914535d | ||
|
44975bc606 | ||
|
f7931e8a30 | ||
|
3ba655b2f9 | ||
|
735ecab446 | ||
|
64d4eb8c4c | ||
|
e4f1a397d6 | ||
|
5a244dcfd2 | ||
|
b60309284c | ||
|
392fe98c02 | ||
|
873f489fec | ||
|
9d8374208f | ||
|
07551b4890 | ||
|
b2c147c147 | ||
|
5bf5bd36ee | ||
|
4435ff193a | ||
|
5c30f8fa67 |
@@ -15,5 +15,5 @@ indent_size = 2
|
|||||||
[*.{diff,md}]
|
[*.{diff,md}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{php,xml,json}]
|
[*.{php,xml}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -11,5 +11,3 @@ phpunit.xml export-ignore
|
|||||||
tests export-ignore
|
tests export-ignore
|
||||||
|
|
||||||
js/dist/* -diff
|
js/dist/* -diff
|
||||||
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -7,24 +7,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: JS / Build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- uses: actions/checkout@master
|
||||||
uses: actions/checkout@v2
|
- uses: flarum/action-build@master
|
||||||
|
|
||||||
- name: Restore npm cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('js/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
# Our action will install npm, cd into `./js`, run `npm run build`,
|
|
||||||
# then commit and upload any changes
|
|
||||||
- name: Build production JS
|
|
||||||
uses: flarum/action-build@master
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
76
.github/workflows/codeql-analysis.yml
vendored
76
.github/workflows/codeql-analysis.yml
vendored
@@ -1,76 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
# Run on:
|
|
||||||
# - pushes to master, or
|
|
||||||
# - PRs with a base of `master`
|
|
||||||
# - which do not **only** consist of changes to .md or .less files
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
paths-ignore:
|
|
||||||
- '**/*.md'
|
|
||||||
- '**/*.less'
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
paths-ignore:
|
|
||||||
- '**/*.md'
|
|
||||||
- '**/*.less'
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 1,3,5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze / ${{ matrix.language }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
21
.github/workflows/lint.yml
vendored
21
.github/workflows/lint.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'js/src/**'
|
- 'js/src/**'
|
||||||
@@ -11,18 +10,22 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prettier:
|
prettier:
|
||||||
name: JS / Prettier
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
name: JS / Prettier
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- uses: actions/checkout@master
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: "14"
|
node-version: "12"
|
||||||
|
|
||||||
- name: Check JS formatting
|
- name: Install JS dependencies
|
||||||
run: npx prettier --check src
|
run: npm ci
|
||||||
|
working-directory: ./js
|
||||||
|
|
||||||
|
- name: Check JS code for formatting
|
||||||
|
run: node_modules/.bin/prettier --check src
|
||||||
working-directory: ./js
|
working-directory: ./js
|
||||||
|
45
.github/workflows/pr_size_change.yml
vendored
45
.github/workflows/pr_size_change.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
name: Bundle size checker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- "js/**"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "js/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bundlewatch:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
name: Bundlewatch
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "14"
|
|
||||||
|
|
||||||
- name: Use npm v7
|
|
||||||
run: sudo npm install -g npm@7.x.x
|
|
||||||
|
|
||||||
- name: Install JS dependencies
|
|
||||||
# We need to use `npm install` here. If we don't, the workflow will fail.
|
|
||||||
run: npm install
|
|
||||||
working-directory: ./js
|
|
||||||
|
|
||||||
- name: Build production assets
|
|
||||||
run: npm run build
|
|
||||||
working-directory: ./js
|
|
||||||
|
|
||||||
- name: Check bundle size change
|
|
||||||
run: node_modules/.bin/bundlewatch --config .bundlewatch.config.json
|
|
||||||
working-directory: ./js
|
|
||||||
env:
|
|
||||||
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
|
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: [7.3, 7.4, '8.0']
|
php: [7.2, 7.3, 7.4]
|
||||||
service: ['mysql:5.7', mariadb]
|
service: ['mysql:5.7', mariadb]
|
||||||
prefix: ['', flarum_]
|
prefix: ['', flarum_]
|
||||||
|
|
||||||
@@ -21,16 +21,16 @@ jobs:
|
|||||||
prefixStr: (prefix)
|
prefixStr: (prefix)
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
- php: 7.3
|
- php: 7.2
|
||||||
service: 'mysql:5.7'
|
service: 'mysql:5.7'
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 7.3
|
- php: 7.2
|
||||||
service: mariadb
|
service: mariadb
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 8.0
|
- php: 7.3
|
||||||
service: 'mysql:5.7'
|
service: 'mysql:5.7'
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
- php: 8.0
|
- php: 7.3
|
||||||
service: mariadb
|
service: mariadb
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
|
|
||||||
@@ -43,25 +43,15 @@ jobs:
|
|||||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- uses: actions/checkout@master
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Select PHP version
|
||||||
uses: shivammathur/setup-php@0b9d33cd0782337377999751fc10ea079fdd7104 # pin@v2
|
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
coverage: xdebug
|
|
||||||
extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
|
|
||||||
tools: phpunit, composer:v2
|
|
||||||
|
|
||||||
# The authentication alter is necessary because newer mysql versions use the `caching_sha2_password` driver,
|
|
||||||
# which isn't supported prior to PHP7.4
|
|
||||||
# When we drop support for PHP7.3, we should remove this from the setup.
|
|
||||||
- name: Create MySQL Database
|
- name: Create MySQL Database
|
||||||
run: |
|
run: |
|
||||||
sudo systemctl start mysql
|
sudo systemctl start mysql
|
||||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||||
mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" --port 13306
|
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
- name: Install Composer dependencies
|
||||||
run: composer install
|
run: composer install
|
||||||
@@ -75,5 +65,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Composer tests
|
- name: Run Composer tests
|
||||||
run: composer test
|
run: composer test
|
||||||
env:
|
|
||||||
COMPOSER_PROCESS_TIMEOUT: 600
|
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,8 +4,6 @@ composer.phar
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
tests/.phpunit.result.cache
|
|
||||||
/tests/integration/tmp
|
/tests/integration/tmp
|
||||||
.vagrant
|
.vagrant
|
||||||
.idea/*
|
.idea/*
|
||||||
.vscode
|
|
||||||
|
@@ -12,3 +12,7 @@ disabled:
|
|||||||
- phpdoc_order
|
- phpdoc_order
|
||||||
- phpdoc_separation
|
- phpdoc_separation
|
||||||
- phpdoc_types
|
- phpdoc_types
|
||||||
|
|
||||||
|
finder:
|
||||||
|
exclude:
|
||||||
|
- "stubs"
|
||||||
|
256
CHANGELOG.md
256
CHANGELOG.md
@@ -1,261 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.1.0-beta.16](https://github.com/flarum/core/compare/v0.1.0-beta.15...v0.1.0-beta.16)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Allow event subscribers (https://github.com/flarum/core/pull/2535)
|
|
||||||
- Allow Settings extender to have a default value (https://github.com/flarum/core/pull/2495)
|
|
||||||
- Allow hooking into the sending of notifications before being send (https://github.com/flarum/core/pull/2533)
|
|
||||||
- PHP 8 support (https://github.com/flarum/core/pull/2507)
|
|
||||||
- Search extender (https://github.com/flarum/core/pull/2483)
|
|
||||||
- User badges to post preview (https://github.com/flarum/core/pull/2555)
|
|
||||||
- Optional extension dependencies allow a booting order (https://github.com/flarum/core/pull/2579)
|
|
||||||
- Auth extender (https://github.com/flarum/core/pull/2176)
|
|
||||||
- `X-Powered-By` header added to allow indexers easier data aggregation of Flarum adoption (https://github.com/flarum/core/pull/2618)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Run integration tests in transaction (https://github.com/flarum/core/pull/2304)
|
|
||||||
- Allow policies to return a boolean for simplified allow/deny (https://github.com/flarum/core/pull/2534)
|
|
||||||
- Converted highlight helper to typescript (https://github.com/flarum/core/pull/2532)
|
|
||||||
- Add accessibility attributes to Mark as Read button (https://github.com/flarum/core/pull/2564)
|
|
||||||
- Dismiss errors on change email modal upon a new request ([00913d5](https://github.com/flarum/core/commit/00913d5b0be2172cfce1f16aaf64a24f3d2e6d4b))
|
|
||||||
- Disabled extensions now are marked with a red circle instead of a red dot (https://github.com/flarum/core/pull/2562)
|
|
||||||
- Extension dependency errors now show the extension title instead of the ID (https://github.com/flarum/core/pull/2563)
|
|
||||||
- Change `mutate` method on ApiSerializer extender to `attributes` (https://github.com/flarum/core/pull/2578)
|
|
||||||
- Moved locale files to the core from the language pack (https://github.com/flarum/core/pull/2408)
|
|
||||||
- AdminPage extensibility and generic improvements (https://github.com/flarum/core/pull/2593)
|
|
||||||
- Remove entry of authors, link to https://flarum.org/team (https://github.com/flarum/core/pull/2625)
|
|
||||||
- Search and filtering are split (https://github.com/flarum/core/pull/2454)
|
|
||||||
- Move IP identification into a middleware (https://github.com/flarum/core/pull/2624)
|
|
||||||
- Editor Driver abstraction introduced (https://github.com/flarum/core/pull/2594)
|
|
||||||
- Allow overriding routes (https://github.com/flarum/core/pull/2577)
|
|
||||||
- Split user edit permissions into permissions for editing of user credentials, username, groups and suspending (https://github.com/flarum/core/pull/2620)
|
|
||||||
- Reduced number of admin extension categories (https://github.com/flarum/core/pull/2604)
|
|
||||||
- Move search related classes to a dedicated Query namespace (https://github.com/flarum/core/pull/2645)
|
|
||||||
- Rewrite common helpers into typescript (https://github.com/flarum/core/pull/2541)
|
|
||||||
- `TextEditor` is moved to the common namespace for use in the admin frontend (https://github.com/flarum/core/pull/2649)
|
|
||||||
- Update Laravel/Illuminate components to 8 (https://github.com/flarum/core/pull/2576)
|
|
||||||
- Eager load relations in discussion listing to improve performance (https://github.com/flarum/core/pull/2639)
|
|
||||||
- Adopt flarum/testing package (https://github.com/flarum/core/pull/2545)
|
|
||||||
- Replace `user` gambit with `author` gambit ([612a57c](https://github.com/flarum/core/commit/612a57c4664415a3ea120103483645c32acc6f12))
|
|
||||||
- Posts page of on user profile loads posts using username instead of id ([30017ee](https://github.com/flarum/core/commit/30017eef09ae9e78640c4e2cacd4909fffa8d775))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Transform css breaks iOS scroll functionality (https://github.com/flarum/core/pull/2527)
|
|
||||||
- Composer header is hidden on mobile devices (https://github.com/flarum/core/pull/2279)
|
|
||||||
- Cannot delete a post or discussion of a deleted user (https://github.com/flarum/core/pull/2521)
|
|
||||||
- DiscussionListPane jumps around not keeping the scroll position (https://github.com/flarum/core/pull/2402)
|
|
||||||
- Infinite scroll on notifications dropdown broken (https://github.com/flarum/core/pull/2524)
|
|
||||||
- The show language selector switch remains toggled on ([9347b12](https://github.com/flarum/core/commit/9347b12b47bf4ab97ffb7ca92673604b237c1012))
|
|
||||||
- Model Visibility extender throws exception on extensions that aren't installed or enabled (https://github.com/flarum/core/pull/2580)
|
|
||||||
- Extensions are marked as enabled when enabling fails to unmet extension dependencies (https://github.com/flarum/core/pull/2558)
|
|
||||||
- Routes to admin extension pages without a valid ID break the admin page (https://github.com/flarum/core/pull/2584)
|
|
||||||
- Disabled fieldset use an incorrect CSS property `disallowed` (https://github.com/flarum/core/pull/2585)
|
|
||||||
- Scrolling to a post that is already loaded the Load More button shows and does not trigger (https://github.com/flarum/core/pull/2388)
|
|
||||||
- Opening discussions on some mobile devices require a double tap (https://github.com/flarum/core/pull/2607)
|
|
||||||
- iOS devices show erratic behavior in the post stream while updating (https://github.com/flarum/core/pull/2548)
|
|
||||||
- Small mobile screens partially hides the composer when the keyboard is open (https://github.com/flarum/core/pull/2631)
|
|
||||||
- Clearing cache does not clear the template cache in storage/views (https://github.com/flarum/core/pull/2648)
|
|
||||||
- Boot errors show critical information (https://github.com/flarum/core/pull/2633)
|
|
||||||
- List user endpoint discloses last online even if user choose against it (https://github.com/flarum/core/pull/2634)
|
|
||||||
- Group gambit disclosed hidden groups (https://github.com/flarum/core/pull/2657)
|
|
||||||
- Search results on small windows not fully visible (https://github.com/flarum/core/pull/2650)
|
|
||||||
- Composer goes off screen on Safari when starting to type (https://github.com/flarum/core/pull/2660)
|
|
||||||
- A search that has no results shows the search results dropdown ([b88a7cb](https://github.com/flarum/core/commit/b88a7cb33b56e318f11670e9e2d563aef94db039))
|
|
||||||
- The composer modal moves around when typing on Safari ([a64c398](https://github.com/flarum/core/commit/a64c39835aba43e831209609f4a9638ae589aa41))
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Deprecated CSRF wildcard path match
|
|
||||||
- Deprecated policy and visibility scoping events
|
|
||||||
- Deprecated post types event
|
|
||||||
- Deprecated validation events
|
|
||||||
- Deprecated notification events
|
|
||||||
- Deprecated floodgate
|
|
||||||
- Deprecated user preferences event
|
|
||||||
- Deprecated formatting events
|
|
||||||
- Deprecated api events
|
|
||||||
- Deprecated bootstrap.php support
|
|
||||||
- PHP 7.2 support (https://github.com/flarum/core/pull/2507)
|
|
||||||
- Bidi attribute in the rendered HTML (https://github.com/flarum/core/pull/2602)
|
|
||||||
- `AccessToken::find`, use `AccessToken::findValid` instead (https://github.com/flarum/core/pull/2651)
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
- `GetModelIsPrivate` event (https://github.com/flarum/core/pull/2587)
|
|
||||||
- `CheckingPassword` event (https://github.com/flarum/core/pull/2176)
|
|
||||||
- `event()` helper (https://github.com/flarum/core/pull/2608)
|
|
||||||
- `AccessToken::generate` argument `$lifetime` (https://github.com/flarum/core/pull/2651)
|
|
||||||
- `Rememberer::remember` argument `$token` should receive an instance of `RememberAccessToken` with `AccessToken` being deprecated (https://github.com/flarum/core/pull/2651)
|
|
||||||
- `Rememberer::rememberUser` (https://github.com/flarum/core/pull/2651)
|
|
||||||
- `SessionAuthenticator::logIn` argument `$userId`, should be replaced with `AccessToken` (https://github.com/flarum/core/pull/2651)
|
|
||||||
- `TextEditor` has been moved to `common` (https://github.com/flarum/core/pull/2649)
|
|
||||||
- `UserFilter` ([91e8b56](https://github.com/flarum/core/commit/91e8b569618957c86757ef89bac666e9102db5ae))
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-beta.15](https://github.com/flarum/core/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Slug drivers support (https://github.com/flarum/core/pull/2456).
|
|
||||||
- Notification type extender (https://github.com/flarum/core/pull/2424).
|
|
||||||
- Validation extender (https://github.com/flarum/core/pull/2102).
|
|
||||||
- Post extender (https://github.com/flarum/core/pull/2101).
|
|
||||||
- Notification channel extender (https://github.com/flarum/core/pull/2432).
|
|
||||||
- Service provider extender (https://github.com/flarum/core/pull/2437).
|
|
||||||
- API serializer extender (https://github.com/flarum/core/pull/2438).
|
|
||||||
- User preferences extender (https://github.com/flarum/core/pull/2463).
|
|
||||||
- Settings extender (https://github.com/flarum/core/pull/2452).
|
|
||||||
- ApiController extender (https://github.com/flarum/core/pull/2451).
|
|
||||||
- Model visibility extender (https://github.com/flarum/core/pull/2460).
|
|
||||||
- Policy extender (https://github.com/flarum/core/pull/2461).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Time helpers converted to Typescript (https://github.com/flarum/core/pull/2391).
|
|
||||||
- Improved the formatter extender (https://github.com/flarum/core/pull/2098).
|
|
||||||
- Improve wording on installer when facing file permission issues (https://github.com/flarum/core/pull/2435).
|
|
||||||
- Background color of checkbox toggles improved for better usability (https://github.com/flarum/core/pull/2443).
|
|
||||||
- Route resolving refactored (https://github.com/flarum/core/pull/2425).
|
|
||||||
- Administration panel UX refactored (https://github.com/flarum/core/pull/2409).
|
|
||||||
- Floodgate moved to middleware and extender added (https://github.com/flarum/core/pull/2170).
|
|
||||||
- DRY up image uploading logic (https://github.com/flarum/core/pull/2477).
|
|
||||||
- Process isolation on testing (https://github.com/flarum/core/commit/984f751c718c89501cc09857bc271efa2c7eea8c).
|
|
||||||
- Forum and admin javascript exports namespaced (https://github.com/flarum/core/pull/2488).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Web updater does not take into account subfolder installations (https://github.com/flarum/core/pull/2426).
|
|
||||||
- Callables handling in extenders failed (https://github.com/flarum/core/pull/2423).
|
|
||||||
- Scrolling on mobile from PostSteam changes didn't work correctly (https://github.com/flarum/core/pull/2385).
|
|
||||||
- Side pane covers part of the discussion page due to `app.discussions` being empty (https://github.com/flarum/core/commit/102e76b084bf47fdfb4c73f95e1fbb322537f7aa).
|
|
||||||
- Change email modal keeps showing the previous error message even on success (https://github.com/flarum/core/pull/2467).
|
|
||||||
- Comment count not updated when discussions are deleted (https://github.com/flarum/core/pull/2472).
|
|
||||||
- `goToIndex` in PostStream does not trigger an xhr to retrieve new data (https://github.com/flarum/core/commit/09e2736cbcc267594b660beabbd001d9030f9880).
|
|
||||||
- On refresh the post number is reduced by one (https://github.com/flarum/core/pull/2476).
|
|
||||||
- Queue worker would instantiate a new Queue factory, not the bound one (https://github.com/flarum/core/pull/2481).
|
|
||||||
- Header accidentally has a border bottom (https://github.com/flarum/core/pull/2489).
|
|
||||||
- Namespace mentioned in docblock is incorrect (https://github.com/flarum/core/pull/2494).
|
|
||||||
- Scrolling inside longer discussions (especially Firefox) skips posts (https://github.com/flarum/core/commit/210a6b3e253d7917bd1eacd3ed8d2f95073ae99d).
|
|
||||||
- Uploading avatars that are jpg/jpeg fails with a validation error (https://github.com/flarum/core/pull/2497).
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- MomentJS alias (https://github.com/flarum/core/pull/2428).
|
|
||||||
- Deprecated user events `GetDisplayName` and `PrepareUserGroups` (https://github.com/flarum/core/pull/2428).
|
|
||||||
- AssertPermissionTrait (https://github.com/flarum/core/pull/2428).
|
|
||||||
- Path related helpers and methods in Application (https://github.com/flarum/core/pull/2428).
|
|
||||||
- Backward compatibility layers from the frontend rewrite (https://github.com/flarum/core/pull/2428).
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
- `CheckingForFlooding` (https://github.com/flarum/core/commit/8e25bcb68f86cc992c46dfa70368419fe9f936ac).
|
|
||||||
|
|
||||||
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- SuperTextarea component is not exported.
|
|
||||||
- Symfony dependencies do not match those depended on by Laravel (https://github.com/flarum/core/pull/2407).
|
|
||||||
- Scripts from textformatter aren't executed (https://github.com/flarum/core/pull/2415)
|
|
||||||
- Sub path installations have no page title.
|
|
||||||
- Losing focus of Composer area when coming from fullscreen.
|
|
||||||
|
|
||||||
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
|
|
||||||
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
|
|
||||||
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
|
|
||||||
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
|
|
||||||
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
|
|
||||||
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
|
|
||||||
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
|
|
||||||
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
|
|
||||||
- View extender, tests (https://github.com/flarum/core/pull/2134)
|
|
||||||
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
|
|
||||||
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
|
|
||||||
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
|
|
||||||
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
|
|
||||||
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
|
|
||||||
- `ConfirmDocumentUnload` component
|
|
||||||
- Error handler middleware can now be manipulated by the middleware extender
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
|
|
||||||
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
|
|
||||||
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
|
|
||||||
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
|
|
||||||
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
|
|
||||||
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
|
|
||||||
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
|
|
||||||
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
|
|
||||||
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
|
|
||||||
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
|
|
||||||
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
|
|
||||||
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
|
|
||||||
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
|
|
||||||
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
|
|
||||||
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
|
|
||||||
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
|
|
||||||
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
|
|
||||||
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
|
|
||||||
- Replace `affixedSidebar` util with `AffixedSidebar` component
|
|
||||||
- Replace `m.withAttr` with `flarum/utils/withAttr`
|
|
||||||
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
|
|
||||||
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
|
|
||||||
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
|
|
||||||
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
|
|
||||||
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
|
|
||||||
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
|
|
||||||
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
|
|
||||||
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
|
|
||||||
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
|
|
||||||
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
|
|
||||||
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
|
|
||||||
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
|
|
||||||
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
|
|
||||||
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
|
|
||||||
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
|
|
||||||
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
|
|
||||||
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
|
|
||||||
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
|
|
||||||
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
|
|
||||||
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
|
|
||||||
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
|
|
||||||
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
|
|
||||||
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
|
|
||||||
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
|
|
||||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
|
|
||||||
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
|
|
||||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
|
|
||||||
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureApiRoutes` event class
|
|
||||||
- `Flarum\Event\ConfigureForumRoutes` event class
|
|
||||||
- `Flarum\Console\Event\Configuring` event class
|
|
||||||
- `Flarum\Event\ConfigureModelDates` event class
|
|
||||||
- `Flarum\Event\ConfigureLocales` event class
|
|
||||||
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
|
|
||||||
- `Flarum\Event\GetModelRelationship` event class
|
|
||||||
- `Flarum\User\Event\BioChanged` event class
|
|
||||||
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
|
|
||||||
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
|
|
||||||
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
- `Flarum\User\Event\GetDisplayName` event class
|
|
||||||
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
|
|
||||||
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
|
|
||||||
|
|
||||||
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
13
README.md
13
README.md
@@ -1,14 +1,12 @@
|
|||||||
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
|
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/flarum/core/actions?query=workflow%3ATests"><img src="https://github.com/flarum/core/workflows/Tests/badge.svg" alt="PHP Tests"></a>
|
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
|
||||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
|
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
|
||||||
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## About Flarum
|
## About Flarum
|
||||||
|
|
||||||
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
||||||
@@ -34,3 +32,4 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||||
|
|
||||||
|
@@ -1,17 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "flarum/core",
|
"name": "flarum/core",
|
||||||
"description": "Delightfully simple forum software.",
|
"description": "Delightfully simple forum software.",
|
||||||
"keywords": [
|
"keywords": ["forum", "discussion"],
|
||||||
"forum",
|
|
||||||
"discussion"
|
|
||||||
],
|
|
||||||
"homepage": "https://flarum.org/",
|
"homepage": "https://flarum.org/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Flarum",
|
"name": "Franz Liedke",
|
||||||
"email": "info@flarum.org",
|
"email": "franz@develophp.org"
|
||||||
"homepage": "https://flarum.org/team"
|
},
|
||||||
|
{
|
||||||
|
"name": "Daniel Klabbers",
|
||||||
|
"email": "daniel@klabbers.email",
|
||||||
|
"homepage": "https://luceos.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "David Sevilla Martin",
|
||||||
|
"email": "me+flarum@datitisev.me",
|
||||||
|
"homepage": "https://datitisev.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clark Winkelmann",
|
||||||
|
"email": "clark.winkelmann@gmail.com",
|
||||||
|
"homepage": "https://clarkwinkelmann.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matthew Kilgore",
|
||||||
|
"email": "matthew@kilgore.dev"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
@@ -20,38 +35,36 @@
|
|||||||
"docs": "https://flarum.org/docs/"
|
"docs": "https://flarum.org/docs/"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.3",
|
"php": ">=7.2",
|
||||||
"axy/sourcemap": "^0.1.4",
|
"axy/sourcemap": "^0.1.4",
|
||||||
"components/font-awesome": "^5.14.0",
|
"components/font-awesome": "^5.14.0",
|
||||||
"dflydev/fig-cookies": "^3.0.0",
|
"dflydev/fig-cookies": "^2.0.1",
|
||||||
"doctrine/dbal": "^2.7",
|
"doctrine/dbal": "^2.7",
|
||||||
"dragonmantank/cron-expression": "^3.1.0",
|
"franzl/whoops-middleware": "^0.4.0",
|
||||||
"franzl/whoops-middleware": "^2.0.0",
|
"illuminate/bus": "^6.0",
|
||||||
"illuminate/bus": "^8.0",
|
"illuminate/cache": "^6.0",
|
||||||
"illuminate/cache": "^8.0",
|
"illuminate/config": "^6.0",
|
||||||
"illuminate/config": "^8.0",
|
"illuminate/container": "^6.0",
|
||||||
"illuminate/console": "^8.0",
|
"illuminate/contracts": "^6.0",
|
||||||
"illuminate/container": "^8.0",
|
"illuminate/database": "^6.0",
|
||||||
"illuminate/contracts": "^8.0",
|
"illuminate/events": "^6.0",
|
||||||
"illuminate/database": "^8.0",
|
"illuminate/filesystem": "^6.0",
|
||||||
"illuminate/events": "^8.0",
|
"illuminate/hashing": "^6.0",
|
||||||
"illuminate/filesystem": "^8.0",
|
"illuminate/mail": "^6.0",
|
||||||
"illuminate/hashing": "^8.0",
|
"illuminate/queue": "^6.0",
|
||||||
"illuminate/mail": "^8.0",
|
"illuminate/session": "^6.0",
|
||||||
"illuminate/queue": "^8.0",
|
"illuminate/support": "^6.0",
|
||||||
"illuminate/session": "^8.0",
|
"illuminate/validation": "^6.0",
|
||||||
"illuminate/support": "^8.0",
|
"illuminate/view": "^6.0",
|
||||||
"illuminate/validation": "^8.0",
|
|
||||||
"illuminate/view": "^8.0",
|
|
||||||
"intervention/image": "^2.5.0",
|
"intervention/image": "^2.5.0",
|
||||||
"laminas/laminas-diactoros": "^2.4.1",
|
"laminas/laminas-diactoros": "^1.8.4",
|
||||||
"laminas/laminas-httphandlerrunner": "^1.2.0",
|
"laminas/laminas-httphandlerrunner": "^1.0",
|
||||||
"laminas/laminas-stratigility": "^3.2.2",
|
"laminas/laminas-stratigility": "^3.0",
|
||||||
"league/flysystem": "^1.0.11",
|
"league/flysystem": "^1.0.11",
|
||||||
"matthiasmullie/minify": "^1.3",
|
"matthiasmullie/minify": "^1.3",
|
||||||
"middlewares/base-path": "^2.0.1",
|
"middlewares/base-path": "^1.1",
|
||||||
"middlewares/base-path-router": "^2.0.1",
|
"middlewares/base-path-router": "^0.2.1",
|
||||||
"middlewares/request-handler": "^2.0.1",
|
"middlewares/request-handler": "^1.2",
|
||||||
"monolog/monolog": "^1.16.0",
|
"monolog/monolog": "^1.16.0",
|
||||||
"nesbot/carbon": "^2.0",
|
"nesbot/carbon": "^2.0",
|
||||||
"nikic/fast-route": "^0.6",
|
"nikic/fast-route": "^0.6",
|
||||||
@@ -59,17 +72,17 @@
|
|||||||
"psr/http-server-handler": "^1.0",
|
"psr/http-server-handler": "^1.0",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"s9e/text-formatter": "^2.3.6",
|
"s9e/text-formatter": "^2.3.6",
|
||||||
"symfony/config": "^5.2.2",
|
"symfony/config": "^3.3",
|
||||||
"symfony/console": "^5.2.2",
|
"symfony/console": "^4.2",
|
||||||
"symfony/event-dispatcher": "^5.2.2",
|
"symfony/event-dispatcher": "^4.3.2",
|
||||||
"symfony/mime": "^5.2.0",
|
"symfony/translation": "^3.3",
|
||||||
"symfony/translation": "^5.1.5",
|
"symfony/yaml": "^3.3",
|
||||||
"symfony/yaml": "^5.2.2",
|
|
||||||
"tobscure/json-api": "^0.3.0",
|
"tobscure/json-api": "^0.3.0",
|
||||||
"wikimedia/less.php": "^3.0"
|
"wikimedia/less.php": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"flarum/testing": "^0.1.0-beta.16"
|
"mockery/mockery": "^1.0",
|
||||||
|
"phpunit/phpunit": "^7.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"path": "./dist/*.js"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"defaultCompression": "gzip"
|
|
||||||
}
|
|
16
js/dist/admin.js
vendored
16
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
54
js/dist/forum.js
vendored
54
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
11642
js/package-lock.json
generated
11642
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,40 +2,32 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/core",
|
"name": "@flarum/core",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/preset-typescript": "^7.10.1",
|
||||||
|
"@types/mithril": "^2.0.3",
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"clsx": "^1.1.1",
|
"classnames": "^2.2.5",
|
||||||
"color-thief-browser": "^2.0.2",
|
"color-thief-browser": "^2.0.2",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.8.28",
|
||||||
"expose-loader": "^2.0.0",
|
"expose-loader": "^0.7.5",
|
||||||
"jquery": "^3.6.0",
|
"flarum-webpack-config": "0.1.0-beta.10",
|
||||||
|
"jquery": "^3.4.1",
|
||||||
"jquery.hotkeys": "^0.1.0",
|
"jquery.hotkeys": "^0.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.14",
|
||||||
|
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
||||||
"mithril": "^2.0.4",
|
"mithril": "^2.0.4",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
"spin.js": "^3.1.0",
|
"spin.js": "^3.1.0",
|
||||||
"textarea-caret": "^3.1.0"
|
"webpack": "^4.43.0",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-merge": "^4.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"husky": "^4.2.5",
|
||||||
"@types/jquery": "^3.5.5",
|
"prettier": "2.0.2"
|
||||||
"@types/lodash-es": "^4.17.4",
|
|
||||||
"@types/mithril": "^2.0.7",
|
|
||||||
"@types/punycode": "^2.1.0",
|
|
||||||
"@types/textarea-caret": "^3.0.0",
|
|
||||||
"bundlewatch": "^0.3.2",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"flarum-webpack-config": "0.1.0-beta.10",
|
|
||||||
"husky": "^4.3.8",
|
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"webpack": "^5.0.0",
|
|
||||||
"webpack-bundle-analyzer": "^4.4.1",
|
|
||||||
"webpack-cli": "^4.0.0",
|
|
||||||
"webpack-merge": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"analyze": "cross-env ANALYZER=true npm run build",
|
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"format-check": "prettier --check src"
|
"format-check": "prettier --check src"
|
||||||
},
|
},
|
||||||
|
34
js/shims.d.ts
vendored
34
js/shims.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
// Mithril
|
// Mithril
|
||||||
import Mithril from 'mithril';
|
import * as Mithril from 'mithril';
|
||||||
|
import Stream from 'mithril/stream';
|
||||||
|
|
||||||
// Other third-party libs
|
// Other third-party libs
|
||||||
import * as _dayjs from 'dayjs';
|
import * as _dayjs from 'dayjs';
|
||||||
@@ -8,6 +9,21 @@ import * as _$ from 'jquery';
|
|||||||
// Globals from flarum/core
|
// Globals from flarum/core
|
||||||
import Application from './src/common/Application';
|
import Application from './src/common/Application';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers that flarum/core patches into Mithril
|
||||||
|
*/
|
||||||
|
interface m extends Mithril.Static {
|
||||||
|
prop: typeof Stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export Mithril typings globally.
|
||||||
|
*
|
||||||
|
* This lets us use these typings without an extra import everywhere we use
|
||||||
|
* Mithril in a TypeScript file.
|
||||||
|
*/
|
||||||
|
export as namespace Mithril;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* flarum/core exposes several extensions globally:
|
* flarum/core exposes several extensions globally:
|
||||||
*
|
*
|
||||||
@@ -19,21 +35,9 @@ import Application from './src/common/Application';
|
|||||||
* to (and should not) bundle these themselves.
|
* to (and should not) bundle these themselves.
|
||||||
*/
|
*/
|
||||||
declare global {
|
declare global {
|
||||||
// $ is already defined by `@types/jquery`
|
const $: typeof _$;
|
||||||
const m: Mithril.Static;
|
const m: m;
|
||||||
const dayjs: typeof _dayjs;
|
const dayjs: typeof _dayjs;
|
||||||
|
|
||||||
// Extend JQuery with our custom functions, defined with $.fn
|
|
||||||
interface JQuery {
|
|
||||||
/**
|
|
||||||
* Creates a tooltip on a jQuery element reference.
|
|
||||||
*
|
|
||||||
* Optionally accepts placement and delay options.
|
|
||||||
*
|
|
||||||
* Returns the same reference to allow for method chaining.
|
|
||||||
*/
|
|
||||||
tooltip: (tooltipOptions?: { placement?: 'top' | 'bottom' | 'left' | 'right'; delay?: number }) => JQuery;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,16 +4,9 @@ import routes from './routes';
|
|||||||
import Application from '../common/Application';
|
import Application from '../common/Application';
|
||||||
import Navigation from '../common/components/Navigation';
|
import Navigation from '../common/components/Navigation';
|
||||||
import AdminNav from './components/AdminNav';
|
import AdminNav from './components/AdminNav';
|
||||||
import ExtensionData from './utils/ExtensionData';
|
|
||||||
|
|
||||||
export default class AdminApplication extends Application {
|
export default class AdminApplication extends Application {
|
||||||
extensionData = new ExtensionData();
|
extensionSettings = {};
|
||||||
|
|
||||||
extensionCategories = {
|
|
||||||
feature: 30,
|
|
||||||
theme: 20,
|
|
||||||
language: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
history = {
|
history = {
|
||||||
canGoBack: () => true,
|
canGoBack: () => true,
|
||||||
@@ -34,24 +27,27 @@ export default class AdminApplication extends Application {
|
|||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
mount() {
|
mount() {
|
||||||
|
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
||||||
|
m.mount(document.getElementById('header-navigation'), Navigation);
|
||||||
|
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
||||||
|
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
||||||
|
m.mount(document.getElementById('admin-navigation'), AdminNav);
|
||||||
|
|
||||||
// Mithril does not render the home route on https://example.com/admin, so
|
// Mithril does not render the home route on https://example.com/admin, so
|
||||||
// we need to go to https://example.com/admin#/ explicitly.
|
// we need to go to https://example.com/admin#/ explicitly.
|
||||||
if (!document.location.hash) document.location.hash = '#/';
|
if (!document.location.hash) document.location.hash = '#/';
|
||||||
|
|
||||||
m.route.prefix = '#';
|
m.route.prefix = '#';
|
||||||
|
|
||||||
super.mount();
|
super.mount();
|
||||||
|
|
||||||
m.mount(document.getElementById('app-navigation'), {
|
// If an extension has just been enabled, then we will run its settings
|
||||||
view: () =>
|
// callback.
|
||||||
Navigation.component({
|
const enabled = localStorage.getItem('enabledExtension');
|
||||||
className: 'App-backControl',
|
if (enabled && this.extensionSettings[enabled]) {
|
||||||
drawer: true,
|
this.extensionSettings[enabled]();
|
||||||
}),
|
localStorage.removeItem('enabledExtension');
|
||||||
});
|
}
|
||||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
|
||||||
m.mount(document.getElementById('admin-navigation'), AdminNav);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequiredPermissions(permission) {
|
getRequiredPermissions(permission) {
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
import Admin from './AdminApplication';
|
|
||||||
|
|
||||||
const app = new Admin();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
window.app = app;
|
|
||||||
|
|
||||||
export default app;
|
|
@@ -1,24 +1,19 @@
|
|||||||
import compat from '../common/compat';
|
import compat from '../common/compat';
|
||||||
|
|
||||||
import saveSettings from './utils/saveSettings';
|
import saveSettings from './utils/saveSettings';
|
||||||
import ExtensionData from './utils/ExtensionData';
|
|
||||||
import isExtensionEnabled from './utils/isExtensionEnabled';
|
|
||||||
import getCategorizedExtensions from './utils/getCategorizedExtensions';
|
|
||||||
import SettingDropdown from './components/SettingDropdown';
|
import SettingDropdown from './components/SettingDropdown';
|
||||||
import EditCustomFooterModal from './components/EditCustomFooterModal';
|
import EditCustomFooterModal from './components/EditCustomFooterModal';
|
||||||
import SessionDropdown from './components/SessionDropdown';
|
import SessionDropdown from './components/SessionDropdown';
|
||||||
import HeaderPrimary from './components/HeaderPrimary';
|
import HeaderPrimary from './components/HeaderPrimary';
|
||||||
import AdminPage from './components/AdminPage';
|
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
import StatusWidget from './components/StatusWidget';
|
import StatusWidget from './components/StatusWidget';
|
||||||
import ExtensionsWidget from './components/ExtensionsWidget';
|
|
||||||
import HeaderSecondary from './components/HeaderSecondary';
|
import HeaderSecondary from './components/HeaderSecondary';
|
||||||
import SettingsModal from './components/SettingsModal';
|
import SettingsModal from './components/SettingsModal';
|
||||||
import DashboardWidget from './components/DashboardWidget';
|
import DashboardWidget from './components/DashboardWidget';
|
||||||
import ExtensionPage from './components/ExtensionPage';
|
import AddExtensionModal from './components/AddExtensionModal';
|
||||||
import ExtensionLinkButton from './components/ExtensionLinkButton';
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
|
import AdminLinkButton from './components/AdminLinkButton';
|
||||||
import PermissionGrid from './components/PermissionGrid';
|
import PermissionGrid from './components/PermissionGrid';
|
||||||
import ExtensionPermissionGrid from './components/ExtensionPermissionGrid';
|
|
||||||
import MailPage from './components/MailPage';
|
import MailPage from './components/MailPage';
|
||||||
import UploadImageButton from './components/UploadImageButton';
|
import UploadImageButton from './components/UploadImageButton';
|
||||||
import LoadingModal from './components/LoadingModal';
|
import LoadingModal from './components/LoadingModal';
|
||||||
@@ -28,7 +23,6 @@ import EditCustomHeaderModal from './components/EditCustomHeaderModal';
|
|||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
import PermissionDropdown from './components/PermissionDropdown';
|
import PermissionDropdown from './components/PermissionDropdown';
|
||||||
import AdminNav from './components/AdminNav';
|
import AdminNav from './components/AdminNav';
|
||||||
import AdminHeader from './components/AdminHeader';
|
|
||||||
import EditCustomCssModal from './components/EditCustomCssModal';
|
import EditCustomCssModal from './components/EditCustomCssModal';
|
||||||
import EditGroupModal from './components/EditGroupModal';
|
import EditGroupModal from './components/EditGroupModal';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
@@ -36,24 +30,19 @@ import AdminApplication from './AdminApplication';
|
|||||||
|
|
||||||
export default Object.assign(compat, {
|
export default Object.assign(compat, {
|
||||||
'utils/saveSettings': saveSettings,
|
'utils/saveSettings': saveSettings,
|
||||||
'utils/ExtensionData': ExtensionData,
|
|
||||||
'utils/isExtensionEnabled': isExtensionEnabled,
|
|
||||||
'utils/getCategorizedExtensions': getCategorizedExtensions,
|
|
||||||
'components/SettingDropdown': SettingDropdown,
|
'components/SettingDropdown': SettingDropdown,
|
||||||
'components/EditCustomFooterModal': EditCustomFooterModal,
|
'components/EditCustomFooterModal': EditCustomFooterModal,
|
||||||
'components/SessionDropdown': SessionDropdown,
|
'components/SessionDropdown': SessionDropdown,
|
||||||
'components/HeaderPrimary': HeaderPrimary,
|
'components/HeaderPrimary': HeaderPrimary,
|
||||||
'components/AdminPage': AdminPage,
|
|
||||||
'components/AppearancePage': AppearancePage,
|
'components/AppearancePage': AppearancePage,
|
||||||
'components/StatusWidget': StatusWidget,
|
'components/StatusWidget': StatusWidget,
|
||||||
'components/ExtensionsWidget': ExtensionsWidget,
|
|
||||||
'components/HeaderSecondary': HeaderSecondary,
|
'components/HeaderSecondary': HeaderSecondary,
|
||||||
'components/SettingsModal': SettingsModal,
|
'components/SettingsModal': SettingsModal,
|
||||||
'components/DashboardWidget': DashboardWidget,
|
'components/DashboardWidget': DashboardWidget,
|
||||||
'components/ExtensionPage': ExtensionPage,
|
'components/AddExtensionModal': AddExtensionModal,
|
||||||
'components/ExtensionLinkButton': ExtensionLinkButton,
|
'components/ExtensionsPage': ExtensionsPage,
|
||||||
|
'components/AdminLinkButton': AdminLinkButton,
|
||||||
'components/PermissionGrid': PermissionGrid,
|
'components/PermissionGrid': PermissionGrid,
|
||||||
'components/ExtensionPermissionGrid': ExtensionPermissionGrid,
|
|
||||||
'components/MailPage': MailPage,
|
'components/MailPage': MailPage,
|
||||||
'components/UploadImageButton': UploadImageButton,
|
'components/UploadImageButton': UploadImageButton,
|
||||||
'components/LoadingModal': LoadingModal,
|
'components/LoadingModal': LoadingModal,
|
||||||
@@ -63,7 +52,6 @@ export default Object.assign(compat, {
|
|||||||
'components/PermissionsPage': PermissionsPage,
|
'components/PermissionsPage': PermissionsPage,
|
||||||
'components/PermissionDropdown': PermissionDropdown,
|
'components/PermissionDropdown': PermissionDropdown,
|
||||||
'components/AdminNav': AdminNav,
|
'components/AdminNav': AdminNav,
|
||||||
'components/AdminHeader': AdminHeader,
|
|
||||||
'components/EditCustomCssModal': EditCustomCssModal,
|
'components/EditCustomCssModal': EditCustomCssModal,
|
||||||
'components/EditGroupModal': EditGroupModal,
|
'components/EditGroupModal': EditGroupModal,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
|
32
js/src/admin/components/AddExtensionModal.js
Normal file
32
js/src/admin/components/AddExtensionModal.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Modal from '../../common/components/Modal';
|
||||||
|
|
||||||
|
export default class AddExtensionModal extends Modal {
|
||||||
|
className() {
|
||||||
|
return 'AddExtensionModal Modal--small';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return app.translator.trans('core.admin.add_extension.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="Modal-body">
|
||||||
|
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
|
||||||
|
<p>
|
||||||
|
{app.translator.trans('core.admin.add_extension.install_text', { a: <a href="https://discuss.flarum.org/t/extensions" target="_blank" /> })}
|
||||||
|
</p>
|
||||||
|
<p>{app.translator.trans('core.admin.add_extension.developer_text', { a: <a href="http://flarum.org/docs/extend" target="_blank" /> })}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,19 +0,0 @@
|
|||||||
import Component from '../../common/Component';
|
|
||||||
import classList from '../../common/utils/classList';
|
|
||||||
import icon from '../../common/helpers/icon';
|
|
||||||
|
|
||||||
export default class AdminHeader extends Component {
|
|
||||||
view(vnode) {
|
|
||||||
return [
|
|
||||||
<div className={classList(['AdminHeader', this.attrs.className])}>
|
|
||||||
<div className="container">
|
|
||||||
<h2>
|
|
||||||
{icon(this.attrs.icon)}
|
|
||||||
{vnode.children}
|
|
||||||
</h2>
|
|
||||||
<div className="AdminHeader-description">{this.attrs.description}</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
16
js/src/admin/components/AdminLinkButton.js
Normal file
16
js/src/admin/components/AdminLinkButton.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
|
||||||
|
export default class AdminLinkButton extends LinkButton {
|
||||||
|
getButtonContent(children) {
|
||||||
|
return [...super.getButtonContent(children), <div className="AdminLinkButton-description">{this.attrs.description}</div>];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,151 +1,106 @@
|
|||||||
import ExtensionLinkButton from './ExtensionLinkButton';
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import LinkButton from '../../common/components/LinkButton';
|
import AdminLinkButton from './AdminLinkButton';
|
||||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
|
|
||||||
export default class AdminNav extends Component {
|
export default class AdminNav extends Component {
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.query = Stream('');
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<SelectDropdown className="AdminNav App-titleControl AdminNav-Main" buttonClassName="Button">
|
<SelectDropdown className="AdminNav App-titleControl" buttonClassName="Button">
|
||||||
{this.items().toArray().concat(this.extensionItems().toArray())}
|
{this.items().toArray()}
|
||||||
</SelectDropdown>
|
</SelectDropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.scrollToActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
onupdate() {
|
|
||||||
this.scrollToActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToActive() {
|
|
||||||
const children = $('.Dropdown-menu').children('.active');
|
|
||||||
const nav = $('#admin-navigation');
|
|
||||||
const time = app.previous.type ? 250 : 0;
|
|
||||||
|
|
||||||
if (
|
|
||||||
children.length > 0 &&
|
|
||||||
(children[0].offsetTop > nav.scrollTop() + nav.outerHeight() || children[0].offsetTop + children[0].offsetHeight < nav.scrollTop())
|
|
||||||
) {
|
|
||||||
nav.animate(
|
|
||||||
{
|
|
||||||
scrollTop: children[0].offsetTop - nav.height() / 2,
|
|
||||||
},
|
|
||||||
time
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an item list of main links to show in the admin navigation.
|
* Build an item list of links to show in the admin navigation.
|
||||||
*
|
*
|
||||||
* @return {ItemList}
|
* @return {ItemList}
|
||||||
*/
|
*/
|
||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add('category-core', <h4 className="ExtensionListTitle">{app.translator.trans('core.admin.nav.categories.core')}</h4>);
|
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'dashboard',
|
'dashboard',
|
||||||
<LinkButton href={app.route('dashboard')} icon="far fa-chart-bar" title={app.translator.trans('core.admin.nav.dashboard_title')}>
|
AdminLinkButton.component(
|
||||||
{app.translator.trans('core.admin.nav.dashboard_button')}
|
{
|
||||||
</LinkButton>
|
href: app.route('dashboard'),
|
||||||
|
icon: 'far fa-chart-bar',
|
||||||
|
description: app.translator.trans('core.admin.nav.dashboard_text'),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.nav.dashboard_button')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'basics',
|
'basics',
|
||||||
<LinkButton href={app.route('basics')} icon="fas fa-pencil-alt" title={app.translator.trans('core.admin.nav.basics_title')}>
|
AdminLinkButton.component(
|
||||||
{app.translator.trans('core.admin.nav.basics_button')}
|
{
|
||||||
</LinkButton>
|
href: app.route('basics'),
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
description: app.translator.trans('core.admin.nav.basics_text'),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.nav.basics_button')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'mail',
|
'mail',
|
||||||
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>
|
AdminLinkButton.component(
|
||||||
{app.translator.trans('core.admin.nav.email_button')}
|
{
|
||||||
</LinkButton>
|
href: app.route('mail'),
|
||||||
|
icon: 'fas fa-envelope',
|
||||||
|
description: app.translator.trans('core.admin.nav.email_text'),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.nav.email_button')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'permissions',
|
'permissions',
|
||||||
<LinkButton href={app.route('permissions')} icon="fas fa-key" title={app.translator.trans('core.admin.nav.permissions_title')}>
|
AdminLinkButton.component(
|
||||||
{app.translator.trans('core.admin.nav.permissions_button')}
|
{
|
||||||
</LinkButton>
|
href: app.route('permissions'),
|
||||||
|
icon: 'fas fa-key',
|
||||||
|
description: app.translator.trans('core.admin.nav.permissions_text'),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.nav.permissions_button')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'appearance',
|
'appearance',
|
||||||
<LinkButton href={app.route('appearance')} icon="fas fa-paint-brush" title={app.translator.trans('core.admin.nav.appearance_title')}>
|
AdminLinkButton.component(
|
||||||
{app.translator.trans('core.admin.nav.appearance_button')}
|
{
|
||||||
</LinkButton>
|
href: app.route('appearance'),
|
||||||
|
icon: 'fas fa-paint-brush',
|
||||||
|
description: app.translator.trans('core.admin.nav.appearance_text'),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.nav.appearance_button')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'search',
|
'extensions',
|
||||||
<div className="Search-input">
|
AdminLinkButton.component(
|
||||||
<input
|
{
|
||||||
className="FormControl SearchBar"
|
href: app.route('extensions'),
|
||||||
bidi={this.query}
|
icon: 'fas fa-puzzle-piece',
|
||||||
type="search"
|
description: app.translator.trans('core.admin.nav.extensions_text'),
|
||||||
placeholder={app.translator.trans('core.admin.nav.search_placeholder')}
|
},
|
||||||
/>
|
app.translator.trans('core.admin.nav.extensions_button')
|
||||||
</div>
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
const categorizedExtensions = getCategorizedExtensions();
|
|
||||||
const categories = app.extensionCategories;
|
|
||||||
|
|
||||||
Object.keys(categorizedExtensions).map((category) => {
|
|
||||||
if (!this.query()) {
|
|
||||||
items.add(
|
|
||||||
`category-${category}`,
|
|
||||||
<h4 className="ExtensionListTitle">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>,
|
|
||||||
categories[category]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
categorizedExtensions[category].map((extension) => {
|
|
||||||
const query = this.query().toUpperCase();
|
|
||||||
const title = extension.extra['flarum-extension'].title || '';
|
|
||||||
const description = extension.description || '';
|
|
||||||
|
|
||||||
if (!query || title.toUpperCase().includes(query) || description.toUpperCase().includes(query)) {
|
|
||||||
items.add(
|
|
||||||
`extension-${extension.id}`,
|
|
||||||
<ExtensionLinkButton
|
|
||||||
href={app.route('extension', { id: extension.id })}
|
|
||||||
extensionId={extension.id}
|
|
||||||
className="ExtensionNavButton"
|
|
||||||
title={description}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</ExtensionLinkButton>,
|
|
||||||
categories[category]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,180 +0,0 @@
|
|||||||
import Page from '../../common/components/Page';
|
|
||||||
import Button from '../../common/components/Button';
|
|
||||||
import Switch from '../../common/components/Switch';
|
|
||||||
import Select from '../../common/components/Select';
|
|
||||||
import classList from '../../common/utils/classList';
|
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
import saveSettings from '../utils/saveSettings';
|
|
||||||
import AdminHeader from './AdminHeader';
|
|
||||||
|
|
||||||
export default class AdminPage extends Page {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.settings = {};
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
const className = classList(['AdminPage', this.headerInfo().className]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{this.header()}
|
|
||||||
<div className="container">{this.content()}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
submitButton() {
|
|
||||||
return (
|
|
||||||
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}>
|
|
||||||
{app.translator.trans('core.admin.settings.submit_button')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
header() {
|
|
||||||
const headerInfo = this.headerInfo();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminHeader icon={headerInfo.icon} description={headerInfo.description} className={headerInfo.className + '-header'}>
|
|
||||||
{headerInfo.title}
|
|
||||||
</AdminHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
headerInfo() {
|
|
||||||
return {
|
|
||||||
className: '',
|
|
||||||
icon: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* buildSettingComponent takes a settings object and turns it into a component.
|
|
||||||
* Depending on the type of input, you can set the type to 'bool', 'select', or
|
|
||||||
* any standard <input> type. Any values inside the 'extra' object will be added
|
|
||||||
* to the component as an attribute.
|
|
||||||
*
|
|
||||||
* Alternatively, you can pass a callback that will be executed in ExtensionPage's
|
|
||||||
* context to include custom JSX elements.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* setting: 'acme.checkbox',
|
|
||||||
* label: app.translator.trans('acme.admin.setting_label'),
|
|
||||||
* type: 'bool',
|
|
||||||
* help: app.translator.trans('acme.admin.setting_help'),
|
|
||||||
* className: 'Setting-item'
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* setting: 'acme.select',
|
|
||||||
* label: app.translator.trans('acme.admin.setting_label'),
|
|
||||||
* type: 'select',
|
|
||||||
* options: {
|
|
||||||
* 'option1': 'Option 1 label',
|
|
||||||
* 'option2': 'Option 2 label',
|
|
||||||
* },
|
|
||||||
* default: 'option1',
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param setting
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
*/
|
|
||||||
buildSettingComponent(entry) {
|
|
||||||
if (typeof entry === 'function') {
|
|
||||||
return entry.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { setting, help, ...componentAttrs } = entry;
|
|
||||||
|
|
||||||
delete componentAttrs.help;
|
|
||||||
|
|
||||||
const value = this.setting([setting])();
|
|
||||||
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
|
|
||||||
return (
|
|
||||||
<div className="Form-group">
|
|
||||||
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
|
|
||||||
{componentAttrs.label}
|
|
||||||
</Switch>
|
|
||||||
<div className="helpText">{help}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
|
|
||||||
return (
|
|
||||||
<div className="Form-group">
|
|
||||||
<label>{componentAttrs.label}</label>
|
|
||||||
<div className="helpText">{help}</div>
|
|
||||||
<Select
|
|
||||||
value={value || componentAttrs.default}
|
|
||||||
options={componentAttrs.options}
|
|
||||||
buttonClassName="Button"
|
|
||||||
onchange={this.settings[setting]}
|
|
||||||
{...componentAttrs}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
|
|
||||||
return (
|
|
||||||
<div className="Form-group">
|
|
||||||
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
|
|
||||||
<div className="helpText">{help}</div>
|
|
||||||
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onsaved() {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.settings.saved_message'));
|
|
||||||
}
|
|
||||||
|
|
||||||
setting(key, fallback = '') {
|
|
||||||
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
|
|
||||||
|
|
||||||
return this.settings[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty() {
|
|
||||||
const dirty = {};
|
|
||||||
|
|
||||||
Object.keys(this.settings).forEach((key) => {
|
|
||||||
const value = this.settings[key]();
|
|
||||||
|
|
||||||
if (value !== app.data.settings[key]) {
|
|
||||||
dirty[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return dirty;
|
|
||||||
}
|
|
||||||
|
|
||||||
isChanged() {
|
|
||||||
return Object.keys(this.dirty()).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
app.alerts.clear();
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
return saveSettings(this.dirty()).then(this.onsaved.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,67 +1,75 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
|
import Switch from '../../common/components/Switch';
|
||||||
import EditCustomCssModal from './EditCustomCssModal';
|
import EditCustomCssModal from './EditCustomCssModal';
|
||||||
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||||
import EditCustomFooterModal from './EditCustomFooterModal';
|
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||||
import UploadImageButton from './UploadImageButton';
|
import UploadImageButton from './UploadImageButton';
|
||||||
import AdminPage from './AdminPage';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
import withAttr from '../../common/utils/withAttr';
|
||||||
|
|
||||||
export default class AppearancePage extends AdminPage {
|
export default class AppearancePage extends Page {
|
||||||
headerInfo() {
|
oninit(vnode) {
|
||||||
return {
|
super.oninit(vnode);
|
||||||
className: 'AppearancePage',
|
|
||||||
icon: 'fas fa-paint-brush',
|
this.primaryColor = m.stream(app.data.settings.theme_primary_color);
|
||||||
title: app.translator.trans('core.admin.appearance.title'),
|
this.secondaryColor = m.stream(app.data.settings.theme_secondary_color);
|
||||||
description: app.translator.trans('core.admin.appearance.description'),
|
this.darkMode = m.stream(app.data.settings.theme_dark_mode);
|
||||||
};
|
this.coloredHeader = m.stream(app.data.settings.theme_colored_header);
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
view() {
|
||||||
return [
|
return (
|
||||||
<div className="Form">
|
<div className="AppearancePage">
|
||||||
|
<div className="container">
|
||||||
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
<fieldset className="AppearancePage-colors">
|
<fieldset className="AppearancePage-colors">
|
||||||
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
|
<div className="helpText">{app.translator.trans('core.admin.appearance.colors_text')}</div>
|
||||||
|
|
||||||
<div className="AppearancePage-colors-input">
|
<div className="AppearancePage-colors-input">
|
||||||
{this.buildSettingComponent({
|
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.primaryColor} />
|
||||||
type: 'text',
|
<input className="FormControl" type="text" placeholder="#aaaaaa" bidi={this.secondaryColor} />
|
||||||
setting: 'theme_primary_color',
|
|
||||||
placeholder: '#aaaaaa',
|
|
||||||
})}
|
|
||||||
{this.buildSettingComponent({
|
|
||||||
type: 'text',
|
|
||||||
setting: 'theme_secondary_color',
|
|
||||||
placeholder: '#aaaaaa',
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.buildSettingComponent({
|
{Switch.component(
|
||||||
type: 'switch',
|
{
|
||||||
setting: 'theme_dark_mode',
|
state: this.darkMode(),
|
||||||
label: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
onchange: this.darkMode,
|
||||||
})}
|
},
|
||||||
|
app.translator.trans('core.admin.appearance.dark_mode_label')
|
||||||
|
)}
|
||||||
|
|
||||||
{this.buildSettingComponent({
|
{Switch.component(
|
||||||
type: 'switch',
|
{
|
||||||
setting: 'theme_colored_header',
|
state: this.coloredHeader(),
|
||||||
label: app.translator.trans('core.admin.appearance.colored_header_label'),
|
onchange: this.coloredHeader,
|
||||||
})}
|
},
|
||||||
|
app.translator.trans('core.admin.appearance.colored_header_label')
|
||||||
|
)}
|
||||||
|
|
||||||
{this.submitButton()}
|
{Button.component(
|
||||||
|
{
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
type: 'submit',
|
||||||
|
loading: this.loading,
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.appearance.submit_button')
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>,
|
</form>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
|
<div className="helpText">{app.translator.trans('core.admin.appearance.logo_text')}</div>
|
||||||
<UploadImageButton name="logo" />
|
<UploadImageButton name="logo" />
|
||||||
</fieldset>,
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
|
<div className="helpText">{app.translator.trans('core.admin.appearance.favicon_text')}</div>
|
||||||
<UploadImageButton name="favicon" />
|
<UploadImageButton name="favicon" />
|
||||||
</fieldset>,
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
||||||
@@ -73,7 +81,7 @@ export default class AppearancePage extends AdminPage {
|
|||||||
},
|
},
|
||||||
app.translator.trans('core.admin.appearance.edit_header_button')
|
app.translator.trans('core.admin.appearance.edit_header_button')
|
||||||
)}
|
)}
|
||||||
</fieldset>,
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
|
||||||
@@ -85,7 +93,7 @@ export default class AppearancePage extends AdminPage {
|
|||||||
},
|
},
|
||||||
app.translator.trans('core.admin.appearance.edit_footer_button')
|
app.translator.trans('core.admin.appearance.edit_footer_button')
|
||||||
)}
|
)}
|
||||||
</fieldset>,
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
||||||
@@ -97,24 +105,29 @@ export default class AppearancePage extends AdminPage {
|
|||||||
},
|
},
|
||||||
app.translator.trans('core.admin.appearance.edit_css_button')
|
app.translator.trans('core.admin.appearance.edit_css_button')
|
||||||
)}
|
)}
|
||||||
</fieldset>,
|
</fieldset>
|
||||||
];
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onsaved() {
|
onsubmit(e) {
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
||||||
|
|
||||||
if (!hex.test(this.settings['theme_primary_color']()) || !hex.test(this.settings['theme_secondary_color']())) {
|
if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
|
||||||
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
|
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.saveSettings(e);
|
this.loading = true;
|
||||||
|
|
||||||
|
saveSettings({
|
||||||
|
theme_primary_color: this.primaryColor(),
|
||||||
|
theme_secondary_color: this.secondaryColor(),
|
||||||
|
theme_dark_mode: this.darkMode(),
|
||||||
|
theme_colored_header: this.coloredHeader(),
|
||||||
|
}).then(() => window.location.reload());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,33 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
import FieldSet from '../../common/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
|
import Select from '../../common/components/Select';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import saveSettings from '../utils/saveSettings';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import AdminPage from './AdminPage';
|
import Switch from '../../common/components/Switch';
|
||||||
|
import withAttr from '../../common/utils/withAttr';
|
||||||
|
|
||||||
export default class BasicsPage extends AdminPage {
|
export default class BasicsPage extends Page {
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.fields = [
|
||||||
|
'forum_title',
|
||||||
|
'forum_description',
|
||||||
|
'default_locale',
|
||||||
|
'show_language_selector',
|
||||||
|
'default_route',
|
||||||
|
'welcome_title',
|
||||||
|
'welcome_message',
|
||||||
|
'display_name_driver',
|
||||||
|
];
|
||||||
|
this.values = {};
|
||||||
|
|
||||||
|
const settings = app.data.settings;
|
||||||
|
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
|
||||||
|
|
||||||
this.localeOptions = {};
|
this.localeOptions = {};
|
||||||
const locales = app.data.locales;
|
const locales = app.data.locales;
|
||||||
for (const i in locales) {
|
for (const i in locales) {
|
||||||
@@ -18,101 +40,125 @@ export default class BasicsPage extends AdminPage {
|
|||||||
this.displayNameOptions[identifier] = identifier;
|
this.displayNameOptions[identifier] = identifier;
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.slugDriverOptions = {};
|
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
|
||||||
Object.keys(app.data.slugDrivers).forEach((model) => {
|
|
||||||
this.slugDriverOptions[model] = {};
|
|
||||||
|
|
||||||
app.data.slugDrivers[model].forEach((option) => {
|
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
|
||||||
this.slugDriverOptions[model][option] = option;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
headerInfo() {
|
view() {
|
||||||
return {
|
return (
|
||||||
className: 'BasicsPage',
|
<div className="BasicsPage">
|
||||||
icon: 'fas fa-pencil-alt',
|
<div className="container">
|
||||||
title: app.translator.trans('core.admin.basics.title'),
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
description: app.translator.trans('core.admin.basics.description'),
|
{FieldSet.component(
|
||||||
};
|
{
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
<div className="Form">
|
|
||||||
{this.buildSettingComponent({
|
|
||||||
type: 'text',
|
|
||||||
setting: 'forum_title',
|
|
||||||
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||||
})}
|
},
|
||||||
{this.buildSettingComponent({
|
[<input className="FormControl" bidi={this.values.forum_title} />]
|
||||||
type: 'text',
|
)}
|
||||||
setting: 'forum_description',
|
|
||||||
|
{FieldSet.component(
|
||||||
|
{
|
||||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||||
help: app.translator.trans('core.admin.basics.forum_description_text'),
|
},
|
||||||
})}
|
[
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.basics.forum_description_text')}</div>,
|
||||||
|
<textarea className="FormControl" bidi={this.values.forum_description} />,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.keys(this.localeOptions).length > 1
|
{Object.keys(this.localeOptions).length > 1
|
||||||
? [
|
? FieldSet.component(
|
||||||
this.buildSettingComponent({
|
{
|
||||||
type: 'select',
|
|
||||||
setting: 'default_locale',
|
|
||||||
options: this.localeOptions,
|
|
||||||
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
Select.component({
|
||||||
|
options: this.localeOptions,
|
||||||
|
value: this.values.default_locale(),
|
||||||
|
onchange: this.values.default_locale,
|
||||||
}),
|
}),
|
||||||
this.buildSettingComponent({
|
Switch.component(
|
||||||
type: 'switch',
|
{
|
||||||
setting: 'show_language_selector',
|
state: this.values.show_language_selector(),
|
||||||
label: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
onchange: this.values.show_language_selector,
|
||||||
}),
|
},
|
||||||
|
app.translator.trans('core.admin.basics.show_language_selector_label')
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
)
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
<FieldSet className="BasicsPage-homePage Form-group" label={app.translator.trans('core.admin.basics.home_page_heading')}>
|
{FieldSet.component(
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>
|
{
|
||||||
{this.homePageItems()
|
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||||
|
className: 'BasicsPage-homePage',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.basics.home_page_text')}</div>,
|
||||||
|
this.homePageItems()
|
||||||
.toArray()
|
.toArray()
|
||||||
.map(({ path, label }) => (
|
.map(({ path, label }) => (
|
||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input type="radio" name="homePage" value={path} bidi={this.setting('default_route')} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="homePage"
|
||||||
|
value={path}
|
||||||
|
checked={this.values.default_route() === path}
|
||||||
|
onclick={withAttr('value', this.values.default_route)}
|
||||||
|
/>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
))}
|
)),
|
||||||
</FieldSet>
|
]
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="Form-group BasicsPage-welcomeBanner-input">
|
{FieldSet.component(
|
||||||
<label>{app.translator.trans('core.admin.basics.welcome_banner_heading')}</label>
|
{
|
||||||
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>
|
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||||
<input type="text" className="FormControl" bidi={this.setting('welcome_title')} />
|
className: 'BasicsPage-welcomeBanner',
|
||||||
<textarea className="FormControl" bidi={this.setting('welcome_message')} />
|
},
|
||||||
</div>
|
[
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.basics.welcome_banner_text')}</div>,
|
||||||
|
<div className="BasicsPage-welcomeBanner-input">
|
||||||
|
<input className="FormControl" bidi={this.values.welcome_title} />
|
||||||
|
<textarea className="FormControl" bidi={this.values.welcome_message} />
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.keys(this.displayNameOptions).length > 1
|
{Object.keys(this.displayNameOptions).length > 1
|
||||||
? this.buildSettingComponent({
|
? FieldSet.component(
|
||||||
type: 'select',
|
{
|
||||||
setting: 'display_name_driver',
|
|
||||||
options: this.displayNameOptions,
|
|
||||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||||
help: app.translator.trans('core.admin.basics.display_name_text'),
|
},
|
||||||
})
|
[
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
||||||
|
Select.component({
|
||||||
|
options: this.displayNameOptions,
|
||||||
|
bidi: this.values.display_name_driver,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
{Object.keys(this.slugDriverOptions).map((model) => {
|
{Button.component(
|
||||||
const options = this.slugDriverOptions[model];
|
{
|
||||||
if (Object.keys(options).length > 1) {
|
type: 'submit',
|
||||||
return this.buildSettingComponent({
|
className: 'Button Button--primary',
|
||||||
type: 'select',
|
loading: this.loading,
|
||||||
setting: `slug_driver_${model}`,
|
disabled: !this.changed(),
|
||||||
options,
|
},
|
||||||
label: app.translator.trans('core.admin.basics.slug_driver_heading', { model }),
|
app.translator.trans('core.admin.basics.submit_button')
|
||||||
help: app.translator.trans('core.admin.basics.slug_driver_text', { model }),
|
)}
|
||||||
});
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})}
|
|
||||||
|
|
||||||
{this.submitButton()}
|
changed() {
|
||||||
</div>,
|
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,4 +178,29 @@ export default class BasicsPage extends AdminPage {
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
this.fields.forEach((key) => (settings[key] = this.values[key]()));
|
||||||
|
|
||||||
|
saveSettings(settings)
|
||||||
|
.then(() => {
|
||||||
|
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,29 +1,16 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
import StatusWidget from './StatusWidget';
|
import StatusWidget from './StatusWidget';
|
||||||
import ExtensionsWidget from './ExtensionsWidget';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
import AdminPage from './AdminPage';
|
|
||||||
|
|
||||||
export default class DashboardPage extends AdminPage {
|
export default class DashboardPage extends Page {
|
||||||
headerInfo() {
|
view() {
|
||||||
return {
|
return (
|
||||||
className: 'DashboardPage',
|
<div className="DashboardPage">
|
||||||
icon: 'fas fa-chart-bar',
|
<div className="container">{this.availableWidgets()}</div>
|
||||||
title: app.translator.trans('core.admin.dashboard.title'),
|
</div>
|
||||||
description: app.translator.trans('core.admin.dashboard.description'),
|
);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return this.availableWidgets().toArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
availableWidgets() {
|
availableWidgets() {
|
||||||
const items = new ItemList();
|
return [<StatusWidget />];
|
||||||
|
|
||||||
items.add('status', <StatusWidget />, 30);
|
|
||||||
|
|
||||||
items.add('extensions', <ExtensionsWidget />, 10);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import Badge from '../../common/components/Badge';
|
|||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Switch from '../../common/components/Switch';
|
import Switch from '../../common/components/Switch';
|
||||||
import Stream from '../../common/utils/Stream';
|
import withAttr from '../../common/utils/withAttr';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||||
@@ -16,11 +16,11 @@ export default class EditGroupModal extends Modal {
|
|||||||
|
|
||||||
this.group = this.attrs.group || app.store.createRecord('groups');
|
this.group = this.attrs.group || app.store.createRecord('groups');
|
||||||
|
|
||||||
this.nameSingular = Stream(this.group.nameSingular() || '');
|
this.nameSingular = m.stream(this.group.nameSingular() || '');
|
||||||
this.namePlural = Stream(this.group.namePlural() || '');
|
this.namePlural = m.stream(this.group.namePlural() || '');
|
||||||
this.icon = Stream(this.group.icon() || '');
|
this.icon = m.stream(this.group.icon() || '');
|
||||||
this.color = Stream(this.group.color() || '');
|
this.color = m.stream(this.group.color() || '');
|
||||||
this.isHidden = Stream(this.group.isHidden() || false);
|
this.isHidden = m.stream(this.group.isHidden() || false);
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
|
||||||
import LinkButton from '../../common/components/LinkButton';
|
|
||||||
import icon from '../../common/helpers/icon';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
|
|
||||||
export default class ExtensionLinkButton extends LinkButton {
|
|
||||||
getButtonContent(children) {
|
|
||||||
const content = super.getButtonContent(children);
|
|
||||||
const extension = app.data.extensions[this.attrs.extensionId];
|
|
||||||
const statuses = this.statusItems(extension.id).toArray();
|
|
||||||
|
|
||||||
content.unshift(
|
|
||||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
|
||||||
{extension.icon ? icon(extension.icon.name) : ''}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
content.push(statuses);
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusItems(name) {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('enabled', <span class={'ExtensionListItem-Dot ' + (isExtensionEnabled(name) ? 'enabled' : 'disabled')} />);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,248 +0,0 @@
|
|||||||
import Button from '../../common/components/Button';
|
|
||||||
import Link from '../../common/components/Link';
|
|
||||||
import LinkButton from '../../common/components/LinkButton';
|
|
||||||
import Switch from '../../common/components/Switch';
|
|
||||||
import icon from '../../common/helpers/icon';
|
|
||||||
import punctuateSeries from '../../common/helpers/punctuateSeries';
|
|
||||||
import listItems from '../../common/helpers/listItems';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
import LoadingModal from './LoadingModal';
|
|
||||||
import ExtensionPermissionGrid from './ExtensionPermissionGrid';
|
|
||||||
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
|
||||||
import AdminPage from './AdminPage';
|
|
||||||
|
|
||||||
export default class ExtensionPage extends AdminPage {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.extension = app.data.extensions[this.attrs.id];
|
|
||||||
this.changingState = false;
|
|
||||||
|
|
||||||
this.infoFields = {
|
|
||||||
discuss: 'fas fa-comment-alt',
|
|
||||||
documentation: 'fas fa-book',
|
|
||||||
support: 'fas fa-life-ring',
|
|
||||||
website: 'fas fa-link',
|
|
||||||
donate: 'fas fa-donate',
|
|
||||||
source: 'fas fa-code',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.extension) {
|
|
||||||
return m.route.set(app.route('dashboard'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
if (!this.extension) return '';
|
|
||||||
|
|
||||||
return this.extension.id + '-Page';
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
if (!this.extension) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'ExtensionPage ' + this.className()}>
|
|
||||||
{this.header()}
|
|
||||||
{!this.isEnabled() ? (
|
|
||||||
<div className="container">
|
|
||||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="ExtensionPage-body">{this.sections().toArray()}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
header() {
|
|
||||||
const isEnabled = this.isEnabled();
|
|
||||||
|
|
||||||
return [
|
|
||||||
<div className="ExtensionPage-header">
|
|
||||||
<div className="container">
|
|
||||||
<div className="ExtensionTitle">
|
|
||||||
<span className="ExtensionIcon" style={this.extension.icon}>
|
|
||||||
{this.extension.icon ? icon(this.extension.icon.name) : ''}
|
|
||||||
</span>
|
|
||||||
<div className="ExtensionName">
|
|
||||||
<h2>{this.extension.extra['flarum-extension'].title}</h2>
|
|
||||||
</div>
|
|
||||||
<div className="ExtensionPage-headerTopItems">
|
|
||||||
<ul>{listItems(this.topItems().toArray())}</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="helpText">{this.extension.description}</div>
|
|
||||||
<div className="ExtensionPage-headerItems">
|
|
||||||
<Switch
|
|
||||||
state={this.changingState ? !isEnabled : isEnabled}
|
|
||||||
loading={this.changingState}
|
|
||||||
onchange={this.toggle.bind(this, this.extension.id)}
|
|
||||||
>
|
|
||||||
{isEnabled ? app.translator.trans('core.admin.extension.enabled') : app.translator.trans('core.admin.extension.disabled')}
|
|
||||||
</Switch>
|
|
||||||
<aside className="ExtensionInfo">
|
|
||||||
<ul>{listItems(this.infoItems().toArray())}</ul>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
sections() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('content', this.content());
|
|
||||||
|
|
||||||
items.add('permissions', [
|
|
||||||
<div className="ExtensionPage-permissions">
|
|
||||||
<div className="ExtensionPage-permissions-header">
|
|
||||||
<div className="container">
|
|
||||||
<h2 className="ExtensionTitle">{app.translator.trans('core.admin.extension.permissions_title')}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="container">
|
|
||||||
{app.extensionData.extensionHasPermissions(this.extension.id) ? (
|
|
||||||
ExtensionPermissionGrid.component({ extensionId: this.extension.id })
|
|
||||||
) : (
|
|
||||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_permissions')}</h3>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
const settings = app.extensionData.getSettings(this.extension.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ExtensionPage-settings">
|
|
||||||
<div className="container">
|
|
||||||
{settings ? (
|
|
||||||
<div className="Form">
|
|
||||||
{settings.map(this.buildSettingComponent.bind(this))}
|
|
||||||
<div className="Form-group">{this.submitButton()}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
topItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add('version', <span className="ExtensionVersion">{this.extension.version}</span>);
|
|
||||||
|
|
||||||
if (!this.isEnabled()) {
|
|
||||||
const uninstall = () => {
|
|
||||||
if (confirm(app.translator.trans('core.admin.extension.confirm_uninstall'))) {
|
|
||||||
app
|
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
.then(() => window.location.reload());
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'uninstall',
|
|
||||||
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={uninstall.bind(this)}>
|
|
||||||
{app.translator.trans('core.admin.extension.uninstall_button')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
infoItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
const links = this.extension.links;
|
|
||||||
|
|
||||||
if (links.authors.length) {
|
|
||||||
let authors = [];
|
|
||||||
|
|
||||||
links.authors.map((author) => {
|
|
||||||
authors.push(
|
|
||||||
<Link href={author.link} external={true} target="_blank">
|
|
||||||
{author.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('authors', [icon('fas fa-user'), <span>{punctuateSeries(authors)}</span>]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(this.infoFields).map((field) => {
|
|
||||||
if (links[field]) {
|
|
||||||
items.add(
|
|
||||||
field,
|
|
||||||
<LinkButton href={links[field]} icon={this.infoFields[field]} external={true} target="_blank">
|
|
||||||
{app.translator.trans(`core.admin.extension.info_links.${field}`)}
|
|
||||||
</LinkButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
const enabled = this.isEnabled();
|
|
||||||
|
|
||||||
this.changingState = true;
|
|
||||||
|
|
||||||
app
|
|
||||||
.request({
|
|
||||||
url: app.forum.attribute('apiUrl') + '/extensions/' + this.extension.id,
|
|
||||||
method: 'PATCH',
|
|
||||||
body: { enabled: !enabled },
|
|
||||||
errorHandler: this.onerror.bind(this),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (!enabled) localStorage.setItem('enabledExtension', this.extension.id);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.modal.show(LoadingModal);
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return isExtensionEnabled(this.extension.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
onerror(e) {
|
|
||||||
// We need to give the modal animation time to start; if we close the modal too early,
|
|
||||||
// it breaks the bootstrap modal library.
|
|
||||||
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
|
|
||||||
setTimeout(() => {
|
|
||||||
app.modal.close();
|
|
||||||
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
|
|
||||||
this.changingState = false;
|
|
||||||
|
|
||||||
if (e.status !== 409) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = e.response.errors[0];
|
|
||||||
|
|
||||||
app.alerts.show(
|
|
||||||
{ type: 'error' },
|
|
||||||
app.translator.trans(`core.lib.error.${error.code}_message`, {
|
|
||||||
extension: error.extension,
|
|
||||||
extensions: error.extensions.join(', '),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,53 +0,0 @@
|
|||||||
import PermissionGrid from './PermissionGrid';
|
|
||||||
import Button from '../../common/components/Button';
|
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
|
|
||||||
export default class ExtensionPermissionGrid extends PermissionGrid {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.extensionId = this.attrs.extensionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionItems() {
|
|
||||||
const permissionCategories = super.permissionItems();
|
|
||||||
|
|
||||||
permissionCategories.items = Object.entries(permissionCategories.items)
|
|
||||||
.filter(([category, info]) => info.content.children.length > 0)
|
|
||||||
.reduce((obj, [category, info]) => {
|
|
||||||
obj[category] = info;
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return permissionCategories;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewItems() {
|
|
||||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'view') || new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
startItems() {
|
|
||||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'start') || new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
replyItems() {
|
|
||||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'reply') || new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
moderateItems() {
|
|
||||||
return app.extensionData.getExtensionPermissions(this.extensionId, 'moderate') || new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeControlItems() {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'configureScopes',
|
|
||||||
<Button className="Button Button--text" onclick={() => m.route.set(app.route('permissions'))}>
|
|
||||||
{app.translator.trans('core.admin.extension.configure_scopes')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
134
js/src/admin/components/ExtensionsPage.js
Normal file
134
js/src/admin/components/ExtensionsPage.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import AddExtensionModal from './AddExtensionModal';
|
||||||
|
import LoadingModal from './LoadingModal';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
|
export default class ExtensionsPage extends Page {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="ExtensionsPage">
|
||||||
|
<div className="ExtensionsPage-header">
|
||||||
|
<div className="container">
|
||||||
|
{Button.component(
|
||||||
|
{
|
||||||
|
icon: 'fas fa-plus',
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
onclick: () => app.modal.show(AddExtensionModal),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.extensions.add_button')
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ExtensionsPage-list">
|
||||||
|
<div className="container">
|
||||||
|
<ul className="ExtensionList">
|
||||||
|
{Object.keys(app.data.extensions).map((id) => {
|
||||||
|
const extension = app.data.extensions[id];
|
||||||
|
const controls = this.controlItems(extension.id).toArray();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
||||||
|
<div className="ExtensionListItem-content">
|
||||||
|
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||||
|
{extension.icon ? icon(extension.icon.name) : ''}
|
||||||
|
</span>
|
||||||
|
{controls.length ? (
|
||||||
|
<Dropdown
|
||||||
|
className="ExtensionListItem-controls"
|
||||||
|
buttonClassName="Button Button--icon Button--flat"
|
||||||
|
menuClassName="Dropdown-menu--right"
|
||||||
|
icon="fas fa-ellipsis-h"
|
||||||
|
>
|
||||||
|
{controls}
|
||||||
|
</Dropdown>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
<div className="ExtensionListItem-main">
|
||||||
|
<label className="ExtensionListItem-title">
|
||||||
|
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)} />{' '}
|
||||||
|
{extension.extra['flarum-extension'].title}
|
||||||
|
</label>
|
||||||
|
<div className="ExtensionListItem-version">{extension.version}</div>
|
||||||
|
<div className="ExtensionListItem-description">{extension.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
controlItems(name) {
|
||||||
|
const items = new ItemList();
|
||||||
|
const enabled = this.isEnabled(name);
|
||||||
|
|
||||||
|
if (app.extensionSettings[name]) {
|
||||||
|
items.add(
|
||||||
|
'settings',
|
||||||
|
Button.component(
|
||||||
|
{
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
onclick: app.extensionSettings[name],
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.extensions.settings_button')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
items.add(
|
||||||
|
'uninstall',
|
||||||
|
Button.component(
|
||||||
|
{
|
||||||
|
icon: 'far fa-trash-alt',
|
||||||
|
onclick: () => {
|
||||||
|
app
|
||||||
|
.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
.then(() => window.location.reload());
|
||||||
|
|
||||||
|
app.modal.show(LoadingModal);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.extensions.uninstall_button')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(name) {
|
||||||
|
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
||||||
|
|
||||||
|
return enabled.indexOf(name) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(id) {
|
||||||
|
const enabled = this.isEnabled(id);
|
||||||
|
|
||||||
|
app
|
||||||
|
.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { enabled: !enabled },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.modal.show(LoadingModal);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,51 +0,0 @@
|
|||||||
import DashboardWidget from './DashboardWidget';
|
|
||||||
import isExtensionEnabled from '../utils/isExtensionEnabled';
|
|
||||||
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
|
|
||||||
import Link from '../../common/components/Link';
|
|
||||||
import icon from '../../common/helpers/icon';
|
|
||||||
|
|
||||||
export default class ExtensionsWidget extends DashboardWidget {
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.categorizedExtensions = getCategorizedExtensions();
|
|
||||||
}
|
|
||||||
|
|
||||||
className() {
|
|
||||||
return 'ExtensionsWidget';
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
const categories = app.extensionCategories;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ExtensionsWidget-list">
|
|
||||||
{Object.keys(categories).map((category) => (this.categorizedExtensions[category] ? this.extensionCategory(category) : ''))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionCategory(category) {
|
|
||||||
return (
|
|
||||||
<div className="ExtensionList-Category">
|
|
||||||
<h4 className="ExtensionList-Label">{app.translator.trans(`core.admin.nav.categories.${category}`)}</h4>
|
|
||||||
<ul className="ExtensionList">{this.categorizedExtensions[category].map((extension) => this.extensionWidget(extension))}</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionWidget(extension) {
|
|
||||||
return (
|
|
||||||
<li className={'ExtensionListItem ' + (!isExtensionEnabled(extension.id) ? 'disabled' : '')}>
|
|
||||||
<Link href={app.route('extension', { id: extension.id })}>
|
|
||||||
<div className="ExtensionListItem-content">
|
|
||||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
|
||||||
{extension.icon ? icon(extension.icon.name) : ''}
|
|
||||||
</span>
|
|
||||||
<span className="ExtensionListItem-title">{extension.extra['flarum-extension'].title}</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,4 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import LinkButton from '../../common/components/LinkButton';
|
|
||||||
import SessionDropdown from './SessionDropdown';
|
import SessionDropdown from './SessionDropdown';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
@@ -20,13 +19,6 @@ export default class HeaderSecondary extends Component {
|
|||||||
items() {
|
items() {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
items.add(
|
|
||||||
'help',
|
|
||||||
<LinkButton href="https://docs.flarum.org/troubleshoot.html" icon="fas fa-question-circle" external={true} target="_blank">
|
|
||||||
{app.translator.trans('core.admin.header.get_help')}
|
|
||||||
</LinkButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add('session', SessionDropdown.component());
|
items.add('session', SessionDropdown.component());
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@@ -1,31 +1,32 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
import FieldSet from '../../common/components/FieldSet';
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Alert from '../../common/components/Alert';
|
import Alert from '../../common/components/Alert';
|
||||||
|
import Select from '../../common/components/Select';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import AdminPage from './AdminPage';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
import withAttr from '../../common/utils/withAttr';
|
||||||
|
|
||||||
export default class MailPage extends AdminPage {
|
export default class MailPage extends Page {
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.saving = false;
|
||||||
this.sendingTest = false;
|
this.sendingTest = false;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
headerInfo() {
|
|
||||||
return {
|
|
||||||
className: 'MailPage',
|
|
||||||
icon: 'fas fa-envelope',
|
|
||||||
title: app.translator.trans('core.admin.email.title'),
|
|
||||||
description: app.translator.trans('core.admin.email.description'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
this.driverFields = {};
|
||||||
|
this.fields = ['mail_driver', 'mail_from'];
|
||||||
|
this.values = {};
|
||||||
this.status = { sending: false, errors: {} };
|
this.status = { sending: false, errors: {} };
|
||||||
|
|
||||||
|
const settings = app.data.settings;
|
||||||
|
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
|
||||||
|
|
||||||
app
|
app
|
||||||
.request({
|
.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -36,34 +37,73 @@ export default class MailPage extends AdminPage {
|
|||||||
this.status.sending = response['data']['attributes']['sending'];
|
this.status.sending = response['data']['attributes']['sending'];
|
||||||
this.status.errors = response['data']['attributes']['errors'];
|
this.status.errors = response['data']['attributes']['errors'];
|
||||||
|
|
||||||
|
for (const driver in this.driverFields) {
|
||||||
|
for (const field in this.driverFields[driver]) {
|
||||||
|
this.fields.push(field);
|
||||||
|
this.values[field] = m.stream(settings[field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
view() {
|
||||||
if (this.loading) {
|
if (this.loading || this.saving) {
|
||||||
return <LoadingIndicator />;
|
return (
|
||||||
|
<div className="MailPage">
|
||||||
|
<div className="container">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.driverFields[this.setting('mail_driver')()];
|
const fields = this.driverFields[this.values.mail_driver()];
|
||||||
const fieldKeys = Object.keys(fields);
|
const fieldKeys = Object.keys(fields);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Form">
|
<div className="MailPage">
|
||||||
{this.buildSettingComponent({
|
<div className="container">
|
||||||
type: 'text',
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
setting: 'mail_from',
|
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.email.text')}</div>
|
||||||
|
|
||||||
|
{FieldSet.component(
|
||||||
|
{
|
||||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||||
className: 'MailPage-MailSettings',
|
className: 'MailPage-MailSettings',
|
||||||
})}
|
},
|
||||||
{this.buildSettingComponent({
|
[
|
||||||
type: 'select',
|
<div className="MailPage-MailSettings-input">
|
||||||
setting: 'mail_driver',
|
<label>
|
||||||
options: Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
|
{app.translator.trans('core.admin.email.from_label')}
|
||||||
|
<input className="FormControl" bidi={this.values.mail_from} />
|
||||||
|
</label>
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
|
||||||
|
{FieldSet.component(
|
||||||
|
{
|
||||||
label: app.translator.trans('core.admin.email.driver_heading'),
|
label: app.translator.trans('core.admin.email.driver_heading'),
|
||||||
className: 'MailPage-MailSettings',
|
className: 'MailPage-MailSettings',
|
||||||
})}
|
},
|
||||||
|
[
|
||||||
|
<div className="MailPage-MailSettings-input">
|
||||||
|
<label>
|
||||||
|
{app.translator.trans('core.admin.email.driver_label')}
|
||||||
|
<Select
|
||||||
|
value={this.values.mail_driver()}
|
||||||
|
options={Object.keys(this.driverFields).reduce((memo, val) => ({ ...memo, [val]: val }), {})}
|
||||||
|
onchange={this.values.mail_driver}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
|
||||||
{this.status.sending ||
|
{this.status.sending ||
|
||||||
Alert.component(
|
Alert.component(
|
||||||
{
|
{
|
||||||
@@ -72,42 +112,75 @@ export default class MailPage extends AdminPage {
|
|||||||
app.translator.trans('core.admin.email.not_sending_message')
|
app.translator.trans('core.admin.email.not_sending_message')
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fieldKeys.length > 0 && (
|
{fieldKeys.length > 0 &&
|
||||||
<FieldSet label={app.translator.trans(`core.admin.email.${this.setting('mail_driver')()}_heading`)} className="MailPage-MailSettings">
|
FieldSet.component(
|
||||||
|
{
|
||||||
|
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
},
|
||||||
|
[
|
||||||
<div className="MailPage-MailSettings-input">
|
<div className="MailPage-MailSettings-input">
|
||||||
{fieldKeys.map((field) => {
|
{fieldKeys.map((field) => [
|
||||||
const fieldInfo = fields[field];
|
<label>
|
||||||
|
{app.translator.trans(`core.admin.email.${field}_label`)}
|
||||||
return [
|
{this.renderField(field)}
|
||||||
this.buildSettingComponent({
|
</label>,
|
||||||
type: typeof this.setting(field)() === 'string' ? 'text' : 'select',
|
|
||||||
label: app.translator.trans(`core.admin.email.${field}_label`),
|
|
||||||
setting: field,
|
|
||||||
options: fieldInfo,
|
|
||||||
}),
|
|
||||||
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
|
this.status.errors[field] && <p className="ValidationError">{this.status.errors[field]}</p>,
|
||||||
];
|
])}
|
||||||
})}
|
</div>,
|
||||||
</div>
|
]
|
||||||
</FieldSet>
|
|
||||||
)}
|
)}
|
||||||
{this.submitButton()}
|
|
||||||
|
|
||||||
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
|
<FieldSet>
|
||||||
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>
|
|
||||||
{Button.component(
|
{Button.component(
|
||||||
{
|
{
|
||||||
|
type: 'submit',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
disabled: this.sendingTest || this.isChanged(),
|
disabled: !this.changed(),
|
||||||
|
},
|
||||||
|
app.translator.trans('core.admin.email.submit_button')
|
||||||
|
)}
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
{FieldSet.component(
|
||||||
|
{
|
||||||
|
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
|
||||||
|
className: 'MailPage-MailSettings',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>,
|
||||||
|
Button.component(
|
||||||
|
{
|
||||||
|
className: 'Button Button--primary',
|
||||||
|
disabled: this.sendingTest || this.changed(),
|
||||||
onclick: () => this.sendTestEmail(),
|
onclick: () => this.sendTestEmail(),
|
||||||
},
|
},
|
||||||
app.translator.trans('core.admin.email.send_test_mail_button')
|
app.translator.trans('core.admin.email.send_test_mail_button')
|
||||||
|
),
|
||||||
|
]
|
||||||
)}
|
)}
|
||||||
</FieldSet>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderField(name) {
|
||||||
|
const driver = this.values.mail_driver();
|
||||||
|
const field = this.driverFields[driver][name];
|
||||||
|
const prop = this.values[name];
|
||||||
|
|
||||||
|
if (typeof field === 'string') {
|
||||||
|
return <input className="FormControl" bidi={prop} />;
|
||||||
|
} else {
|
||||||
|
return <Select value={prop()} options={field} onchange={prop} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed() {
|
||||||
|
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
|
||||||
|
}
|
||||||
|
|
||||||
sendTestEmail() {
|
sendTestEmail() {
|
||||||
if (this.saving || this.sendingTest) return;
|
if (this.saving || this.sendingTest) return;
|
||||||
|
|
||||||
@@ -121,7 +194,9 @@ export default class MailPage extends AdminPage {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.sendingTest = false;
|
this.sendingTest = false;
|
||||||
this.testEmailSuccessAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.email.send_test_mail_success'));
|
this.testEmailSuccessAlert = app.alerts.show(app.translator.trans('core.admin.email.send_test_mail_success'), {
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.sendingTest = false;
|
this.sendingTest = false;
|
||||||
@@ -130,7 +205,28 @@ export default class MailPage extends AdminPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings(e) {
|
onsubmit(e) {
|
||||||
super.saveSettings(e).then(this.refresh());
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.saving || this.sendingTest) return;
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
app.alerts.dismiss(this.successAlert);
|
||||||
|
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
this.fields.forEach((key) => (settings[key] = this.values[key]()));
|
||||||
|
|
||||||
|
saveSettings(settings)
|
||||||
|
.then(() => {
|
||||||
|
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
this.saving = false;
|
||||||
|
this.refresh();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,12 @@ import ItemList from '../../common/utils/ItemList';
|
|||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
|
|
||||||
export default class PermissionGrid extends Component {
|
export default class PermissionGrid extends Component {
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.permissions = this.permissionItems().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
const scopes = this.scopeItems().toArray();
|
const scopes = this.scopeItems().toArray();
|
||||||
|
|
||||||
@@ -29,9 +35,7 @@ export default class PermissionGrid extends Component {
|
|||||||
<th>{this.scopeControlItems().toArray()}</th>
|
<th>{this.scopeControlItems().toArray()}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{this.permissionItems()
|
{this.permissions.map((section) => (
|
||||||
.toArray()
|
|
||||||
.map((section) => (
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="PermissionGrid-section">
|
<tr className="PermissionGrid-section">
|
||||||
<th>{section.label}</th>
|
<th>{section.label}</th>
|
||||||
@@ -154,8 +158,6 @@ export default class PermissionGrid extends Component {
|
|||||||
permission: 'user.viewLastSeenAt',
|
permission: 'user.viewLastSeenAt',
|
||||||
});
|
});
|
||||||
|
|
||||||
items.merge(app.extensionData.getAllExtensionPermissions('view'));
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +198,6 @@ export default class PermissionGrid extends Component {
|
|||||||
90
|
90
|
||||||
);
|
);
|
||||||
|
|
||||||
items.merge(app.extensionData.getAllExtensionPermissions('start'));
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +238,6 @@ export default class PermissionGrid extends Component {
|
|||||||
90
|
90
|
||||||
);
|
);
|
||||||
|
|
||||||
items.merge(app.extensionData.getAllExtensionPermissions('reply'));
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,38 +324,16 @@ export default class PermissionGrid extends Component {
|
|||||||
60
|
60
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(
|
|
||||||
'userEditCredentials',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-user-cog',
|
|
||||||
label: app.translator.trans('core.admin.permissions.edit_users_credentials_label'),
|
|
||||||
permission: 'user.editCredentials',
|
|
||||||
},
|
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
'userEditGroups',
|
|
||||||
{
|
|
||||||
icon: 'fas fa-users-cog',
|
|
||||||
label: app.translator.trans('core.admin.permissions.edit_users_groups_label'),
|
|
||||||
permission: 'user.editGroups',
|
|
||||||
},
|
|
||||||
60
|
|
||||||
);
|
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'userEdit',
|
'userEdit',
|
||||||
{
|
{
|
||||||
icon: 'fas fa-address-card',
|
icon: 'fas fa-user-cog',
|
||||||
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
label: app.translator.trans('core.admin.permissions.edit_users_label'),
|
||||||
permission: 'user.edit',
|
permission: 'user.edit',
|
||||||
},
|
},
|
||||||
60
|
60
|
||||||
);
|
);
|
||||||
|
|
||||||
items.merge(app.extensionData.getAllExtensionPermissions('moderate'));
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,23 +1,16 @@
|
|||||||
|
import Page from '../../common/components/Page';
|
||||||
import GroupBadge from '../../common/components/GroupBadge';
|
import GroupBadge from '../../common/components/GroupBadge';
|
||||||
import EditGroupModal from './EditGroupModal';
|
import EditGroupModal from './EditGroupModal';
|
||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import icon from '../../common/helpers/icon';
|
import icon from '../../common/helpers/icon';
|
||||||
import PermissionGrid from './PermissionGrid';
|
import PermissionGrid from './PermissionGrid';
|
||||||
import AdminPage from './AdminPage';
|
|
||||||
|
|
||||||
export default class PermissionsPage extends AdminPage {
|
export default class PermissionsPage extends Page {
|
||||||
headerInfo() {
|
view() {
|
||||||
return {
|
return (
|
||||||
className: 'PermissionsPage',
|
<div className="PermissionsPage">
|
||||||
icon: 'fas fa-key',
|
|
||||||
title: app.translator.trans('core.admin.permissions.title'),
|
|
||||||
description: app.translator.trans('core.admin.permissions.description'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
<div className="PermissionsPage-groups">
|
<div className="PermissionsPage-groups">
|
||||||
|
<div className="container">
|
||||||
{app.store
|
{app.store
|
||||||
.all('groups')
|
.all('groups')
|
||||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||||
@@ -35,9 +28,13 @@ export default class PermissionsPage extends AdminPage {
|
|||||||
{icon('fas fa-plus', { className: 'Group-icon' })}
|
{icon('fas fa-plus', { className: 'Group-icon' })}
|
||||||
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>,
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="PermissionsPage-permissions">{PermissionGrid.component()}</div>,
|
<div className="PermissionsPage-permissions">
|
||||||
];
|
<div className="container">{PermissionGrid.component()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import Modal from '../../common/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
|
||||||
export default class SettingsModal extends Modal {
|
export default class SettingsModal extends Modal {
|
||||||
@@ -36,7 +35,7 @@ export default class SettingsModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setting(key, fallback = '') {
|
setting(key, fallback = '') {
|
||||||
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
|
this.settings[key] = this.settings[key] || m.stream(app.data.settings[key] || fallback);
|
||||||
|
|
||||||
return this.settings[key];
|
return this.settings[key];
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
import app from './app';
|
import AdminApplication from './AdminApplication';
|
||||||
|
|
||||||
|
const app = new AdminApplication();
|
||||||
|
|
||||||
|
// Backwards compatibility
|
||||||
|
window.app = app;
|
||||||
|
|
||||||
export { app };
|
export { app };
|
||||||
|
|
||||||
// Export public API
|
// Export public API
|
||||||
|
|
||||||
// Export compat API
|
// Export compat API
|
||||||
import compatObj from './compat';
|
import compat from './compat';
|
||||||
import proxifyCompat from '../common/utils/proxifyCompat';
|
|
||||||
|
|
||||||
compatObj.app = app;
|
compat.app = app;
|
||||||
|
|
||||||
export const compat = proxifyCompat(compatObj, 'admin');
|
export { compat };
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
import DefaultResolver from '../../common/resolvers/DefaultResolver';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom route resolver for ExtensionPage that generates handles routes
|
|
||||||
* to default extension pages or a page provided by an extension.
|
|
||||||
*/
|
|
||||||
export default class ExtensionPageResolver extends DefaultResolver {
|
|
||||||
static extension: string | null = null;
|
|
||||||
|
|
||||||
onmatch(args, requestedPath, route) {
|
|
||||||
const extensionPage = app.extensionData.getPage(args.id);
|
|
||||||
|
|
||||||
if (extensionPage) {
|
|
||||||
return extensionPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onmatch(args, requestedPath, route);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,9 +2,8 @@ import DashboardPage from './components/DashboardPage';
|
|||||||
import BasicsPage from './components/BasicsPage';
|
import BasicsPage from './components/BasicsPage';
|
||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
import AppearancePage from './components/AppearancePage';
|
import AppearancePage from './components/AppearancePage';
|
||||||
|
import ExtensionsPage from './components/ExtensionsPage';
|
||||||
import MailPage from './components/MailPage';
|
import MailPage from './components/MailPage';
|
||||||
import ExtensionPage from './components/ExtensionPage';
|
|
||||||
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `routes` initializer defines the forum app's routes.
|
* The `routes` initializer defines the forum app's routes.
|
||||||
@@ -17,7 +16,7 @@ export default function (app) {
|
|||||||
basics: { path: '/basics', component: BasicsPage },
|
basics: { path: '/basics', component: BasicsPage },
|
||||||
permissions: { path: '/permissions', component: PermissionsPage },
|
permissions: { path: '/permissions', component: PermissionsPage },
|
||||||
appearance: { path: '/appearance', component: AppearancePage },
|
appearance: { path: '/appearance', component: AppearancePage },
|
||||||
|
extensions: { path: '/extensions', component: ExtensionsPage },
|
||||||
mail: { path: '/mail', component: MailPage },
|
mail: { path: '/mail', component: MailPage },
|
||||||
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,177 +0,0 @@
|
|||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
|
|
||||||
export default class ExtensionData {
|
|
||||||
constructor() {
|
|
||||||
this.data = {};
|
|
||||||
this.currentExtension = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function simply takes the extension id
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* app.extensionData.load('flarum-tags')
|
|
||||||
*
|
|
||||||
* flarum/flags -> flarum-flags | acme/extension -> acme-extension
|
|
||||||
*
|
|
||||||
* @param extension
|
|
||||||
*/
|
|
||||||
for(extension) {
|
|
||||||
this.currentExtension = extension;
|
|
||||||
this.data[extension] = this.data[extension] || {};
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function registers your settings with Flarum
|
|
||||||
*
|
|
||||||
* It takes either a settings object or a callback.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* .registerSetting({
|
|
||||||
* setting: 'flarum-flags.guidelines_url',
|
|
||||||
* type: 'text', // This will be inputted into the input tag for the setting (text/number/etc)
|
|
||||||
* label: app.translator.trans('flarum-flags.admin.settings.guidelines_url_label')
|
|
||||||
* }, 15) // priority is optional (ItemList)
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param content
|
|
||||||
* @param priority
|
|
||||||
* @returns {ExtensionData}
|
|
||||||
*/
|
|
||||||
registerSetting(content, priority = 0) {
|
|
||||||
this.data[this.currentExtension].settings = this.data[this.currentExtension].settings || new ItemList();
|
|
||||||
|
|
||||||
// Callbacks can be passed in instead of settings to display custom content.
|
|
||||||
// By default, they will be added with the `null` key, since they don't have a `.setting` attr.
|
|
||||||
// To support multiple such items for one extension, we assign a random ID.
|
|
||||||
// 36 is arbitrary length, but makes collisions very unlikely.
|
|
||||||
if (typeof content === 'function') {
|
|
||||||
content.setting = Math.random().toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data[this.currentExtension].settings.add(content.setting, content, priority);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function registers your permission with Flarum
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* .registerPermission('permissions', {
|
|
||||||
* icon: 'fas fa-flag',
|
|
||||||
* label: app.translator.trans('flarum-flags.admin.permissions.view_flags_label'),
|
|
||||||
* permission: 'discussion.viewFlags'
|
|
||||||
* }, 'moderate', 65)
|
|
||||||
*
|
|
||||||
* @param content
|
|
||||||
* @param permissionType
|
|
||||||
* @param priority
|
|
||||||
* @returns {ExtensionData}
|
|
||||||
*/
|
|
||||||
registerPermission(content, permissionType = null, priority = 0) {
|
|
||||||
this.data[this.currentExtension].permissions = this.data[this.currentExtension].permissions || {};
|
|
||||||
|
|
||||||
if (!this.data[this.currentExtension].permissions[permissionType]) {
|
|
||||||
this.data[this.currentExtension].permissions[permissionType] = new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data[this.currentExtension].permissions[permissionType].add(content.permission, content, priority);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the default extension page with a custom component.
|
|
||||||
* This component would typically extend ExtensionPage
|
|
||||||
*
|
|
||||||
* @param component
|
|
||||||
* @returns {ExtensionData}
|
|
||||||
*/
|
|
||||||
registerPage(component) {
|
|
||||||
this.data[this.currentExtension].page = component;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an extension's registered settings
|
|
||||||
*
|
|
||||||
* @param extensionId
|
|
||||||
* @returns {boolean|*}
|
|
||||||
*/
|
|
||||||
getSettings(extensionId) {
|
|
||||||
if (this.data[extensionId] && this.data[extensionId].settings) {
|
|
||||||
return this.data[extensionId].settings.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Get an ItemList of all extensions' registered permissions
|
|
||||||
*
|
|
||||||
* @param extension
|
|
||||||
* @param type
|
|
||||||
* @returns {ItemList}
|
|
||||||
*/
|
|
||||||
getAllExtensionPermissions(type) {
|
|
||||||
const items = new ItemList();
|
|
||||||
|
|
||||||
Object.keys(this.data).map((extension) => {
|
|
||||||
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
|
|
||||||
items.merge(this.data[extension].permissions[type]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a singular extension's registered permissions
|
|
||||||
*
|
|
||||||
* @param extension
|
|
||||||
* @param type
|
|
||||||
* @returns {boolean|*}
|
|
||||||
*/
|
|
||||||
getExtensionPermissions(extension, type) {
|
|
||||||
if (this.extensionHasPermissions(extension) && this.data[extension].permissions[type]) {
|
|
||||||
return this.data[extension].permissions[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ItemList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given extension has registered permissions.
|
|
||||||
*
|
|
||||||
* @param extension
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
extensionHasPermissions(extension) {
|
|
||||||
if (this.data[extension] && this.data[extension].permissions) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an extension's custom page component if it exists.
|
|
||||||
*
|
|
||||||
* @param extension
|
|
||||||
* @returns {boolean|*}
|
|
||||||
*/
|
|
||||||
getPage(extension) {
|
|
||||||
if (this.data[extension]) {
|
|
||||||
return this.data[extension].page;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
export default function getCategorizedExtensions() {
|
|
||||||
let extensions = {};
|
|
||||||
|
|
||||||
Object.keys(app.data.extensions).map((id) => {
|
|
||||||
const extension = app.data.extensions[id];
|
|
||||||
let category = extension.extra['flarum-extension'].category;
|
|
||||||
|
|
||||||
// Wrap languages packs into new system
|
|
||||||
if (extension.extra['flarum-locale']) {
|
|
||||||
category = 'language';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category in app.extensionCategories) {
|
|
||||||
extensions[category] = extensions[category] || [];
|
|
||||||
|
|
||||||
extensions[category].push(extension);
|
|
||||||
} else {
|
|
||||||
extensions.feature = extensions.feature || [];
|
|
||||||
|
|
||||||
extensions.feature.push(extension);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return extensions;
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
export default function isExtensionEnabled(name) {
|
|
||||||
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
|
||||||
|
|
||||||
return enabled.includes(name);
|
|
||||||
}
|
|
@@ -159,8 +159,6 @@ export default class Application {
|
|||||||
title = '';
|
title = '';
|
||||||
titleCount = 0;
|
titleCount = 0;
|
||||||
|
|
||||||
initialRoute;
|
|
||||||
|
|
||||||
load(payload) {
|
load(payload) {
|
||||||
this.data = payload;
|
this.data = payload;
|
||||||
this.translator.locale = payload.locale;
|
this.translator.locale = payload.locale;
|
||||||
@@ -176,8 +174,6 @@ export default class Application {
|
|||||||
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
|
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
|
||||||
|
|
||||||
this.mount();
|
this.mount();
|
||||||
|
|
||||||
this.initialRoute = window.location.href;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootExtensions(extensions) {
|
bootExtensions(extensions) {
|
||||||
@@ -202,19 +198,13 @@ export default class Application {
|
|||||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
||||||
|
|
||||||
// Add a class to the body which indicates that the page has been scrolled
|
// Add a class to the body which indicates that the page has been scrolled
|
||||||
// down. When this happens, we'll add classes to the header and app body
|
// down.
|
||||||
// which will set the navbar's position to fixed. We don't want to always
|
new ScrollListener((top) => {
|
||||||
// have it fixed, as that could overlap with custom headers.
|
|
||||||
const scrollListener = new ScrollListener((top) => {
|
|
||||||
const $app = $('#app');
|
const $app = $('#app');
|
||||||
const offset = $app.offset().top;
|
const offset = $app.offset().top;
|
||||||
|
|
||||||
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
|
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
|
||||||
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
|
}).start();
|
||||||
});
|
|
||||||
|
|
||||||
scrollListener.start();
|
|
||||||
scrollListener.update();
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||||
@@ -230,8 +220,7 @@ export default class Application {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
preloadedApiDocument() {
|
preloadedApiDocument() {
|
||||||
// If the URL has changed, the preloaded Api document is invalid.
|
if (this.data.apiDocument) {
|
||||||
if (this.data.apiDocument && window.location.href === this.initialRoute) {
|
|
||||||
const results = this.store.pushPayload(this.data.apiDocument);
|
const results = this.store.pushPayload(this.data.apiDocument);
|
||||||
|
|
||||||
this.data.apiDocument = null;
|
this.data.apiDocument = null;
|
||||||
@@ -275,7 +264,7 @@ export default class Application {
|
|||||||
|
|
||||||
updateTitle() {
|
updateTitle() {
|
||||||
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
const count = this.titleCount ? `(${this.titleCount}) ` : '';
|
||||||
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
|
const pageTitleWithSeparator = this.title && m.route.get() !== '/' ? this.title + ' - ' : '';
|
||||||
const title = this.forum.attribute('title');
|
const title = this.forum.attribute('title');
|
||||||
document.title = count + pageTitleWithSeparator + title;
|
document.title = count + pageTitleWithSeparator + title;
|
||||||
}
|
}
|
||||||
@@ -283,7 +272,7 @@ export default class Application {
|
|||||||
/**
|
/**
|
||||||
* Make an AJAX request, handling any low-level errors that may occur.
|
* Make an AJAX request, handling any low-level errors that may occur.
|
||||||
*
|
*
|
||||||
* @see https://mithril.js.org/request.html
|
* @see https://lhorie.github.io/mithril/mithril.request.html
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
* @public
|
* @public
|
||||||
@@ -415,7 +404,7 @@ export default class Application {
|
|||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
|
this.requestErrorAlert = this.alerts.show(error.alert.content, error.alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import * as Mithril from 'mithril';
|
import * as Mithril from 'mithril';
|
||||||
|
|
||||||
export interface ComponentAttrs extends Mithril.Attributes {}
|
export type ComponentAttrs = {
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Component` class defines a user interface 'building block'. A component
|
* The `Component` class defines a user interface 'building block'. A component
|
||||||
@@ -29,7 +33,7 @@ export interface ComponentAttrs extends Mithril.Attributes {}
|
|||||||
*
|
*
|
||||||
* @see https://mithril.js.org/components.html
|
* @see https://mithril.js.org/components.html
|
||||||
*/
|
*/
|
||||||
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
|
export default abstract class Component<T extends ComponentAttrs = any> implements Mithril.ClassComponent<T> {
|
||||||
/**
|
/**
|
||||||
* The root DOM element for the component.
|
* The root DOM element for the component.
|
||||||
*/
|
*/
|
||||||
@@ -77,12 +81,12 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
|||||||
* containing all of the `li` elements inside the DOM element of this
|
* containing all of the `li` elements inside the DOM element of this
|
||||||
* component.
|
* component.
|
||||||
*
|
*
|
||||||
* @param [selector] a jQuery-compatible selector string
|
* @param {String} [selector] a jQuery-compatible selector string
|
||||||
* @returns the jQuery object for the DOM node
|
* @returns {jQuery} the jQuery object for the DOM node
|
||||||
* @final
|
* @final
|
||||||
*/
|
*/
|
||||||
protected $(selector: string): JQuery {
|
protected $(selector) {
|
||||||
const $element = $(this.element) as JQuery<HTMLElement>;
|
const $element = $(this.element);
|
||||||
|
|
||||||
return selector ? $element.find(selector) : $element;
|
return selector ? $element.find(selector) : $element;
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
|||||||
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
|
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
|
||||||
*/
|
*/
|
||||||
static component(attrs = {}, children = null): Mithril.Vnode {
|
static component(attrs = {}, children = null): Mithril.Vnode {
|
||||||
const componentAttrs = Object.assign({}, attrs) as Record<string, unknown>;
|
const componentAttrs = Object.assign({}, attrs);
|
||||||
|
|
||||||
return m(this as any, componentAttrs, children);
|
return m(this as any, componentAttrs, children);
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ import * as Mithril from 'mithril';
|
|||||||
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
|
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
|
||||||
* this or `Component, you probably need `Component`.
|
* this or `Component, you probably need `Component`.
|
||||||
*/
|
*/
|
||||||
export default abstract class Fragment {
|
export default abstract class Fragment implements Mithril.ClassComponent {
|
||||||
/**
|
/**
|
||||||
* The root DOM element for the fragment.
|
* The root DOM element for the fragment.
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +52,7 @@ export default abstract class Fragment {
|
|||||||
*
|
*
|
||||||
* @final
|
* @final
|
||||||
*/
|
*/
|
||||||
public render(): Mithril.Vnode<Mithril.Attributes, this> {
|
public render(): Mithril.Vnode {
|
||||||
const vdom = this.view();
|
const vdom = this.view();
|
||||||
|
|
||||||
vdom.attrs = vdom.attrs || {};
|
vdom.attrs = vdom.attrs || {};
|
||||||
@@ -61,14 +61,15 @@ export default abstract class Fragment {
|
|||||||
|
|
||||||
vdom.attrs.oncreate = (vnode) => {
|
vdom.attrs.oncreate = (vnode) => {
|
||||||
this.element = vnode.dom;
|
this.element = vnode.dom;
|
||||||
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
|
if (this.oncreate) this.oncreate.apply(this, vnode);
|
||||||
|
if (originalOnCreate) originalOnCreate.apply(this, vnode);
|
||||||
};
|
};
|
||||||
|
|
||||||
return vdom;
|
return vdom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a view out of virtual elements.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
|
abstract view();
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import * as extend from './extend';
|
import * as extend from './extend';
|
||||||
import Session from './Session';
|
import Session from './Session';
|
||||||
import Store from './Store';
|
import Store from './Store';
|
||||||
import BasicEditorDriver from './utils/BasicEditorDriver';
|
|
||||||
import evented from './utils/evented';
|
import evented from './utils/evented';
|
||||||
import liveHumanTimes from './utils/liveHumanTimes';
|
import liveHumanTimes from './utils/liveHumanTimes';
|
||||||
import ItemList from './utils/ItemList';
|
import ItemList from './utils/ItemList';
|
||||||
@@ -13,7 +12,6 @@ import anchorScroll from './utils/anchorScroll';
|
|||||||
import RequestError from './utils/RequestError';
|
import RequestError from './utils/RequestError';
|
||||||
import abbreviateNumber from './utils/abbreviateNumber';
|
import abbreviateNumber from './utils/abbreviateNumber';
|
||||||
import * as string from './utils/string';
|
import * as string from './utils/string';
|
||||||
import Stream from './utils/Stream';
|
|
||||||
import SubtreeRetainer from './utils/SubtreeRetainer';
|
import SubtreeRetainer from './utils/SubtreeRetainer';
|
||||||
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
|
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
|
||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
@@ -21,7 +19,6 @@ import ScrollListener from './utils/ScrollListener';
|
|||||||
import stringToColor from './utils/stringToColor';
|
import stringToColor from './utils/stringToColor';
|
||||||
import subclassOf from './utils/subclassOf';
|
import subclassOf from './utils/subclassOf';
|
||||||
import patchMithril from './utils/patchMithril';
|
import patchMithril from './utils/patchMithril';
|
||||||
import proxifyCompat from './utils/proxifyCompat';
|
|
||||||
import classList from './utils/classList';
|
import classList from './utils/classList';
|
||||||
import extractText from './utils/extractText';
|
import extractText from './utils/extractText';
|
||||||
import formatNumber from './utils/formatNumber';
|
import formatNumber from './utils/formatNumber';
|
||||||
@@ -49,7 +46,6 @@ import FieldSet from './components/FieldSet';
|
|||||||
import Select from './components/Select';
|
import Select from './components/Select';
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './components/Navigation';
|
||||||
import Alert from './components/Alert';
|
import Alert from './components/Alert';
|
||||||
import Link from './components/Link';
|
|
||||||
import LinkButton from './components/LinkButton';
|
import LinkButton from './components/LinkButton';
|
||||||
import Checkbox from './components/Checkbox';
|
import Checkbox from './components/Checkbox';
|
||||||
import SelectDropdown from './components/SelectDropdown';
|
import SelectDropdown from './components/SelectDropdown';
|
||||||
@@ -57,8 +53,6 @@ import ModalManager from './components/ModalManager';
|
|||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import Modal from './components/Modal';
|
import Modal from './components/Modal';
|
||||||
import GroupBadge from './components/GroupBadge';
|
import GroupBadge from './components/GroupBadge';
|
||||||
import TextEditor from './components/TextEditor';
|
|
||||||
import TextEditorButton from './components/TextEditorButton';
|
|
||||||
import Model from './Model';
|
import Model from './Model';
|
||||||
import Application from './Application';
|
import Application from './Application';
|
||||||
import fullTime from './helpers/fullTime';
|
import fullTime from './helpers/fullTime';
|
||||||
@@ -71,13 +65,11 @@ import username from './helpers/username';
|
|||||||
import userOnline from './helpers/userOnline';
|
import userOnline from './helpers/userOnline';
|
||||||
import listItems from './helpers/listItems';
|
import listItems from './helpers/listItems';
|
||||||
import Fragment from './Fragment';
|
import Fragment from './Fragment';
|
||||||
import DefaultResolver from './resolvers/DefaultResolver';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extend: extend,
|
extend: extend,
|
||||||
Session: Session,
|
Session: Session,
|
||||||
Store: Store,
|
Store: Store,
|
||||||
'utils/BasicEditorDriver': BasicEditorDriver,
|
|
||||||
'utils/evented': evented,
|
'utils/evented': evented,
|
||||||
'utils/liveHumanTimes': liveHumanTimes,
|
'utils/liveHumanTimes': liveHumanTimes,
|
||||||
'utils/ItemList': ItemList,
|
'utils/ItemList': ItemList,
|
||||||
@@ -93,11 +85,9 @@ export default {
|
|||||||
'utils/extract': extract,
|
'utils/extract': extract,
|
||||||
'utils/ScrollListener': ScrollListener,
|
'utils/ScrollListener': ScrollListener,
|
||||||
'utils/stringToColor': stringToColor,
|
'utils/stringToColor': stringToColor,
|
||||||
'utils/Stream': Stream,
|
|
||||||
'utils/subclassOf': subclassOf,
|
'utils/subclassOf': subclassOf,
|
||||||
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
||||||
'utils/patchMithril': patchMithril,
|
'utils/patchMithril': patchMithril,
|
||||||
'utils/proxifyCompat': proxifyCompat,
|
|
||||||
'utils/classList': classList,
|
'utils/classList': classList,
|
||||||
'utils/extractText': extractText,
|
'utils/extractText': extractText,
|
||||||
'utils/formatNumber': formatNumber,
|
'utils/formatNumber': formatNumber,
|
||||||
@@ -126,7 +116,6 @@ export default {
|
|||||||
'components/Select': Select,
|
'components/Select': Select,
|
||||||
'components/Navigation': Navigation,
|
'components/Navigation': Navigation,
|
||||||
'components/Alert': Alert,
|
'components/Alert': Alert,
|
||||||
'components/Link': Link,
|
|
||||||
'components/LinkButton': LinkButton,
|
'components/LinkButton': LinkButton,
|
||||||
'components/Checkbox': Checkbox,
|
'components/Checkbox': Checkbox,
|
||||||
'components/SelectDropdown': SelectDropdown,
|
'components/SelectDropdown': SelectDropdown,
|
||||||
@@ -134,8 +123,6 @@ export default {
|
|||||||
'components/Button': Button,
|
'components/Button': Button,
|
||||||
'components/Modal': Modal,
|
'components/Modal': Modal,
|
||||||
'components/GroupBadge': GroupBadge,
|
'components/GroupBadge': GroupBadge,
|
||||||
'components/TextEditor': TextEditor,
|
|
||||||
'components/TextEditorButton': TextEditorButton,
|
|
||||||
Model: Model,
|
Model: Model,
|
||||||
Application: Application,
|
Application: Application,
|
||||||
'helpers/fullTime': fullTime,
|
'helpers/fullTime': fullTime,
|
||||||
@@ -147,5 +134,4 @@ export default {
|
|||||||
'helpers/username': username,
|
'helpers/username': username,
|
||||||
'helpers/userOnline': userOnline,
|
'helpers/userOnline': userOnline,
|
||||||
'helpers/listItems': listItems,
|
'helpers/listItems': listItems,
|
||||||
'resolvers/DefaultResolver': DefaultResolver,
|
|
||||||
};
|
};
|
||||||
|
@@ -1,33 +1,31 @@
|
|||||||
import Component, { ComponentAttrs } from '../Component';
|
import Component from '../Component';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import listItems from '../helpers/listItems';
|
import listItems from '../helpers/listItems';
|
||||||
import extract from '../utils/extract';
|
import extract from '../utils/extract';
|
||||||
import Mithril from 'mithril';
|
|
||||||
|
|
||||||
export interface AlertAttrs extends ComponentAttrs {
|
|
||||||
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
|
|
||||||
type?: string;
|
|
||||||
/** An array of controls to show in the alert. */
|
|
||||||
controls?: Mithril.Children;
|
|
||||||
/** Whether or not the alert can be dismissed. */
|
|
||||||
dismissible?: boolean;
|
|
||||||
/** A callback to run when the alert is dismissed */
|
|
||||||
ondismiss?: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Alert` component represents an alert box, which contains a message,
|
* The `Alert` component represents an alert box, which contains a message,
|
||||||
* some controls, and may be dismissible.
|
* some controls, and may be dismissible.
|
||||||
|
*
|
||||||
|
* ### Attrs
|
||||||
|
*
|
||||||
|
* - `type` The type of alert this is. Will be used to give the alert a class
|
||||||
|
* name of `Alert--{type}`.
|
||||||
|
* - `controls` An array of controls to show in the alert.
|
||||||
|
* - `dismissible` Whether or not the alert can be dismissed.
|
||||||
|
* - `ondismiss` A callback to run when the alert is dismissed.
|
||||||
|
*
|
||||||
|
* All other attrs will be assigned as attributes on the DOM element.
|
||||||
*/
|
*/
|
||||||
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
|
export default class Alert extends Component {
|
||||||
view(vnode: Mithril.Vnode) {
|
view(vnode) {
|
||||||
const attrs = Object.assign({}, this.attrs);
|
const attrs = Object.assign({}, this.attrs);
|
||||||
|
|
||||||
const type = extract(attrs, 'type');
|
const type = extract(attrs, 'type');
|
||||||
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
|
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
|
||||||
|
|
||||||
const content = extract(attrs, 'content') || vnode.children;
|
const content = extract(attrs, 'content') || vnode.children;
|
||||||
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
|
const controls = extract(attrs, 'controls') || [];
|
||||||
|
|
||||||
// If the alert is meant to be dismissible (which is the case by default),
|
// If the alert is meant to be dismissible (which is the case by default),
|
||||||
// then we will create a dismiss button to append as the final control in
|
// then we will create a dismiss button to append as the final control in
|
@@ -35,11 +35,6 @@ export default class Button extends Component {
|
|||||||
attrs['aria-label'] = attrs.title;
|
attrs['aria-label'] = attrs.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If given a translation object, extract the text.
|
|
||||||
if (typeof attrs.title === 'object') {
|
|
||||||
attrs.title = extractText(attrs.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing else is provided, we use the textual button content as tooltip
|
// If nothing else is provided, we use the textual button content as tooltip
|
||||||
if (!attrs.title && vnode.children) {
|
if (!attrs.title && vnode.children) {
|
||||||
attrs.title = extractText(vnode.children);
|
attrs.title = extractText(vnode.children);
|
||||||
|
@@ -13,7 +13,6 @@ import listItems from '../helpers/listItems';
|
|||||||
* - `icon` The name of an icon to show in the dropdown toggle button.
|
* - `icon` The name of an icon to show in the dropdown toggle button.
|
||||||
* - `caretIcon` The name of an icon to show on the right of the button.
|
* - `caretIcon` The name of an icon to show on the right of the button.
|
||||||
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||||
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
|
|
||||||
* - `onhide`
|
* - `onhide`
|
||||||
* - `onshow`
|
* - `onshow`
|
||||||
*
|
*
|
||||||
@@ -26,7 +25,6 @@ export default class Dropdown extends Component {
|
|||||||
attrs.menuClassName = attrs.menuClassName || '';
|
attrs.menuClassName = attrs.menuClassName || '';
|
||||||
attrs.label = attrs.label || '';
|
attrs.label = attrs.label || '';
|
||||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
||||||
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
@@ -94,13 +92,7 @@ export default class Dropdown extends Component {
|
|||||||
*/
|
*/
|
||||||
getButton(children) {
|
getButton(children) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
|
||||||
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-label={this.attrs.accessibleToggleLabel}
|
|
||||||
data-toggle="dropdown"
|
|
||||||
onclick={this.attrs.onclick}
|
|
||||||
>
|
|
||||||
{this.getButtonContent(children)}
|
{this.getButtonContent(children)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
import extract from '../utils/extract';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The link component enables both internal and external links.
|
|
||||||
* It will return a regular HTML link for any links to external sites,
|
|
||||||
* and it will use Mithril's m.route.Link for any internal links.
|
|
||||||
*
|
|
||||||
* Links will default to internal; the 'external' attr must be set to
|
|
||||||
* `true` for the link to be external.
|
|
||||||
*/
|
|
||||||
export default class Link extends Component {
|
|
||||||
view(vnode) {
|
|
||||||
let { options = {}, ...attrs } = vnode.attrs;
|
|
||||||
|
|
||||||
attrs.href = attrs.href || '';
|
|
||||||
|
|
||||||
// For some reason, m.route.Link does not like vnode.text, so if present, we
|
|
||||||
// need to convert it to text vnodes and store it in children.
|
|
||||||
const children = vnode.children || { tag: '#', children: vnode.text };
|
|
||||||
|
|
||||||
if (attrs.external) {
|
|
||||||
return <a {...attrs}>{children}</a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the href URL of the link is the same as the current page path
|
|
||||||
// we will not add a new entry to the browser history.
|
|
||||||
// This allows us to still refresh the Page component
|
|
||||||
// without adding endless history entries.
|
|
||||||
if (attrs.href === m.route.get()) {
|
|
||||||
if (!('replace' in options)) options.replace = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mithril 2 does not completely rerender the page if a route change leads to the same route
|
|
||||||
// (or the same component handling a different route).
|
|
||||||
// Here, the `force` parameter will use Mithril's key system to force a full rerender
|
|
||||||
// see https://mithril.js.org/route.html#key-parameter
|
|
||||||
if (extract(attrs, 'force')) {
|
|
||||||
if (!('state' in options)) options.state = {};
|
|
||||||
if (!('key' in options.state)) options.state.key = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs.options = options;
|
|
||||||
|
|
||||||
return <m.route.Link {...attrs}>{children}</m.route.Link>;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,4 @@
|
|||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Link from './Link';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `LinkButton` component defines a `Button` which links to a route.
|
* The `LinkButton` component defines a `Button` which links to a route.
|
||||||
@@ -12,20 +11,18 @@ import Link from './Link';
|
|||||||
* active.
|
* active.
|
||||||
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
||||||
* the `active` prop will automatically be set to true.
|
* the `active` prop will automatically be set to true.
|
||||||
* - `force` Whether the page should be fully rerendered. Defaults to `true`.
|
|
||||||
*/
|
*/
|
||||||
export default class LinkButton extends Button {
|
export default class LinkButton extends Button {
|
||||||
static initAttrs(attrs) {
|
static initAttrs(attrs) {
|
||||||
super.initAttrs(attrs);
|
super.initAttrs(attrs);
|
||||||
|
|
||||||
attrs.active = this.isActive(attrs);
|
attrs.active = this.isActive(attrs);
|
||||||
if (attrs.force === undefined) attrs.force = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode) {
|
||||||
const vdom = super.view(vnode);
|
const vdom = super.view(vnode);
|
||||||
|
|
||||||
vdom.tag = Link;
|
vdom.tag = m.route.Link;
|
||||||
vdom.attrs.active = String(vdom.attrs.active);
|
vdom.attrs.active = String(vdom.attrs.active);
|
||||||
|
|
||||||
return vdom;
|
return vdom;
|
||||||
|
43
js/src/common/components/LoadingIndicator.js
Normal file
43
js/src/common/components/LoadingIndicator.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Component from '../Component';
|
||||||
|
import { Spinner } from 'spin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LoadingIndicator` component displays a loading spinner with spin.js.
|
||||||
|
*
|
||||||
|
* ### Attrs
|
||||||
|
*
|
||||||
|
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
||||||
|
*
|
||||||
|
* All other attrs will be assigned as attributes on the DOM element.
|
||||||
|
*/
|
||||||
|
export default class LoadingIndicator extends Component {
|
||||||
|
view() {
|
||||||
|
const attrs = Object.assign({}, this.attrs);
|
||||||
|
|
||||||
|
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
||||||
|
delete attrs.size;
|
||||||
|
|
||||||
|
return <div {...attrs}>{m.trust(' ')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
const options = { zIndex: 'auto', color: this.$().css('color') };
|
||||||
|
|
||||||
|
switch (this.attrs.size) {
|
||||||
|
case 'large':
|
||||||
|
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tiny':
|
||||||
|
Object.assign(options, { lines: 8, length: 2, width: 2, radius: 3 });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Object.assign(options, { lines: 8, length: 4, width: 3, radius: 5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
new Spinner(options).spin(this.element);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,61 +0,0 @@
|
|||||||
import Component, { ComponentAttrs } from '../Component';
|
|
||||||
import classList from '../utils/classList';
|
|
||||||
|
|
||||||
export interface LoadingIndicatorAttrs extends ComponentAttrs {
|
|
||||||
/**
|
|
||||||
* Custom classes fro the loading indicator's container.
|
|
||||||
*/
|
|
||||||
className?: string;
|
|
||||||
/**
|
|
||||||
* Custom classes for the loading indicator's container.
|
|
||||||
*/
|
|
||||||
containerClassName?: string;
|
|
||||||
/**
|
|
||||||
* Optional size to specify for the loading indicator.
|
|
||||||
*/
|
|
||||||
size?: 'large' | 'medium' | 'small';
|
|
||||||
/**
|
|
||||||
* Optional attributes to apply to the loading indicator's container.
|
|
||||||
*/
|
|
||||||
containerAttrs?: Partial<ComponentAttrs>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `LoadingIndicator` component displays a simple CSS-based loading spinner.
|
|
||||||
*
|
|
||||||
* To set a custom color, use the CSS `color` property.
|
|
||||||
*
|
|
||||||
* To increase spacing around the spinner, use the CSS `height` property on the
|
|
||||||
* spinner's **container**.
|
|
||||||
*
|
|
||||||
* To apply a custom size to the loading indicator, set the `--size` and
|
|
||||||
* `--thickness` custom properties on the loading indicator itself.
|
|
||||||
*
|
|
||||||
* If you really want to change how this looks as part of your custom theme,
|
|
||||||
* you can override the `border-radius` and `border` then set either a
|
|
||||||
* background image, or use `content: "\<glyph>"` (e.g. `content: "\f1ce"`)
|
|
||||||
* and `font-family: 'Font Awesome 5 Free'` to set an FA icon if you'd rather.
|
|
||||||
*
|
|
||||||
* ### Attrs
|
|
||||||
*
|
|
||||||
* - `containerClassName` Class name(s) to apply to the indicator's parent
|
|
||||||
* - `className` Class name(s) to apply to the indicator itself
|
|
||||||
* - `size` Size of the loading indicator
|
|
||||||
* - `containerAttrs` Optional attrs to be applied to the container DOM element
|
|
||||||
*
|
|
||||||
* All other attrs will be assigned as attributes on the DOM element.
|
|
||||||
*/
|
|
||||||
export default class LoadingIndicator extends Component<LoadingIndicatorAttrs> {
|
|
||||||
view() {
|
|
||||||
const { size, ...attrs } = this.attrs;
|
|
||||||
|
|
||||||
attrs.className = classList({ LoadingIndicator: true, [attrs.className || '']: true });
|
|
||||||
attrs.containerClassName = classList({ 'LoadingIndicator-container': true, [attrs.containerClassName || '']: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attrs.containerAttrs} data-size={size} className={attrs.containerClassName}>
|
|
||||||
<div {...attrs}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -24,20 +24,11 @@ export default class Modal extends Component {
|
|||||||
oncreate(vnode) {
|
oncreate(vnode) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
this.attrs.animateShow(() => this.onready());
|
this.attrs.onshow(() => this.onready());
|
||||||
}
|
}
|
||||||
|
|
||||||
onbeforeremove() {
|
onremove() {
|
||||||
// If the global modal state currently contains a modal,
|
this.attrs.onhide();
|
||||||
// we've just opened up a new one, and accordingly,
|
|
||||||
// we don't need to show a hide animation.
|
|
||||||
if (!this.attrs.state.modal) {
|
|
||||||
this.attrs.animateHide();
|
|
||||||
// Here, we ensure that the animation has time to complete.
|
|
||||||
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
|
|
||||||
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
@@ -112,11 +103,13 @@ export default class Modal extends Component {
|
|||||||
this.$('form').find('input, select, textarea').first().focus().select();
|
this.$('form').find('input, select, textarea').first().focus().select();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onhide() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the modal.
|
* Hide the modal.
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
this.attrs.state.close();
|
this.attrs.onhide();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,14 +11,7 @@ export default class ModalManager extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ModalManager modal fade">
|
<div className="ModalManager modal fade">
|
||||||
{modal
|
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
|
||||||
? modal.componentClass.component({
|
|
||||||
...modal.attrs,
|
|
||||||
animateShow: this.animateShow.bind(this),
|
|
||||||
animateHide: this.animateHide.bind(this),
|
|
||||||
state: this.attrs.state,
|
|
||||||
})
|
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,14 +28,6 @@ export default class ModalManager extends Component {
|
|||||||
animateShow(readyCallback) {
|
animateShow(readyCallback) {
|
||||||
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
||||||
|
|
||||||
// If we are opening this modal while another modal is already open,
|
|
||||||
// the shown event will not run, because the modal is already open.
|
|
||||||
// So, we need to manually trigger the readyCallback.
|
|
||||||
if (this.$().hasClass('in')) {
|
|
||||||
readyCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$()
|
this.$()
|
||||||
.one('shown.bs.modal', readyCallback)
|
.one('shown.bs.modal', readyCallback)
|
||||||
.modal({
|
.modal({
|
||||||
|
@@ -11,7 +11,9 @@ export default class Page extends Component {
|
|||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
app.previous = app.current;
|
app.previous = app.current;
|
||||||
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
|
app.current = new PageState(this.constructor);
|
||||||
|
|
||||||
|
this.onNewRoute();
|
||||||
|
|
||||||
app.drawer.hide();
|
app.drawer.hide();
|
||||||
app.modal.close();
|
app.modal.close();
|
||||||
@@ -22,20 +24,16 @@ export default class Page extends Component {
|
|||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
this.bodyClass = '';
|
this.bodyClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we should scroll to the top of the page when its rendered.
|
* A collections of actions to run when the route changes.
|
||||||
*
|
* This is extracted here, and not hardcoded in oninit, as oninit is not called
|
||||||
* @type {Boolean}
|
* when a different route is handled by the same component, but we still need to
|
||||||
|
* adjust the current route name.
|
||||||
*/
|
*/
|
||||||
this.scrollTopOnCreate = true;
|
onNewRoute() {
|
||||||
|
app.current.set('routeName', this.attrs.routeName);
|
||||||
/**
|
|
||||||
* Whether the browser should restore scroll state on refreshes.
|
|
||||||
*
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this.useBrowserScrollRestoration = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode) {
|
||||||
@@ -44,14 +42,6 @@ export default class Page extends Component {
|
|||||||
if (this.bodyClass) {
|
if (this.bodyClass) {
|
||||||
$('#app').addClass(this.bodyClass);
|
$('#app').addClass(this.bodyClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.scrollTopOnCreate) {
|
|
||||||
$(window).scrollTop(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = this.useBrowserScrollRestoration ? 'auto' : 'manual';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onremove() {
|
onremove() {
|
||||||
|
@@ -12,9 +12,6 @@ import icon from '../helpers/icon';
|
|||||||
function isActive(vnode) {
|
function isActive(vnode) {
|
||||||
const tag = vnode.tag;
|
const tag = vnode.tag;
|
||||||
|
|
||||||
// Allow non-selectable dividers/headers to be added.
|
|
||||||
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
|
|
||||||
|
|
||||||
if ('initAttrs' in tag) {
|
if ('initAttrs' in tag) {
|
||||||
tag.initAttrs(vnode.attrs);
|
tag.initAttrs(vnode.attrs);
|
||||||
}
|
}
|
||||||
|
@@ -24,12 +24,7 @@ export default class SplitDropdown extends Dropdown {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
Button.component(buttonAttrs, firstChild.children),
|
Button.component(buttonAttrs, firstChild.children),
|
||||||
<button
|
<button className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} data-toggle="dropdown">
|
||||||
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-label={this.attrs.accessibleToggleLabel}
|
|
||||||
data-toggle="dropdown"
|
|
||||||
>
|
|
||||||
{icon(this.attrs.icon, { className: 'Button-icon' })}
|
{icon(this.attrs.icon, { className: 'Button-icon' })}
|
||||||
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
||||||
</button>,
|
</button>,
|
||||||
|
@@ -1,28 +1,26 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
import User from '../models/User';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `avatar` helper displays a user's avatar.
|
* The `avatar` helper displays a user's avatar.
|
||||||
*
|
*
|
||||||
* @param user
|
* @param {User} user
|
||||||
* @param attrs Attributes to apply to the avatar element
|
* @param {Object} attrs Attributes to apply to the avatar element
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function avatar(user: User, attrs: Object = {}): Mithril.Vnode {
|
export default function avatar(user, attrs = {}) {
|
||||||
attrs.className = 'Avatar ' + (attrs.className || '');
|
attrs.className = 'Avatar ' + (attrs.className || '');
|
||||||
let content: string = '';
|
let content = '';
|
||||||
|
|
||||||
// If the `title` attribute is set to null or false, we don't want to give the
|
// If the `title` attribute is set to null or false, we don't want to give the
|
||||||
// avatar a title. On the other hand, if it hasn't been given at all, we can
|
// avatar a title. On the other hand, if it hasn't been given at all, we can
|
||||||
// safely default it to the user's username.
|
// safely default it to the user's username.
|
||||||
const hasTitle: boolean | string = attrs.title === 'undefined' || attrs.title;
|
const hasTitle = attrs.title === 'undefined' || attrs.title;
|
||||||
if (!hasTitle) delete attrs.title;
|
if (!hasTitle) delete attrs.title;
|
||||||
|
|
||||||
// If a user has been passed, then we will set up an avatar using their
|
// If a user has been passed, then we will set up an avatar using their
|
||||||
// uploaded image, or the first letter of their username if they haven't
|
// uploaded image, or the first letter of their username if they haven't
|
||||||
// uploaded one.
|
// uploaded one.
|
||||||
if (user) {
|
if (user) {
|
||||||
const username: string = user.displayName() || '?';
|
const username = user.displayName() || '?';
|
||||||
const avatarUrl: string = user.avatarUrl();
|
const avatarUrl = user.avatarUrl();
|
||||||
|
|
||||||
if (hasTitle) attrs.title = attrs.title || username;
|
if (hasTitle) attrs.title = attrs.title || username;
|
||||||
|
|
@@ -1,11 +1,11 @@
|
|||||||
import dayjs from 'dayjs';
|
|
||||||
import * as Mithril from 'mithril';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `fullTime` helper displays a formatted time string wrapped in a <time>
|
* The `fullTime` helper displays a formatted time string wrapped in a <time>
|
||||||
* tag.
|
* tag.
|
||||||
|
*
|
||||||
|
* @param {Date} time
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function fullTime(time: Date): Mithril.Vnode {
|
export default function fullTime(time) {
|
||||||
const d = dayjs(time);
|
const d = dayjs(time);
|
||||||
|
|
||||||
const datetime = d.format();
|
const datetime = d.format();
|
@@ -1,16 +1,16 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
import { truncate } from '../utils/string';
|
import { truncate } from '../utils/string';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `highlight` helper searches for a word phrase in a string, and wraps
|
* The `highlight` helper searches for a word phrase in a string, and wraps
|
||||||
* matches with the <mark> tag.
|
* matches with the <mark> tag.
|
||||||
*
|
*
|
||||||
* @param string The string to highlight.
|
* @param {String} string The string to highlight.
|
||||||
* @param phrase The word or words to highlight.
|
* @param {String|RegExp} phrase The word or words to highlight.
|
||||||
* @param [length] The number of characters to truncate the string to.
|
* @param {Integer} [length] The number of characters to truncate the string to.
|
||||||
* The string will be truncated surrounding the first match.
|
* The string will be truncated surrounding the first match.
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function highlight(string: string, phrase: string | RegExp, length?: number): Mithril.Vnode<any, any> | string {
|
export default function highlight(string, phrase, length) {
|
||||||
if (!phrase && !length) return string;
|
if (!phrase && !length) return string;
|
||||||
|
|
||||||
// Convert the word phrase into a global regular expression (if it isn't
|
// Convert the word phrase into a global regular expression (if it isn't
|
@@ -1,13 +1,14 @@
|
|||||||
import dayjs from 'dayjs';
|
|
||||||
import * as Mithril from 'mithril';
|
|
||||||
import humanTimeUtil from '../utils/humanTime';
|
import humanTimeUtil from '../utils/humanTime';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `humanTime` helper displays a time in a human-friendly time-ago format
|
* The `humanTime` helper displays a time in a human-friendly time-ago format
|
||||||
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
|
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
|
||||||
* the time.
|
* the time.
|
||||||
|
*
|
||||||
|
* @param {Date} time
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function humanTime(time: Date): Mithril.Vnode {
|
export default function humanTime(time) {
|
||||||
const d = dayjs(time);
|
const d = dayjs(time);
|
||||||
|
|
||||||
const datetime = d.format();
|
const datetime = d.format();
|
12
js/src/common/helpers/icon.js
Normal file
12
js/src/common/helpers/icon.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* The `icon` helper displays an icon.
|
||||||
|
*
|
||||||
|
* @param {String} fontClass The full icon class, prefix and the icon’s name.
|
||||||
|
* @param {Object} attrs Any other attributes to apply.
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
export default function icon(fontClass, attrs = {}) {
|
||||||
|
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
||||||
|
|
||||||
|
return <i {...attrs} />;
|
||||||
|
}
|
@@ -1,13 +0,0 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `icon` helper displays an icon.
|
|
||||||
*
|
|
||||||
* @param fontClass The full icon class, prefix and the icon’s name.
|
|
||||||
* @param attrs Any other attributes to apply.
|
|
||||||
*/
|
|
||||||
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
|
|
||||||
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
|
||||||
|
|
||||||
return <i {...attrs} />;
|
|
||||||
}
|
|
@@ -1,16 +1,15 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
import Separator from '../components/Separator';
|
import Separator from '../components/Separator';
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
function isSeparator(item): boolean {
|
function isSeparator(item) {
|
||||||
return item.tag === Separator;
|
return item.tag === Separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutUnnecessarySeparators(items: Array<Mithril.Vnode>): Array<Mithril.Vnode> {
|
function withoutUnnecessarySeparators(items) {
|
||||||
const newItems = [];
|
const newItems = [];
|
||||||
let prevItem;
|
let prevItem;
|
||||||
|
|
||||||
items.filter(Boolean).forEach((item: Mithril.Vnode, i: number) => {
|
items.filter(Boolean).forEach((item, i) => {
|
||||||
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
||||||
prevItem = item;
|
prevItem = item;
|
||||||
newItems.push(item);
|
newItems.push(item);
|
||||||
@@ -23,11 +22,14 @@ function withoutUnnecessarySeparators(items: Array<Mithril.Vnode>): Array<Mithri
|
|||||||
/**
|
/**
|
||||||
* The `listItems` helper wraps a collection of components in <li> tags,
|
* The `listItems` helper wraps a collection of components in <li> tags,
|
||||||
* stripping out any unnecessary `Separator` components.
|
* stripping out any unnecessary `Separator` components.
|
||||||
|
*
|
||||||
|
* @param {*} items
|
||||||
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>): Array<Mithril.Vnode> {
|
export default function listItems(items) {
|
||||||
if (!(items instanceof Array)) items = [items];
|
if (!(items instanceof Array)) items = [items];
|
||||||
|
|
||||||
return withoutUnnecessarySeparators(items).map((item: Mithril.Vnode) => {
|
return withoutUnnecessarySeparators(items).map((item) => {
|
||||||
const isListItem = item.tag && item.tag.isListItem;
|
const isListItem = item.tag && item.tag.isListItem;
|
||||||
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
|
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
|
||||||
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
|
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
|
||||||
@@ -38,7 +40,7 @@ export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>):
|
|||||||
item.key = item.attrs.key;
|
item.key = item.attrs.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: Mithril.Vnode = isListItem ? (
|
const node = isListItem ? (
|
||||||
item
|
item
|
||||||
) : (
|
) : (
|
||||||
<li
|
<li
|
||||||
@@ -49,6 +51,8 @@ export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>):
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
node.state = node.state || {};
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
}
|
}
|
@@ -1,11 +1,12 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
import User from '../models/User';
|
|
||||||
import icon from './icon';
|
import icon from './icon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `useronline` helper displays a green circle if the user is online
|
* The `useronline` helper displays a green circle if the user is online
|
||||||
|
*
|
||||||
|
* @param {User} user
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function userOnline(user: User): Mithril.Vnode {
|
export default function userOnline(user) {
|
||||||
if (user.lastSeenAt() && user.isOnline()) {
|
if (user.lastSeenAt() && user.isOnline()) {
|
||||||
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
|
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
|
||||||
}
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
import * as Mithril from 'mithril';
|
|
||||||
import User from '../models/User';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `username` helper displays a user's username in a <span class="username">
|
* The `username` helper displays a user's username in a <span class="username">
|
||||||
* tag. If the user doesn't exist, the username will be displayed as [deleted].
|
* tag. If the user doesn't exist, the username will be displayed as [deleted].
|
||||||
|
*
|
||||||
|
* @param {User} user
|
||||||
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export default function username(user: User): Mithril.Vnode {
|
export default function username(user) {
|
||||||
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
|
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
|
||||||
|
|
||||||
return <span className="username">{name}</span>;
|
return <span className="username">{name}</span>;
|
@@ -1,8 +1,7 @@
|
|||||||
// Expose jQuery, mithril and dayjs to the window browser object
|
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
||||||
import 'expose-loader?exposes=$,jQuery!jquery';
|
import 'expose-loader?m!mithril';
|
||||||
import 'expose-loader?exposes=m!mithril';
|
import 'expose-loader?moment!expose-loader?dayjs!dayjs';
|
||||||
import 'expose-loader?exposes=dayjs!dayjs';
|
import 'expose-loader?m.bidi!m.attrs.bidi';
|
||||||
|
|
||||||
import 'bootstrap/js/affix';
|
import 'bootstrap/js/affix';
|
||||||
import 'bootstrap/js/dropdown';
|
import 'bootstrap/js/dropdown';
|
||||||
import 'bootstrap/js/modal';
|
import 'bootstrap/js/modal';
|
||||||
|
@@ -10,7 +10,6 @@ export default class User extends Model {}
|
|||||||
|
|
||||||
Object.assign(User.prototype, {
|
Object.assign(User.prototype, {
|
||||||
username: Model.attribute('username'),
|
username: Model.attribute('username'),
|
||||||
slug: Model.attribute('slug'),
|
|
||||||
displayName: Model.attribute('displayName'),
|
displayName: Model.attribute('displayName'),
|
||||||
email: Model.attribute('email'),
|
email: Model.attribute('email'),
|
||||||
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
|
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
|
||||||
@@ -30,8 +29,6 @@ Object.assign(User.prototype, {
|
|||||||
commentCount: Model.attribute('commentCount'),
|
commentCount: Model.attribute('commentCount'),
|
||||||
|
|
||||||
canEdit: Model.attribute('canEdit'),
|
canEdit: Model.attribute('canEdit'),
|
||||||
canEditCredentials: Model.attribute('canEditCredentials'),
|
|
||||||
canEditGroups: Model.attribute('canEditGroups'),
|
|
||||||
canDelete: Model.attribute('canDelete'),
|
canDelete: Model.attribute('canDelete'),
|
||||||
|
|
||||||
avatarColor: null,
|
avatarColor: null,
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
import Mithril from 'mithril';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a route resolver for a given component.
|
|
||||||
* In addition to regular route resolver functionality:
|
|
||||||
* - It provide the current route name as an attr
|
|
||||||
* - It sets a key on the component so a rerender will be triggered on route change.
|
|
||||||
*/
|
|
||||||
export default class DefaultResolver {
|
|
||||||
component: Mithril.Component;
|
|
||||||
routeName: string;
|
|
||||||
|
|
||||||
constructor(component, routeName) {
|
|
||||||
this.component = component;
|
|
||||||
this.routeName = routeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a route change results in a changed key, a full page
|
|
||||||
* rerender occurs. This method can be overriden in subclasses
|
|
||||||
* to prevent rerenders on some route changes.
|
|
||||||
*/
|
|
||||||
makeKey() {
|
|
||||||
return this.routeName + JSON.stringify(m.route.param());
|
|
||||||
}
|
|
||||||
|
|
||||||
makeAttrs(vnode) {
|
|
||||||
return {
|
|
||||||
...vnode.attrs,
|
|
||||||
routeName: this.routeName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onmatch(args, requestedPath, route) {
|
|
||||||
return this.component;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(vnode) {
|
|
||||||
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
|
|
||||||
}
|
|
||||||
}
|
|
50
js/src/common/states/AlertManagerState.js
Normal file
50
js/src/common/states/AlertManagerState.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Alert from '../components/Alert';
|
||||||
|
|
||||||
|
export default class AlertManagerState {
|
||||||
|
constructor() {
|
||||||
|
this.activeAlerts = {};
|
||||||
|
this.alertId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveAlerts() {
|
||||||
|
return this.activeAlerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an Alert in the alerts area.
|
||||||
|
*/
|
||||||
|
show(children, attrs, componentClass = Alert) {
|
||||||
|
// Breaking Change Compliance Warning, Remove in Beta 15.
|
||||||
|
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
|
||||||
|
if (attrs === Alert || attrs instanceof Alert) {
|
||||||
|
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
||||||
|
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||||
|
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||||
|
}
|
||||||
|
// End Change Compliance Warning, Remove in Beta 15
|
||||||
|
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
return this.alertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss an alert.
|
||||||
|
*/
|
||||||
|
dismiss(key) {
|
||||||
|
if (!key || !(key in this.activeAlerts)) return;
|
||||||
|
|
||||||
|
delete this.activeAlerts[key];
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all alerts.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.activeAlerts = {};
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,80 +0,0 @@
|
|||||||
import Mithril from 'mithril';
|
|
||||||
import Alert, { AlertAttrs } from '../components/Alert';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returned by `AlertManagerState.show`. Used to dismiss alerts.
|
|
||||||
*/
|
|
||||||
export type AlertIdentifier = number;
|
|
||||||
|
|
||||||
export interface AlertState {
|
|
||||||
componentClass: typeof Alert;
|
|
||||||
attrs: AlertAttrs;
|
|
||||||
children: Mithril.Children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AlertManagerState {
|
|
||||||
protected activeAlerts: { [id: number]: AlertState } = {};
|
|
||||||
protected alertId = 0;
|
|
||||||
|
|
||||||
getActiveAlerts() {
|
|
||||||
return this.activeAlerts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an Alert in the alerts area.
|
|
||||||
*
|
|
||||||
* @returns The alert's ID, which can be used to dismiss the alert.
|
|
||||||
*/
|
|
||||||
show(children: Mithril.Children): AlertIdentifier;
|
|
||||||
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
|
||||||
show(componentClass: Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
|
||||||
|
|
||||||
show(arg1: any, arg2?: any, arg3?: any) {
|
|
||||||
// Assigns variables as per the above signatures
|
|
||||||
let componentClass = Alert;
|
|
||||||
let attrs: AlertAttrs = {};
|
|
||||||
let children: Mithril.Children;
|
|
||||||
|
|
||||||
if (arguments.length == 1) {
|
|
||||||
children = arg1 as Mithril.Children;
|
|
||||||
} else if (arguments.length == 2) {
|
|
||||||
attrs = arg1 as AlertAttrs;
|
|
||||||
children = arg2 as Mithril.Children;
|
|
||||||
} else if (arguments.length == 3) {
|
|
||||||
componentClass = arg1 as typeof Alert;
|
|
||||||
attrs = arg2 as AlertAttrs;
|
|
||||||
children = arg3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Breaking Change Compliance Warning, Remove in Beta 15.
|
|
||||||
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
|
|
||||||
if (attrs === Alert || attrs instanceof Alert) {
|
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
|
||||||
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
|
||||||
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
|
||||||
}
|
|
||||||
// End Change Compliance Warning, Remove in Beta 15
|
|
||||||
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
return this.alertId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismiss an alert.
|
|
||||||
*/
|
|
||||||
dismiss(key: AlertIdentifier): void {
|
|
||||||
if (!key || !(key in this.activeAlerts)) return;
|
|
||||||
|
|
||||||
delete this.activeAlerts[key];
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all alerts.
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this.activeAlerts = {};
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
import getCaretCoordinates from 'textarea-caret';
|
|
||||||
import EditorDriverInterface, { EditorDriverParams } from './EditorDriverInterface';
|
|
||||||
|
|
||||||
export default class BasicEditorDriver implements EditorDriverInterface {
|
|
||||||
el: HTMLTextAreaElement;
|
|
||||||
|
|
||||||
constructor(dom: HTMLElement, params: EditorDriverParams) {
|
|
||||||
this.el = document.createElement('textarea');
|
|
||||||
|
|
||||||
this.build(dom, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
build(dom: HTMLElement, params: EditorDriverParams) {
|
|
||||||
this.el.className = params.classNames.join(' ');
|
|
||||||
this.el.disabled = params.disabled;
|
|
||||||
this.el.placeholder = params.placeholder;
|
|
||||||
this.el.value = params.value;
|
|
||||||
|
|
||||||
const callInputListeners = (e) => {
|
|
||||||
params.inputListeners.forEach((listener) => {
|
|
||||||
listener();
|
|
||||||
});
|
|
||||||
|
|
||||||
e.redraw = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.el.oninput = (e) => {
|
|
||||||
params.oninput(this.el.value);
|
|
||||||
callInputListeners(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.el.onclick = callInputListeners;
|
|
||||||
this.el.onkeyup = callInputListeners;
|
|
||||||
|
|
||||||
this.el.addEventListener('keydown', function (e) {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
||||||
params.onsubmit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dom.append(this.el);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setValue(value: string) {
|
|
||||||
$(this.el).val(value).trigger('input');
|
|
||||||
|
|
||||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
moveCursorTo(position: number) {
|
|
||||||
this.setSelectionRange(position, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectionRange(): Array<number> {
|
|
||||||
return [this.el.selectionStart, this.el.selectionEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
getLastNChars(n: number): string {
|
|
||||||
const value = this.el.value;
|
|
||||||
|
|
||||||
return value.slice(Math.max(0, this.el.selectionStart - n), this.el.selectionStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertAtCursor(text: string) {
|
|
||||||
this.insertAt(this.el.selectionStart, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertAt(pos: number, text: string) {
|
|
||||||
this.insertBetween(pos, pos, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertBetween(start: number, end: number, text: string) {
|
|
||||||
const value = this.el.value;
|
|
||||||
|
|
||||||
const before = value.slice(0, start);
|
|
||||||
const after = value.slice(end);
|
|
||||||
|
|
||||||
this.setValue(`${before}${text}${after}`);
|
|
||||||
|
|
||||||
// Move the textarea cursor to the end of the content we just inserted.
|
|
||||||
this.moveCursorTo(start + text.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceBeforeCursor(start: number, text: string) {
|
|
||||||
this.insertBetween(start, this.el.selectionStart, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setSelectionRange(start: number, end: number) {
|
|
||||||
this.el.setSelectionRange(start, end);
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
getCaretCoordinates(position: number) {
|
|
||||||
const relCoords = getCaretCoordinates(this.el, position);
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: relCoords.top - this.el.scrollTop,
|
|
||||||
left: relCoords.left,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOM Interactions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the disabled status of the editor.
|
|
||||||
*/
|
|
||||||
disabled(disabled: boolean) {
|
|
||||||
this.el.disabled = disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus on the editor.
|
|
||||||
*/
|
|
||||||
focus() {
|
|
||||||
this.el.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this.el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -31,24 +31,7 @@ export default class Drawer {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
/**
|
$('#app').removeClass('drawerOpen');
|
||||||
* As part of hiding the drawer, this function also ensures that the drawer
|
|
||||||
* correctly animates out, while ensuring it is not part of the navigation
|
|
||||||
* tree while off-screen.
|
|
||||||
*
|
|
||||||
* More info: https://github.com/flarum/core/pull/2666#discussion_r595381014
|
|
||||||
*/
|
|
||||||
|
|
||||||
const $app = $('#app');
|
|
||||||
|
|
||||||
if (!$app.hasClass('drawerOpen')) return;
|
|
||||||
|
|
||||||
const $drawer = $('#drawer');
|
|
||||||
|
|
||||||
// Used to prevent `visibility: hidden` from breaking the exit animation
|
|
||||||
$drawer.css('visibility', 'visible').one('transitionend', () => $drawer.css('visibility', ''));
|
|
||||||
|
|
||||||
$app.removeClass('drawerOpen');
|
|
||||||
|
|
||||||
if (this.$backdrop) this.$backdrop.remove();
|
if (this.$backdrop) this.$backdrop.remove();
|
||||||
}
|
}
|
||||||
|
@@ -1,105 +0,0 @@
|
|||||||
export interface EditorDriverParams {
|
|
||||||
/**
|
|
||||||
* An array of HTML class names to apply to the editor's main DOM element.
|
|
||||||
*/
|
|
||||||
classNames: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the editor should be initially disabled.
|
|
||||||
*/
|
|
||||||
disabled: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional placeholder for the editor.
|
|
||||||
*/
|
|
||||||
placeholder: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An optional initial value for the editor.
|
|
||||||
*/
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is separate from inputListeners since the full serialized content will be passed to it.
|
|
||||||
* It is considered private API, and should not be used/modified by extensions not implementing
|
|
||||||
* EditorDriverInterface.
|
|
||||||
*/
|
|
||||||
oninput: Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Each of these functions will be called on click, input, and keyup.
|
|
||||||
* No arguments will be passed.
|
|
||||||
*/
|
|
||||||
inputListeners: Function[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function will be called if submission is triggered programmatically via keybind.
|
|
||||||
* No arguments should be passed.
|
|
||||||
*/
|
|
||||||
onsubmit: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default interface EditorDriverInterface {
|
|
||||||
/**
|
|
||||||
* Focus the editor and place the cursor at the given position.
|
|
||||||
*/
|
|
||||||
moveCursorTo(position: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected range of the editor.
|
|
||||||
*/
|
|
||||||
getSelectionRange(): Array<number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last N characters from the current "text block".
|
|
||||||
*
|
|
||||||
* A textarea-based driver would just return the last N characters,
|
|
||||||
* but more advanced implementations might restrict to the current block.
|
|
||||||
*
|
|
||||||
* This is useful for monitoring recent user input to trigger autocomplete.
|
|
||||||
*/
|
|
||||||
getLastNChars(n: number): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the editor at the position of the cursor.
|
|
||||||
*/
|
|
||||||
insertAtCursor(text: string, escape: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the editor at the given position.
|
|
||||||
*/
|
|
||||||
insertAt(pos: number, text: string, escape: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the editor between the given positions.
|
|
||||||
*
|
|
||||||
* If the start and end positions are different, any text between them will be
|
|
||||||
* overwritten.
|
|
||||||
*/
|
|
||||||
insertBetween(start: number, end: number, text: string, escape: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace existing content from the start to the current cursor position.
|
|
||||||
*/
|
|
||||||
replaceBeforeCursor(start: number, text: string, escape: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get left and top coordinates of the caret relative to the editor viewport.
|
|
||||||
*/
|
|
||||||
getCaretCoordinates(position: number): { left: number; top: number };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the disabled status of the editor.
|
|
||||||
*/
|
|
||||||
disabled(disabled: boolean): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus on the editor.
|
|
||||||
*/
|
|
||||||
focus(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor
|
|
||||||
*/
|
|
||||||
destroy(): void;
|
|
||||||
}
|
|
@@ -58,7 +58,7 @@ export default class ScrollListener {
|
|||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
window.addEventListener('scroll', (this.active = this.loop.bind(this)), { passive: true });
|
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
import Stream from 'mithril/stream';
|
|
||||||
|
|
||||||
export default Stream;
|
|
@@ -28,9 +28,6 @@ export default class SubtreeRetainer {
|
|||||||
constructor(...callbacks) {
|
constructor(...callbacks) {
|
||||||
this.callbacks = callbacks;
|
this.callbacks = callbacks;
|
||||||
this.data = {};
|
this.data = {};
|
||||||
// Build the initial data, so it is available when calling
|
|
||||||
// needsRebuild from the onbeforeupdate hook.
|
|
||||||
this.needsRebuild();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,8 +60,6 @@ export default class SubtreeRetainer {
|
|||||||
*/
|
*/
|
||||||
check(...callbacks) {
|
check(...callbacks) {
|
||||||
this.callbacks = this.callbacks.concat(callbacks);
|
this.callbacks = this.callbacks.concat(callbacks);
|
||||||
// Update the data cache when new checks are added.
|
|
||||||
this.needsRebuild();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
109
js/src/common/utils/SuperTextarea.js
Normal file
109
js/src/common/utils/SuperTextarea.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* A textarea wrapper with powerful helpers for text manipulation.
|
||||||
|
*
|
||||||
|
* This wraps a <textarea> DOM element and allows directly manipulating its text
|
||||||
|
* contents and cursor positions.
|
||||||
|
*
|
||||||
|
* I apologize for the pretentious name. :)
|
||||||
|
*/
|
||||||
|
export default class SuperTextarea {
|
||||||
|
/**
|
||||||
|
* @param {HTMLTextAreaElement} textarea
|
||||||
|
*/
|
||||||
|
constructor(textarea) {
|
||||||
|
this.el = textarea;
|
||||||
|
this.$ = $(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the text editor.
|
||||||
|
*
|
||||||
|
* @param {String} value
|
||||||
|
*/
|
||||||
|
setValue(value) {
|
||||||
|
this.$.val(value).trigger('input');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus the textarea and place the cursor at the given index.
|
||||||
|
*
|
||||||
|
* @param {number} position
|
||||||
|
*/
|
||||||
|
moveCursorTo(position) {
|
||||||
|
this.setSelectionRange(position, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected range of the textarea.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
getSelectionRange() {
|
||||||
|
return [this.el.selectionStart, this.el.selectionEnd];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert content into the textarea at the position of the cursor.
|
||||||
|
*
|
||||||
|
* @param {String} text
|
||||||
|
*/
|
||||||
|
insertAtCursor(text) {
|
||||||
|
this.insertAt(this.el.selectionStart, text);
|
||||||
|
|
||||||
|
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert content into the textarea at the given position.
|
||||||
|
*
|
||||||
|
* @param {number} pos
|
||||||
|
* @param {String} text
|
||||||
|
*/
|
||||||
|
insertAt(pos, text) {
|
||||||
|
this.insertBetween(pos, pos, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert content into the textarea between the given positions.
|
||||||
|
*
|
||||||
|
* If the start and end positions are different, any text between them will be
|
||||||
|
* overwritten.
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
insertBetween(start, end, text) {
|
||||||
|
const value = this.el.value;
|
||||||
|
|
||||||
|
const before = value.slice(0, start);
|
||||||
|
const after = value.slice(end);
|
||||||
|
|
||||||
|
this.setValue(`${before}${text}${after}`);
|
||||||
|
|
||||||
|
// Move the textarea cursor to the end of the content we just inserted.
|
||||||
|
this.moveCursorTo(start + text.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace existing content from the start to the current cursor position.
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
replaceBeforeCursor(start, text) {
|
||||||
|
this.insertBetween(start, this.el.selectionStart, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected range of the textarea.
|
||||||
|
*
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} end
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
setSelectionRange(start, end) {
|
||||||
|
this.el.setSelectionRange(start, end);
|
||||||
|
this.$.focus();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,50 +0,0 @@
|
|||||||
function bidi(node, prop) {
|
|
||||||
var type = node.tag === 'select' ? (node.attrs.multi ? 'multi' : 'select') : node.attrs.type;
|
|
||||||
|
|
||||||
// Setup: bind listeners
|
|
||||||
if (type === 'multi') {
|
|
||||||
node.attrs.onchange = function () {
|
|
||||||
prop(
|
|
||||||
[].slice.call(this.selectedOptions, function (x) {
|
|
||||||
return x.value;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
} else if (type === 'select') {
|
|
||||||
node.attrs.onchange = function (e) {
|
|
||||||
prop(this.selectedOptions[0].value);
|
|
||||||
};
|
|
||||||
} else if (type === 'checkbox') {
|
|
||||||
node.attrs.onchange = function (e) {
|
|
||||||
prop(this.checked);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
node.attrs.onchange = node.attrs.oninput = function (e) {
|
|
||||||
prop(this.value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.tag === 'select') {
|
|
||||||
node.children.forEach(function (option) {
|
|
||||||
if (option.attrs.value === prop() || option.children[0] === prop()) {
|
|
||||||
option.attrs.selected = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'checkbox') {
|
|
||||||
node.attrs.checked = prop();
|
|
||||||
} else if (type === 'radio') {
|
|
||||||
node.attrs.checked = prop() === node.attrs.value;
|
|
||||||
} else {
|
|
||||||
node.attrs.value = prop();
|
|
||||||
}
|
|
||||||
|
|
||||||
node.attrs.bidi = null;
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
bidi.view = function (ctrl, node, prop) {
|
|
||||||
return bidi(node, node.attrs.bidi);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default bidi;
|
|
26
js/src/common/utils/classList.js
Normal file
26
js/src/common/utils/classList.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* The `classList` utility creates a list of class names by joining an object's
|
||||||
|
* keys, but only for values which are truthy.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* classList({ foo: true, bar: false, qux: 'qaz' });
|
||||||
|
* // "foo qux"
|
||||||
|
*
|
||||||
|
* @param {Object} classes
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
export default function classList(classes) {
|
||||||
|
let classNames;
|
||||||
|
|
||||||
|
if (classes instanceof Array) {
|
||||||
|
classNames = classes.filter((name) => name);
|
||||||
|
} else {
|
||||||
|
classNames = [];
|
||||||
|
|
||||||
|
for (const i in classes) {
|
||||||
|
if (classes[i]) classNames.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classNames.join(' ');
|
||||||
|
}
|
@@ -1,12 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This util exposes `clsx` to core and extensions as a re-usable utility.
|
|
||||||
*
|
|
||||||
* For full documentation, see `clsx` on GitHub.
|
|
||||||
*
|
|
||||||
* @see https://github.com/lukeed/clsx
|
|
||||||
*/
|
|
||||||
const classList = clsx;
|
|
||||||
|
|
||||||
export default classList;
|
|
@@ -1,6 +1,3 @@
|
|||||||
import dayjs from 'dayjs';
|
|
||||||
import 'dayjs/plugin/relativeTime';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||||
* ago string.
|
* ago string.
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
import DefaultResolver from '../resolvers/DefaultResolver';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `mapRoutes` utility converts a map of named application routes into a
|
* The `mapRoutes` utility converts a map of named application routes into a
|
||||||
* format that can be understood by Mithril, and wraps them in route resolvers
|
* format that can be understood by Mithril.
|
||||||
* to provide each route with the current route name.
|
|
||||||
*
|
*
|
||||||
* @see https://mithril.js.org/route.html#signature
|
* @see https://mithril.js.org/route.html#signature
|
||||||
* @param {Object} routes
|
* @param {Object} routes
|
||||||
@@ -13,17 +10,14 @@ import DefaultResolver from '../resolvers/DefaultResolver';
|
|||||||
export default function mapRoutes(routes, basePath = '') {
|
export default function mapRoutes(routes, basePath = '') {
|
||||||
const map = {};
|
const map = {};
|
||||||
|
|
||||||
for (const routeName in routes) {
|
for (const key in routes) {
|
||||||
const route = routes[routeName];
|
const route = routes[key];
|
||||||
|
|
||||||
if ('resolver' in route) {
|
map[basePath + route.path] = {
|
||||||
map[basePath + route.path] = route.resolver;
|
render() {
|
||||||
} else if ('component' in route) {
|
return m(route.component, { routeName: key });
|
||||||
const resolverClass = 'resolverClass' in route ? route.resolverClass : DefaultResolver;
|
},
|
||||||
map[basePath + route.path] = new resolverClass(route.component, routeName);
|
};
|
||||||
} else {
|
|
||||||
throw new Error(`Either a resolver or a component must be provided for the route [${routeName}]`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
|
@@ -1,8 +1,39 @@
|
|||||||
import bidi from './bidi';
|
import Stream from 'mithril/stream';
|
||||||
|
import extract from './extract';
|
||||||
|
|
||||||
export default function patchMithril(global) {
|
export default function patchMithril(global) {
|
||||||
const defaultMithril = global.m;
|
const defaultMithril = global.m;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the href URL of the link is the same as the current page path
|
||||||
|
* we will not add a new entry to the browser history.
|
||||||
|
*
|
||||||
|
* This allows us to still refresh the Page component
|
||||||
|
* without adding endless history entries.
|
||||||
|
*
|
||||||
|
* We also add the `force` attribute that adds a custom state key
|
||||||
|
* for when you want to force a complete refresh of the Page
|
||||||
|
*/
|
||||||
|
const defaultLinkView = defaultMithril.route.Link.view;
|
||||||
|
const modifiedLink = {
|
||||||
|
view: function (vnode) {
|
||||||
|
let { href, options = {} } = vnode.attrs;
|
||||||
|
|
||||||
|
if (href === m.route.get()) {
|
||||||
|
if (!('replace' in options)) options.replace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extract(vnode.attrs, 'force')) {
|
||||||
|
if (!('state' in options)) options.state = {};
|
||||||
|
if (!('key' in options.state)) options.state.key = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
vnode.attrs.options = options;
|
||||||
|
|
||||||
|
return defaultLinkView(vnode);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const modifiedMithril = function (comp, ...args) {
|
const modifiedMithril = function (comp, ...args) {
|
||||||
const node = defaultMithril.apply(this, arguments);
|
const node = defaultMithril.apply(this, arguments);
|
||||||
|
|
||||||
@@ -10,7 +41,22 @@ export default function patchMithril(global) {
|
|||||||
|
|
||||||
// Allows the use of the bidi attr.
|
// Allows the use of the bidi attr.
|
||||||
if (node.attrs.bidi) {
|
if (node.attrs.bidi) {
|
||||||
bidi(node, node.attrs.bidi);
|
modifiedMithril.bidi(node, node.attrs.bidi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows us to use a "route" attr on links, which will automatically convert the link to one which
|
||||||
|
// supports linking to other pages in the SPA without refreshing the document.
|
||||||
|
if (node.attrs.route) {
|
||||||
|
node.attrs.href = node.attrs.route;
|
||||||
|
node.tag = modifiedLink;
|
||||||
|
|
||||||
|
// For some reason, m.route.Link does not like vnode.text, so if present, we
|
||||||
|
// need to convert it to text vnodes and store it in children.
|
||||||
|
if (node.text) {
|
||||||
|
node.children = { tag: '#', children: node.text };
|
||||||
|
}
|
||||||
|
|
||||||
|
delete node.attrs.route;
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
@@ -18,5 +64,9 @@ export default function patchMithril(global) {
|
|||||||
|
|
||||||
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
|
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
|
||||||
|
|
||||||
|
modifiedMithril.stream = Stream;
|
||||||
|
|
||||||
|
modifiedMithril.route.Link = modifiedLink;
|
||||||
|
|
||||||
global.m = modifiedMithril;
|
global.m = modifiedMithril;
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
export default (compat: { [key: string]: any }, namespace: string) => {
|
|
||||||
// regex to replace common/ and NAMESPACE/ for core & core extensions
|
|
||||||
// e.g. admin/utils/extract --> utils/extract
|
|
||||||
// e.g. tags/common/utils/sortTags --> tags/utils/sortTags
|
|
||||||
const regex = new RegExp(`(\\w+\\/)?(${namespace}|common)\\/`);
|
|
||||||
|
|
||||||
return new Proxy(compat, {
|
|
||||||
get: (obj, prop: string) => obj[prop] || obj[prop.replace(regex, '$1')],
|
|
||||||
});
|
|
||||||
};
|
|
@@ -16,7 +16,6 @@ import NotificationListState from './states/NotificationListState';
|
|||||||
import GlobalSearchState from './states/GlobalSearchState';
|
import GlobalSearchState from './states/GlobalSearchState';
|
||||||
import DiscussionListState from './states/DiscussionListState';
|
import DiscussionListState from './states/DiscussionListState';
|
||||||
import ComposerState from './states/ComposerState';
|
import ComposerState from './states/ComposerState';
|
||||||
import isSafariMobile from './utils/isSafariMobile';
|
|
||||||
|
|
||||||
export default class ForumApplication extends Application {
|
export default class ForumApplication extends Application {
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +90,11 @@ export default class ForumApplication extends Application {
|
|||||||
* @type {DiscussionListState}
|
* @type {DiscussionListState}
|
||||||
*/
|
*/
|
||||||
this.discussions = new DiscussionListState({}, this);
|
this.discussions = new DiscussionListState({}, this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated beta 14, remove in beta 15.
|
||||||
|
*/
|
||||||
|
this.cache.discussionList = this.discussions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,19 +115,17 @@ export default class ForumApplication extends Application {
|
|||||||
this.routes[defaultAction].path = '/';
|
this.routes[defaultAction].path = '/';
|
||||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
||||||
|
|
||||||
this.pane = new Pane(document.getElementById('app'));
|
|
||||||
|
|
||||||
m.route.prefix = '';
|
|
||||||
super.mount(this.forum.attribute('basePath'));
|
|
||||||
|
|
||||||
// We mount navigation and header components after the page, so components
|
|
||||||
// like the back button can access the updated state when rendering.
|
|
||||||
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
||||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
m.mount(document.getElementById('header-navigation'), Navigation);
|
||||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
||||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
||||||
m.mount(document.getElementById('composer'), { view: () => Composer.component({ state: this.composer }) });
|
m.mount(document.getElementById('composer'), { view: () => Composer.component({ state: this.composer }) });
|
||||||
|
|
||||||
|
this.pane = new Pane(document.getElementById('app'));
|
||||||
|
|
||||||
|
m.route.prefix = '';
|
||||||
|
super.mount(this.forum.attribute('basePath'));
|
||||||
|
|
||||||
alertEmailConfirmation(this);
|
alertEmailConfirmation(this);
|
||||||
|
|
||||||
// Route the home link back home when clicked. We do not want it to register
|
// Route the home link back home when clicked. We do not want it to register
|
||||||
@@ -139,12 +141,6 @@ export default class ForumApplication extends Application {
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSafariMobile()) {
|
|
||||||
$(() => {
|
|
||||||
$('.App').addClass('mobile-safari');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
import Forum from './ForumApplication';
|
|
||||||
|
|
||||||
const app = new Forum();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
window.app = app;
|
|
||||||
|
|
||||||
export default app;
|
|
@@ -16,7 +16,6 @@ import PostStreamState from './states/PostStreamState';
|
|||||||
import SearchState from './states/SearchState';
|
import SearchState from './states/SearchState';
|
||||||
import AffixedSidebar from './components/AffixedSidebar';
|
import AffixedSidebar from './components/AffixedSidebar';
|
||||||
import DiscussionPage from './components/DiscussionPage';
|
import DiscussionPage from './components/DiscussionPage';
|
||||||
import DiscussionListPane from './components/DiscussionListPane';
|
|
||||||
import LogInModal from './components/LogInModal';
|
import LogInModal from './components/LogInModal';
|
||||||
import ComposerBody from './components/ComposerBody';
|
import ComposerBody from './components/ComposerBody';
|
||||||
import ForgotPasswordModal from './components/ForgotPasswordModal';
|
import ForgotPasswordModal from './components/ForgotPasswordModal';
|
||||||
@@ -36,6 +35,8 @@ import HeaderSecondary from './components/HeaderSecondary';
|
|||||||
import ComposerButton from './components/ComposerButton';
|
import ComposerButton from './components/ComposerButton';
|
||||||
import DiscussionList from './components/DiscussionList';
|
import DiscussionList from './components/DiscussionList';
|
||||||
import ReplyPlaceholder from './components/ReplyPlaceholder';
|
import ReplyPlaceholder from './components/ReplyPlaceholder';
|
||||||
|
import TextEditor from './components/TextEditor';
|
||||||
|
import TextEditorButton from './components/TextEditorButton';
|
||||||
import AvatarEditor from './components/AvatarEditor';
|
import AvatarEditor from './components/AvatarEditor';
|
||||||
import Post from './components/Post';
|
import Post from './components/Post';
|
||||||
import SettingsPage from './components/SettingsPage';
|
import SettingsPage from './components/SettingsPage';
|
||||||
@@ -70,8 +71,6 @@ import Search from './components/Search';
|
|||||||
import DiscussionListItem from './components/DiscussionListItem';
|
import DiscussionListItem from './components/DiscussionListItem';
|
||||||
import LoadingPost from './components/LoadingPost';
|
import LoadingPost from './components/LoadingPost';
|
||||||
import PostsUserPage from './components/PostsUserPage';
|
import PostsUserPage from './components/PostsUserPage';
|
||||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
|
||||||
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
|
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import ForumApplication from './ForumApplication';
|
import ForumApplication from './ForumApplication';
|
||||||
|
|
||||||
@@ -84,7 +83,6 @@ export default Object.assign(compat, {
|
|||||||
'utils/alertEmailConfirmation': alertEmailConfirmation,
|
'utils/alertEmailConfirmation': alertEmailConfirmation,
|
||||||
'utils/UserControls': UserControls,
|
'utils/UserControls': UserControls,
|
||||||
'utils/Pane': Pane,
|
'utils/Pane': Pane,
|
||||||
'utils/BasicEditorDriver': BasicEditorDriver,
|
|
||||||
'states/ComposerState': ComposerState,
|
'states/ComposerState': ComposerState,
|
||||||
'states/DiscussionListState': DiscussionListState,
|
'states/DiscussionListState': DiscussionListState,
|
||||||
'states/GlobalSearchState': GlobalSearchState,
|
'states/GlobalSearchState': GlobalSearchState,
|
||||||
@@ -93,7 +91,6 @@ export default Object.assign(compat, {
|
|||||||
'states/SearchState': SearchState,
|
'states/SearchState': SearchState,
|
||||||
'components/AffixedSidebar': AffixedSidebar,
|
'components/AffixedSidebar': AffixedSidebar,
|
||||||
'components/DiscussionPage': DiscussionPage,
|
'components/DiscussionPage': DiscussionPage,
|
||||||
'components/DiscussionListPane': DiscussionListPane,
|
|
||||||
'components/LogInModal': LogInModal,
|
'components/LogInModal': LogInModal,
|
||||||
'components/ComposerBody': ComposerBody,
|
'components/ComposerBody': ComposerBody,
|
||||||
'components/ForgotPasswordModal': ForgotPasswordModal,
|
'components/ForgotPasswordModal': ForgotPasswordModal,
|
||||||
@@ -113,6 +110,8 @@ export default Object.assign(compat, {
|
|||||||
'components/ComposerButton': ComposerButton,
|
'components/ComposerButton': ComposerButton,
|
||||||
'components/DiscussionList': DiscussionList,
|
'components/DiscussionList': DiscussionList,
|
||||||
'components/ReplyPlaceholder': ReplyPlaceholder,
|
'components/ReplyPlaceholder': ReplyPlaceholder,
|
||||||
|
'components/TextEditor': TextEditor,
|
||||||
|
'components/TextEditorButton': TextEditorButton,
|
||||||
'components/AvatarEditor': AvatarEditor,
|
'components/AvatarEditor': AvatarEditor,
|
||||||
'components/Post': Post,
|
'components/Post': Post,
|
||||||
'components/SettingsPage': SettingsPage,
|
'components/SettingsPage': SettingsPage,
|
||||||
@@ -147,7 +146,6 @@ export default Object.assign(compat, {
|
|||||||
'components/DiscussionListItem': DiscussionListItem,
|
'components/DiscussionListItem': DiscussionListItem,
|
||||||
'components/LoadingPost': LoadingPost,
|
'components/LoadingPost': LoadingPost,
|
||||||
'components/PostsUserPage': PostsUserPage,
|
'components/PostsUserPage': PostsUserPage,
|
||||||
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
|
|
||||||
routes: routes,
|
routes: routes,
|
||||||
ForumApplication: ForumApplication,
|
ForumApplication: ForumApplication,
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import Modal from '../../common/components/Modal';
|
import Modal from '../../common/components/Modal';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ChangeEmailModal` component shows a modal dialog which allows the user
|
* The `ChangeEmailModal` component shows a modal dialog which allows the user
|
||||||
@@ -22,14 +21,14 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
*
|
*
|
||||||
* @type {function}
|
* @type {function}
|
||||||
*/
|
*/
|
||||||
this.email = Stream(app.session.user.email());
|
this.email = m.stream(app.session.user.email());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the password input.
|
* The value of the password input.
|
||||||
*
|
*
|
||||||
* @type {function}
|
* @type {function}
|
||||||
*/
|
*/
|
||||||
this.password = Stream('');
|
this.password = m.stream('');
|
||||||
}
|
}
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@@ -106,8 +105,9 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldEmail = app.session.user.email();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.alertAttrs = null;
|
|
||||||
|
|
||||||
app.session.user
|
app.session.user
|
||||||
.save(
|
.save(
|
||||||
@@ -117,9 +117,7 @@ export default class ChangeEmailModal extends Modal {
|
|||||||
meta: { password: this.password() },
|
meta: { password: this.password() },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => (this.success = true))
|
||||||
this.success = true;
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(this.loaded.bind(this));
|
.then(this.loaded.bind(this));
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,9 @@ export default class CommentPost extends Post {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshContent() {
|
onupdate(vnode) {
|
||||||
|
super.onupdate();
|
||||||
|
|
||||||
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
|
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
|
||||||
|
|
||||||
// If the post content has changed since the last render, we'll run through
|
// If the post content has changed since the last render, we'll run through
|
||||||
@@ -64,28 +66,13 @@ export default class CommentPost extends Post {
|
|||||||
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||||
if (this.contentHtml !== contentHtml) {
|
if (this.contentHtml !== contentHtml) {
|
||||||
this.$('.Post-body script').each(function () {
|
this.$('.Post-body script').each(function () {
|
||||||
const script = document.createElement('script');
|
eval.call(window, $(this).text());
|
||||||
script.textContent = this.textContent;
|
|
||||||
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
|
|
||||||
this.parentNode.replaceChild(script, this);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contentHtml = contentHtml;
|
this.contentHtml = contentHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.refreshContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
onupdate(vnode) {
|
|
||||||
super.onupdate(vnode);
|
|
||||||
|
|
||||||
this.refreshContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
isEditing() {
|
isEditing() {
|
||||||
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
|
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
|
||||||
}
|
}
|
||||||
|
@@ -76,13 +76,13 @@ export default class Composer extends Component {
|
|||||||
|
|
||||||
// Whenever any of the inputs inside the composer are have focus, we want to
|
// Whenever any of the inputs inside the composer are have focus, we want to
|
||||||
// add a class to the composer to draw attention to it.
|
// add a class to the composer to draw attention to it.
|
||||||
this.$().on('focus blur', ':input,.TextEditor-editorContainer', (e) => {
|
this.$().on('focus blur', ':input', (e) => {
|
||||||
this.active = e.type === 'focusin';
|
this.active = e.type === 'focusin';
|
||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
// When the escape key is pressed on any inputs, close the composer.
|
// When the escape key is pressed on any inputs, close the composer.
|
||||||
this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close());
|
this.$().on('keydown', ':input', 'esc', () => this.close());
|
||||||
|
|
||||||
this.handlers = {};
|
this.handlers = {};
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export default class Composer extends Component {
|
|||||||
* Draw focus to the first focusable content element (the text editor).
|
* Draw focus to the first focusable content element (the text editor).
|
||||||
*/
|
*/
|
||||||
focus() {
|
focus() {
|
||||||
this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus();
|
this.$('.Composer-content :input:enabled:visible:first').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,7 +199,7 @@ export default class Composer extends Component {
|
|||||||
*/
|
*/
|
||||||
animatePositionChange() {
|
animatePositionChange() {
|
||||||
// When exiting full-screen mode: focus content
|
// When exiting full-screen mode: focus content
|
||||||
if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {
|
if (this.prevPosition === ComposerState.Position.FULLSCREEN) {
|
||||||
this.focus();
|
this.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -265,17 +265,7 @@ export default class Composer extends Component {
|
|||||||
this.animateHeightChange().then(() => this.focus());
|
this.animateHeightChange().then(() => this.focus());
|
||||||
|
|
||||||
if (app.screen() === 'phone') {
|
if (app.screen() === 'phone') {
|
||||||
// On safari fixed position doesn't properly work on mobile,
|
this.$().css('top', $(window).scrollTop());
|
||||||
// So we use absolute and set the top value.
|
|
||||||
// https://github.com/flarum/core/issues/2652
|
|
||||||
|
|
||||||
// Due to another safari bug, `scrollTop` is unreliable when
|
|
||||||
// at the very bottom of the page AND opening the composer.
|
|
||||||
// So we fallback to a calculated version of scrollTop.
|
|
||||||
// https://github.com/flarum/core/issues/2683
|
|
||||||
const scrollElement = document.documentElement;
|
|
||||||
const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight);
|
|
||||||
this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0);
|
|
||||||
this.showBackdrop();
|
this.showBackdrop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
|
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
|
||||||
import TextEditor from '../../common/components/TextEditor';
|
import TextEditor from './TextEditor';
|
||||||
import avatar from '../../common/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
@@ -44,6 +44,12 @@ export default class ComposerBody extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.composer.fields.content(this.attrs.originalContent || '');
|
this.composer.fields.content(this.attrs.originalContent || '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated BC layer, remove in Beta 15.
|
||||||
|
*/
|
||||||
|
this.content = this.composer.fields.content;
|
||||||
|
this.editor = this.composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import ComposerBody from './ComposerBody';
|
import ComposerBody from './ComposerBody';
|
||||||
import extractText from '../../common/utils/extractText';
|
import extractText from '../../common/utils/extractText';
|
||||||
import Stream from '../../common/utils/Stream';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `DiscussionComposer` component displays the composer content for starting
|
* The `DiscussionComposer` component displays the composer content for starting
|
||||||
@@ -27,7 +26,7 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
this.composer.fields.title = this.composer.fields.title || Stream('');
|
this.composer.fields.title = this.composer.fields.title || m.stream('');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the title input.
|
* The value of the title input.
|
||||||
@@ -100,7 +99,7 @@ export default class DiscussionComposer extends ComposerBody {
|
|||||||
.save(data)
|
.save(data)
|
||||||
.then((discussion) => {
|
.then((discussion) => {
|
||||||
this.composer.hide();
|
this.composer.hide();
|
||||||
app.discussions.refresh({ deferClear: true });
|
app.discussions.refresh();
|
||||||
m.route.set(app.route.discussion(discussion));
|
m.route.set(app.route.discussion(discussion));
|
||||||
}, this.loaded.bind(this));
|
}, this.loaded.bind(this));
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user