Merge v3.3.0.beta5 into stable
This commit is contained in:
commit
4922ad795d
|
@ -52,13 +52,22 @@ updates:
|
|||
- "railties"
|
||||
- "sprockets-rails"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/app/assets/javascripts/"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "08:00"
|
||||
timezone: Australia/Sydney
|
||||
open-pull-requests-limit: 20
|
||||
versioning-strategy: increase
|
||||
ignore: # These are all vendored so need to be updated manually. See lib/tasks/javascript.rake
|
||||
- dependency-name: "chart.js"
|
||||
- dependency-name: "chartjs-plugin-datalabels"
|
||||
- dependency-name: "magnific-popup"
|
||||
- dependency-name: "pikaday"
|
||||
- dependency-name: "moment"
|
||||
- dependency-name: "moment-timezone"
|
||||
- dependency-name: "@discourse/moment-timezon-names-translations"
|
||||
- dependency-name: "squoosh"
|
||||
groups:
|
||||
babel:
|
||||
patterns:
|
||||
|
@ -69,3 +78,11 @@ updates:
|
|||
types:
|
||||
patterns:
|
||||
- "@types/*"
|
||||
# - package-ecosystem: "bundler"
|
||||
# directory: "migrations/config/gemfiles/convert"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# day: "wednesday"
|
||||
# time: "10:00"
|
||||
# timezone: "Europe/Vienna"
|
||||
# versioning-strategy: "increase"
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
# This workflow is designed to stop us accidently committing the lockfile/packagejson symlink changes
|
||||
# which would cause the default Ember version to change.
|
||||
|
||||
name: Ember Version Enforcement
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
- name: Yarn install
|
||||
run: |
|
||||
cd app/assets/javascripts/discourse
|
||||
yarn install
|
||||
- name: "Check default ember version"
|
||||
working-directory: app/assets/javascripts/discourse
|
||||
run: |
|
||||
node -e '
|
||||
const version = require("ember-source/package.json").version;
|
||||
console.log("Ember version is", version);
|
||||
if(version.split(".")[0] !== "5"){
|
||||
console.log(`Ember has unexpectedly been downgraded to version ${version}. If this is intentional, remove this github workflow.`);
|
||||
process.exit(1);
|
||||
}
|
||||
'
|
||||
- name: Downgrade ember
|
||||
run: |
|
||||
script/switch_ember_version 3
|
||||
cd app/assets/javascripts/discourse
|
||||
yarn install
|
||||
- name: "Check upgraded version"
|
||||
working-directory: app/assets/javascripts/discourse
|
||||
run: |
|
||||
node -e '
|
||||
const version = require("ember-source/package.json").version;
|
||||
console.log("Ember version is", version);
|
||||
if(version.split(".")[0] !== "3"){
|
||||
console.log(`Expected Ember 3, but found ${version}`);
|
||||
process.exit(1);
|
||||
}
|
||||
'
|
||||
- name: "Revert ember downgrade"
|
||||
run: |
|
||||
script/switch_ember_version 5
|
||||
- name: "Ensure no diff"
|
||||
run: |
|
||||
if [ ! -z "$(git status --porcelain)" ]; then
|
||||
echo "Working directory was not clean after upgrading/downgrading ember. Perhaps a lockfile is out-of-date. Run this command to re-sync:"
|
||||
echo " script/regen_ember_3_lockfile"
|
||||
echo
|
||||
echo "Current diff:"
|
||||
echo "---------------------------------------------"
|
||||
git -c color.ui=always diff
|
||||
exit 1
|
||||
fi
|
|
@ -1,38 +0,0 @@
|
|||
# This workflow will run on dependabot pull requests to ensure that they update both the ember3 and ember5 lockfiles
|
||||
|
||||
name: Ember Version Lockfiles
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
help_dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: Move lockfile to correct location
|
||||
run: |
|
||||
# Dependabot gets confused by the symlinks and dumps the updated lockfile in the root of the repo.
|
||||
# Let's move it back to the correct location.
|
||||
if [[ -f yarn-ember5.lock ]]; then
|
||||
mv yarn-ember5.lock app/assets/javascripts/yarn-ember5.lock
|
||||
fi
|
||||
- name: Update ember3 lockfile
|
||||
run: script/regen_ember_3_lockfile
|
||||
- name: Push changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ! -z "$(git status --porcelain)" ]; then
|
||||
git config --global user.email "build@discourse.org"
|
||||
git config --global user.name "discoursebuild"
|
||||
git add .
|
||||
git commit -m "Update lockfiles for ember version flag"
|
||||
git push
|
||||
fi
|
|
@ -77,9 +77,3 @@ jobs:
|
|||
yarn global add licensee
|
||||
yarn global upgrade licensee
|
||||
licensee --errors-only
|
||||
|
||||
- name: Check Ember CLI Workspace Licenses
|
||||
if: ${{ !cancelled() }}
|
||||
working-directory: ./app/assets/javascripts
|
||||
run: |
|
||||
licensee --errors-only
|
||||
|
|
|
@ -3,12 +3,14 @@ name: Migration Tests
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/migration-tests.yml"
|
||||
- "migrations/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- stable
|
||||
paths:
|
||||
- ".github/workflows/migration-tests.yml"
|
||||
- "migrations/**"
|
||||
|
||||
concurrency:
|
||||
|
@ -20,7 +22,8 @@ permissions:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
name: Ruby ${{ matrix.ruby }}
|
||||
if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror'
|
||||
name: Tests with Ruby ${{ matrix.ruby }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
container: discourse/discourse_test:slim
|
||||
timeout-minutes: 20
|
||||
|
@ -59,12 +62,23 @@ jobs:
|
|||
sudo -E -u postgres script/start_test_db.rb
|
||||
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
|
||||
|
||||
- name: Container envs
|
||||
id: container-envs
|
||||
run: |
|
||||
echo "ruby_version=$RUBY_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "debian_release=$DEBIAN_RELEASE" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Bundler cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.ruby }}-gem-
|
||||
key: >-
|
||||
${{ runner.os }}-
|
||||
${{ steps.container-envs.outputs.ruby_version }}-
|
||||
${{ steps.container-envs.outputs.debian_release }}-
|
||||
${{ hashFiles('**/Gemfile.lock') }}-
|
||||
${{ hashFiles('migrations/config/gemfiles/**/Gemfile') }}
|
||||
|
||||
- name: Setup gems
|
||||
run: |
|
||||
|
@ -72,8 +86,8 @@ jobs:
|
|||
bundle config --local path vendor/bundle
|
||||
bundle config --local deployment true
|
||||
bundle config --local without development
|
||||
bundle install --jobs 4
|
||||
bundle clean
|
||||
bundle install --jobs $(($(nproc) - 1))
|
||||
# don't call `bundle clean` clean, we need the gems for the migrations
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
|
@ -85,7 +99,6 @@ jobs:
|
|||
with:
|
||||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
@ -99,10 +112,11 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
${{ hashFiles('.github/workflows/tests.yml') }}-
|
||||
${{ hashFiles('db/**/*', 'plugins/**/db/**/*') }}-
|
||||
${{ hashFiles('config/environments/test.rb') }}
|
||||
|
||||
- name: Restore database from cache
|
||||
if: steps.app-cache.outputs.cache-hit == 'true'
|
||||
run: psql --quiet -o /dev/null -f tmp/app-cache/cache.sql postgres
|
||||
run: script/silence_successful_output psql --quiet -o /dev/null -f tmp/app-cache/cache.sql postgres
|
||||
|
||||
- name: Restore uploads from cache
|
||||
if: steps.app-cache.outputs.cache-hit == 'true'
|
||||
|
@ -112,7 +126,7 @@ jobs:
|
|||
if: steps.app-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
bin/rake db:create
|
||||
bin/rake db:migrate
|
||||
script/silence_successful_output bin/rake db:migrate
|
||||
|
||||
- name: Dump database for cache
|
||||
if: steps.app-cache.outputs.cache-hit != 'true'
|
||||
|
@ -122,11 +136,44 @@ jobs:
|
|||
if: steps.app-cache.outputs.cache-hit != 'true'
|
||||
run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads
|
||||
|
||||
- name: Check core database drift
|
||||
run: |
|
||||
mkdir /tmp/intermediate_db
|
||||
./migrations/scripts/schema_generator /tmp/intermediate_db/base_migration.sql
|
||||
diff -u migrations/common/intermediate_db_schema/000_base_schema.sql /tmp/intermediate_db/base_migration.sql
|
||||
# - name: Check core database drift
|
||||
# run: |
|
||||
# mkdir /tmp/intermediate_db
|
||||
# ./migrations/scripts/schema_generator /tmp/intermediate_db/base_migration.sql
|
||||
# diff -u migrations/common/intermediate_db_schema/000_base_schema.sql /tmp/intermediate_db/base_migration.sql
|
||||
|
||||
- name: RSpec
|
||||
run: bin/rspec migrations/spec/
|
||||
run: bin/rspec --default-path migrations/spec
|
||||
|
||||
runtime:
|
||||
if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror'
|
||||
name: Runs on ${{ matrix.os }}, Ruby ${{ matrix.ruby }}
|
||||
timeout-minutes: 20
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macos-latest"]
|
||||
ruby: ["3.2", "3.3"]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Modify path for libpq
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: echo "/opt/homebrew/opt/libpq/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby }}
|
||||
bundler-cache: true
|
||||
|
||||
- name: Run converter
|
||||
working-directory: migrations
|
||||
run: bin/convert version
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
name: Release Notes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
from:
|
||||
description: 'Starting ref (exclusive). Can be a tag, branch or commit ref. `latest-release` refers to the last beta version bump.'
|
||||
required: true
|
||||
default: 'latest-release'
|
||||
type: string
|
||||
to:
|
||||
description: 'Ending ref (inclusive). Can be a tag, branch or commit ref. `HEAD` refers to the most recent commit.'
|
||||
required: true
|
||||
default: 'HEAD'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: run
|
||||
runs-on: ubuntu-latest
|
||||
container: discourse/discourse_test:slim
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
from_ref: ${{ inputs.from || 'latest-release' }}
|
||||
to_ref: ${{ inputs.to || 'HEAD' }}
|
||||
|
||||
steps:
|
||||
- name: Set working directory owner
|
||||
run: chown root:root .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Bundler cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: ${{ runner.os }}-gem-
|
||||
|
||||
- name: Setup gems
|
||||
run: |
|
||||
bundle config --local path vendor/bundle
|
||||
bundle config --local deployment true
|
||||
bundle config --local without development
|
||||
bundle install --jobs 4
|
||||
bundle clean
|
||||
|
||||
- name: Create output dir
|
||||
run: mkdir -p tmp/notes
|
||||
|
||||
- name: Core release notes
|
||||
run: bin/rake "release_note:generate[ $from_ref , $to_ref ]" | tee tmp/notes/core.txt
|
||||
|
||||
- name: Calculate from/to dates from refs
|
||||
id: dates
|
||||
run: |
|
||||
from=$(git show -s --date=format:'%Y-%m-%d' --format=%cd "${from_ref}^{commit}")
|
||||
echo "from=$from" >> $GITHUB_OUTPUT
|
||||
to=$(git show -s --date=format:'%Y-%m-%d' --format=%cd "${to_ref}^{commit}")
|
||||
echo "to=$to" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup all-the-plugins
|
||||
run: |
|
||||
git clone https://github.com/discourse/all-the-plugins tmp/all-the-plugins
|
||||
cd tmp/all-the-plugins
|
||||
./reset-all-repos
|
||||
|
||||
- name: Plugin release notes
|
||||
run: |
|
||||
bin/rake "release_note:plugins:generate[ ${{ steps.dates.outputs.from }} , ${{ steps.dates.outputs.to }} , ./tmp/all-the-plugins/official/* , discourse ]" | tee tmp/notes/plugins.txt
|
||||
|
||||
- name: Export files
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-notes
|
||||
path: ./tmp/notes/*.txt
|
||||
|
||||
- name: Write summary
|
||||
run: |
|
||||
echo "### Release notes" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "From: $from_ref - $(git rev-parse --short $from_ref) - ${{ steps.dates.outputs.from }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "To: $to_ref - $(git rev-parse --short $to_ref) - ${{ steps.dates.outputs.to }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "Core:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '~~~' >> $GITHUB_STEP_SUMMARY
|
||||
cat tmp/notes/core.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '~~~' >> $GITHUB_STEP_SUMMARY
|
||||
echo ""
|
||||
echo "Plugins:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '~~~' >> $GITHUB_STEP_SUMMARY
|
||||
cat tmp/notes/plugins.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '~~~' >> $GITHUB_STEP_SUMMARY
|
|
@ -3,6 +3,9 @@ name: Tests
|
|||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/migration-tests.yml"
|
||||
- ".github/dependabot.yml"
|
||||
- ".github/labeler.yml"
|
||||
- "migrations/**"
|
||||
push:
|
||||
branches:
|
||||
|
@ -10,6 +13,9 @@ on:
|
|||
- beta
|
||||
- stable
|
||||
paths-ignore:
|
||||
- ".github/workflows/migration-tests.yml"
|
||||
- ".github/dependabot.yml"
|
||||
- ".github/labeler.yml"
|
||||
- "migrations/**"
|
||||
|
||||
concurrency:
|
||||
|
@ -22,9 +28,9 @@ permissions:
|
|||
jobs:
|
||||
build:
|
||||
if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror'
|
||||
name: ${{ matrix.target }} ${{ matrix.build_type }}${{ !matrix.updated_ember && ' (Ember 3)' || '' }} # Update fetch-job-id step if changing this
|
||||
name: ${{ matrix.target }} ${{ matrix.build_type }} # Update fetch-job-id step if changing this
|
||||
runs-on: ${{ (matrix.build_type == 'annotations') && 'ubuntu-latest' || 'ubuntu-20.04-8core' }}
|
||||
container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}${{ (matrix.ruby == '3.1') && '-ruby-3.1.0' || '' }}
|
||||
container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}
|
||||
timeout-minutes: 20
|
||||
|
||||
env:
|
||||
|
@ -35,8 +41,8 @@ jobs:
|
|||
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' || matrix.build_type == 'system' }}
|
||||
CAPYBARA_DEFAULT_MAX_WAIT_TIME: 10
|
||||
MINIO_RUNNER_LOG_LEVEL: DEBUG
|
||||
EMBER_VERSION: ${{ (matrix.updated_ember && '5') || '3' }}
|
||||
DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS: ${{ (matrix.build_type == 'system' || matrix.build_type == 'backend') && github.ref_name == 'main' }}
|
||||
DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS: ${{ (matrix.build_type == 'system' || matrix.build_type == 'backend') && github.ref == 'refs/main/head' }}
|
||||
CHEAP_SOURCE_MAPS: "1"
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -44,8 +50,6 @@ jobs:
|
|||
matrix:
|
||||
build_type: [backend, frontend, system, annotations]
|
||||
target: [core, plugins, themes]
|
||||
ruby: ["3.2"]
|
||||
updated_ember: [true]
|
||||
exclude:
|
||||
- build_type: annotations
|
||||
target: plugins
|
||||
|
@ -58,15 +62,15 @@ jobs:
|
|||
include:
|
||||
- build_type: system
|
||||
target: chat
|
||||
updated_ember: true
|
||||
|
||||
steps:
|
||||
- name: Set working directory owner
|
||||
run: chown root:root .
|
||||
|
||||
- name: Remove Chrome
|
||||
- name: Remove Chromium
|
||||
continue-on-error: true
|
||||
run: apt remove -y google-chrome-stable
|
||||
if: matrix.build_type == 'system'
|
||||
run: apt remove -y chromium-driver chromium
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
@ -87,11 +91,18 @@ jobs:
|
|||
sudo -E -u postgres script/start_test_db.rb
|
||||
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
|
||||
|
||||
- name: Container envs
|
||||
id: container-envs
|
||||
run: |
|
||||
echo "ruby_version=$RUBY_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "debian_release=$DEBIAN_RELEASE" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Bundler cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }}-cachev2
|
||||
key: ${{ runner.os }}-${{ steps.container-envs.outputs.ruby_version }}-${{ steps.container-envs.outputs.debian_release }}-gem-${{ hashFiles('**/Gemfile.lock') }}-cachev2
|
||||
|
||||
- name: Setup gems
|
||||
run: |
|
||||
|
@ -99,7 +110,7 @@ jobs:
|
|||
bundle config --local path vendor/bundle
|
||||
bundle config --local deployment true
|
||||
bundle config --local without development
|
||||
bundle install --jobs 4
|
||||
bundle install --jobs $(($(nproc) - 1))
|
||||
bundle clean
|
||||
|
||||
- name: Get yarn cache directory
|
||||
|
@ -133,7 +144,7 @@ jobs:
|
|||
uses: actions/cache@v4
|
||||
with:
|
||||
path: plugins/*/gems
|
||||
key: ${{ runner.os }}-plugin-gems-${{matrix.ruby}}-${{ hashFiles('plugins/*/plugin.rb') }}
|
||||
key: ${{ runner.os }}-plugin-gems-${{ steps.container-envs.outputs.ruby_version }}-${{ steps.container-envs.outputs.debian_release }}-${{ hashFiles('plugins/*/plugin.rb') }}
|
||||
|
||||
- name: Checkout official themes
|
||||
if: matrix.target == 'themes'
|
||||
|
@ -226,8 +237,8 @@ jobs:
|
|||
|
||||
- name: Theme QUnit
|
||||
if: matrix.build_type == 'frontend' && matrix.target == 'themes'
|
||||
run: DISCOURSE_DEV_DB=discourse_test QUNIT_PARALLEL=3 bin/rake themes:qunit_all_official
|
||||
timeout-minutes: 15
|
||||
run: DISCOURSE_DEV_DB=discourse_test bin/rake themes:qunit_all_official
|
||||
timeout-minutes: 10
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always() && matrix.build_type == 'frontend' && matrix.target == 'plugins'
|
||||
|
@ -245,23 +256,31 @@ jobs:
|
|||
|
||||
- name: Core System Tests
|
||||
if: matrix.build_type == 'system' && matrix.target == 'core'
|
||||
env:
|
||||
CHECKOUT_TIMEOUT: 10
|
||||
run: RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error PARALLEL_TEST_PROCESSORS=4 bin/turbo_rspec --use-runtime-info --profile=50 --verbose --format documentation spec/system
|
||||
|
||||
- name: Plugin System Tests
|
||||
if: matrix.build_type == 'system' && matrix.target == 'plugins'
|
||||
env:
|
||||
CHECKOUT_TIMEOUT: 10
|
||||
run: |
|
||||
GLOBIGNORE="plugins/chat/*";
|
||||
PREFABRICATION=0 LOAD_PLUGINS=1 RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error PARALLEL_TEST_PROCESSORS=4 bin/turbo_rspec --use-runtime-info --profile=50 --verbose --format documentation plugins/*/spec/system
|
||||
LOAD_PLUGINS=1 RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error PARALLEL_TEST_PROCESSORS=4 bin/turbo_rspec --use-runtime-info --profile=50 --verbose --format documentation plugins/*/spec/system
|
||||
shell: bash
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Chat System Tests
|
||||
if: matrix.build_type == 'system' && matrix.target == 'chat'
|
||||
env:
|
||||
CHECKOUT_TIMEOUT: 10
|
||||
run: LOAD_PLUGINS=1 RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error PARALLEL_TEST_PROCESSORS=4 bin/turbo_rspec --use-runtime-info --profile=50 --verbose --format documentation plugins/chat/spec/system
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Theme System Tests
|
||||
if: matrix.build_type == 'system' && matrix.target == 'themes'
|
||||
env:
|
||||
CHECKOUT_TIMEOUT: 10
|
||||
run: |
|
||||
RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error PARALLEL_TEST_PROCESSORS=4 bin/turbo_rspec --profile=50 --verbose --format documentation tmp/themes/*/spec/system
|
||||
shell: bash
|
||||
|
@ -300,7 +319,7 @@ jobs:
|
|||
id: fetch-job-id
|
||||
if: always() && steps.check-flaky-spec-report.outputs.exists == 'true'
|
||||
run: |
|
||||
job_id=$(ruby script/get_github_workflow_run_job_id.rb ${{ github.run_id }} ${{ github.run_attempt }} '${{ matrix.target }} ${{ matrix.build_type }}${{ !matrix.updated_ember && ' (Ember 3)' || '' }}')
|
||||
job_id=$(ruby script/get_github_workflow_run_job_id.rb ${{ github.run_id }} ${{ github.run_attempt }} '${{ matrix.target }} ${{ matrix.build_type }}')
|
||||
echo "job_id=$job_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create flaky tests report artifact
|
||||
|
@ -333,7 +352,7 @@ jobs:
|
|||
|
||||
core_frontend_tests:
|
||||
if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror'
|
||||
name: core frontend (${{ matrix.browser }})${{ !matrix.updated_ember && ' (Ember 3)' || '' }}
|
||||
name: core frontend (${{ matrix.browser }})
|
||||
runs-on: ubuntu-20.04-8core
|
||||
container:
|
||||
image: discourse/discourse_test:slim-browsers
|
||||
|
@ -345,14 +364,11 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["Chromium", "Firefox ESR", "Firefox Evergreen"]
|
||||
updated_ember: [true]
|
||||
include:
|
||||
- browser: Chromium
|
||||
updated_ember: false
|
||||
|
||||
env:
|
||||
TESTEM_BROWSER: ${{ (startsWith(matrix.browser, 'Firefox') && 'Firefox') || matrix.browser }}
|
||||
TESTEM_FIREFOX_PATH: ${{ (matrix.browser == 'Firefox Evergreen') && '/opt/firefox-evergreen/firefox' }}
|
||||
CHEAP_SOURCE_MAPS: "1"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -375,12 +391,7 @@ jobs:
|
|||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-cachev2
|
||||
|
||||
- name: Downgrade Ember
|
||||
if: matrix.updated_ember == false
|
||||
run: script/switch_ember_version 3
|
||||
|
||||
- name: Yarn install
|
||||
working-directory: ./app/assets/javascripts/discourse
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Ember Build
|
||||
|
|
|
@ -35,18 +35,20 @@
|
|||
|
||||
# Plugins except for the bundled ones
|
||||
/plugins/*
|
||||
!/plugins/discourse-details/
|
||||
!/plugins/discourse-details
|
||||
!/plugins/discourse-local-dates
|
||||
!/plugins/discourse-narrative-bot
|
||||
!/plugins/discourse-presence
|
||||
!/plugins/discourse-lazy-videos/
|
||||
!/plugins/chat/
|
||||
!/plugins/poll/
|
||||
!/plugins/discourse-lazy-videos
|
||||
!/plugins/automation
|
||||
/plugins/automation/gems
|
||||
!/plugins/chat
|
||||
!/plugins/poll
|
||||
!/plugins/styleguide
|
||||
!/plugins/spoiler-alert/
|
||||
!/plugins/checklist/
|
||||
!/plugins/footnote/
|
||||
/plugins/*/auto_generated/
|
||||
!/plugins/spoiler-alert
|
||||
!/plugins/checklist
|
||||
!/plugins/footnote
|
||||
/plugins/*/auto_generated
|
||||
|
||||
/spec/fixtures/plugins/my_plugin/auto_generated
|
||||
|
||||
|
@ -69,7 +71,7 @@ yarn-error.log
|
|||
|
||||
# Linting artifacts
|
||||
.eslintcache
|
||||
/lint-progress/
|
||||
/lint-progress
|
||||
|
||||
# Auto-generated plugin JS assets
|
||||
/app/assets/javascripts/plugins/*
|
||||
|
|
|
@ -20,17 +20,22 @@ ignored:
|
|||
- date # Ruby (default gem)
|
||||
- digest # Ruby (default gem)
|
||||
- io-wait # Ruby (default gem)
|
||||
- irb # Ruby (default gem)
|
||||
- json # Ruby (default gem)
|
||||
- logger # Ruby (default gem)
|
||||
- mutex_m # BSD-2-Clause
|
||||
- net-http # Ruby (default gem)
|
||||
- net-protocol # Ruby (default gem)
|
||||
- openssl # Ruby (default gem)
|
||||
- racc # Ruby (default gem)
|
||||
- rchardet # LGPL
|
||||
- rdoc # Ruby (default gem)
|
||||
- reline # Ruby (default gem)
|
||||
- ruby2_keywords # Ruby (default gem)
|
||||
- stringio # Ruby (default gem)
|
||||
- strscan # Ruby (default gem)
|
||||
- timeout # Ruby (default gem)
|
||||
- uri # Ruby (default gem)
|
||||
- mutex_m # BSD-2-Clause
|
||||
|
||||
reviewed:
|
||||
bundler:
|
||||
|
@ -38,6 +43,7 @@ reviewed:
|
|||
- coderay # MIT
|
||||
- concurrent-ruby # MIT
|
||||
- css_parser # MIT
|
||||
- drb # BSD-2-Clause
|
||||
- excon # MIT
|
||||
- faraday-em_http # MIT
|
||||
- faraday-em_synchrony # MIT
|
||||
|
@ -65,4 +71,3 @@ reviewed:
|
|||
- tilt # MIT
|
||||
- unf # BSD-2-Clause
|
||||
- unicorn # Ruby or GPLv2/GPLv3
|
||||
- drb # BSD-2-Clause
|
||||
|
|
|
@ -5,17 +5,47 @@
|
|||
"CC0-1.0",
|
||||
"CC-BY-3.0",
|
||||
"CC-BY-4.0",
|
||||
"Apache-2.0 WITH LLVM-exception"
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"ISC"
|
||||
]
|
||||
},
|
||||
"packages": {
|
||||
"@fortawesome/fontawesome-free": "*",
|
||||
"@glimmer/compiler": "*",
|
||||
"@glimmer/debug": "*",
|
||||
"@glimmer/encoder": "*",
|
||||
"@glimmer/global-context": "*",
|
||||
"@glimmer/interfaces": "*",
|
||||
"@glimmer/manager": "*",
|
||||
"@glimmer/node": "*",
|
||||
"@glimmer/opcode-compiler": "*",
|
||||
"@glimmer/program": "*",
|
||||
"@glimmer/syntax": "*",
|
||||
"@glimmer/vm": "*",
|
||||
"@jspreadsheet/formula": "2.0.2",
|
||||
"cli-table": "0.3.11",
|
||||
"component-bind": "1.0.0",
|
||||
"component-inherit": "0.0.3",
|
||||
"duplex": "1.0.0",
|
||||
"ember-template-lint-plugin-discourse": "*",
|
||||
"glob": "3.1.21",
|
||||
"indexof": "0.0.1",
|
||||
"inherits": "1.0.2",
|
||||
"jsonify": "0.0.1",
|
||||
"jspreadsheet-ce": "4.13.4",
|
||||
"line-stream": "0.0.0",
|
||||
"messageformat": "0.1.5",
|
||||
"regenerator-transform": "0.10.1",
|
||||
"source-map": "0.1.43",
|
||||
"sourcemap-validator": "1.1.1",
|
||||
"spawn-command": "0.0.2",
|
||||
"squoosh": "2.0.0",
|
||||
"taffydb": "2.6.2"
|
||||
},
|
||||
"corrections": true
|
||||
"corrections": true,
|
||||
"ignore": [
|
||||
{
|
||||
"author": "Discourse"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
24
.rubocop.yml
24
.rubocop.yml
|
@ -8,6 +8,30 @@ Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
|
|||
Discourse/NoResetColumnInformationInMigrations:
|
||||
Enabled: true
|
||||
|
||||
Discourse/Plugins/CallRequiresPlugin:
|
||||
Include:
|
||||
- "plugins/*/app/controllers/**/*"
|
||||
|
||||
Discourse/Plugins/UsePluginInstanceOn:
|
||||
Include:
|
||||
- "plugins/**/*"
|
||||
|
||||
Discourse/Plugins/NamespaceMethods:
|
||||
Include:
|
||||
- "plugins/**/*"
|
||||
|
||||
Discourse/Plugins/NamespaceConstants:
|
||||
Include:
|
||||
- "plugins/**/*"
|
||||
|
||||
Discourse/Plugins/UseRequireRelative:
|
||||
Include:
|
||||
- "plugins/**/*"
|
||||
|
||||
Discourse/Plugins/NoMonkeyPatching:
|
||||
Include:
|
||||
- "plugins/**/*"
|
||||
|
||||
Lint/Debugger:
|
||||
Exclude:
|
||||
- script/**/*
|
||||
|
|
28
Gemfile
28
Gemfile
|
@ -6,20 +6,20 @@ source "https://rubygems.org"
|
|||
|
||||
gem "bootsnap", require: false, platform: :mri
|
||||
|
||||
gem "actionmailer", "< 7.1"
|
||||
gem "actionpack", "< 7.1"
|
||||
gem "actionview", "< 7.1"
|
||||
gem "activemodel", "< 7.1"
|
||||
gem "activerecord", "< 7.1"
|
||||
gem "activesupport", "< 7.1"
|
||||
gem "railties", "< 7.1"
|
||||
gem "actionmailer", "~> 7.1.0"
|
||||
gem "actionpack", "~> 7.1.0"
|
||||
gem "actionview", "~> 7.1.0"
|
||||
gem "activemodel", "~> 7.1.0"
|
||||
gem "activerecord", "~> 7.1.0"
|
||||
gem "activesupport", "~> 7.1.0"
|
||||
gem "railties", "~> 7.1.0"
|
||||
gem "sprockets-rails"
|
||||
|
||||
gem "json"
|
||||
|
||||
# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
|
||||
# We intend to drop sprockets rather than upgrade to 4.x
|
||||
gem "sprockets", git: "https://github.com/rails/sprockets", branch: "3.x"
|
||||
gem "sprockets", "~> 3.7.3"
|
||||
|
||||
# this will eventually be added to rails,
|
||||
# allows us to precompile all our templates in the unicorn master
|
||||
|
@ -31,7 +31,9 @@ gem "mail"
|
|||
gem "mini_mime"
|
||||
gem "mini_suffix"
|
||||
|
||||
gem "redis"
|
||||
# config/initializers/006-mini_profiler.rb depends upon the RedisClient#call.
|
||||
# Rework this when upgrading to redis client 5.0 and above.
|
||||
gem "redis", "< 5.0"
|
||||
|
||||
# This is explicitly used by Sidekiq and is an optional dependency.
|
||||
# We tell Sidekiq to use the namespace "sidekiq" which triggers this
|
||||
|
@ -87,6 +89,7 @@ gem "mini_sql"
|
|||
gem "pry-rails", require: false
|
||||
gem "pry-byebug", require: false
|
||||
gem "rtlcss", require: false
|
||||
gem "messageformat-wrapper", require: false
|
||||
gem "rake"
|
||||
|
||||
gem "thor", require: false
|
||||
|
@ -123,7 +126,6 @@ group :test do
|
|||
gem "capybara", require: false
|
||||
gem "webmock", require: false
|
||||
gem "fakeweb", require: false
|
||||
gem "minitest", require: false
|
||||
gem "simplecov", require: false
|
||||
gem "selenium-webdriver", "~> 4.14", require: false
|
||||
gem "selenium-devtools", require: false
|
||||
|
@ -145,6 +147,7 @@ group :test, :development do
|
|||
|
||||
gem "shoulda-matchers", require: false
|
||||
gem "rspec-html-matchers"
|
||||
gem "pry-stack_explorer", require: false
|
||||
gem "byebug", require: ENV["RM_INFO"].nil?, platform: :mri
|
||||
gem "rubocop-discourse", require: false
|
||||
gem "parallel_tests"
|
||||
|
@ -210,7 +213,6 @@ gem "cppjieba_rb", require: false
|
|||
|
||||
gem "lograge", require: false
|
||||
gem "logstash-event", require: false
|
||||
gem "logstash-logger", require: false
|
||||
gem "logster"
|
||||
|
||||
# A fork of sassc with dart-sass support
|
||||
|
@ -270,3 +272,7 @@ gem "csv", require: false
|
|||
# TODO: Can be removed once we upgrade to Rails 7.1
|
||||
gem "mutex_m"
|
||||
gem "drb"
|
||||
|
||||
# dependencies for the automation plugin
|
||||
gem "iso8601"
|
||||
gem "rrule"
|
||||
|
|
461
Gemfile.lock
461
Gemfile.lock
|
@ -1,92 +1,92 @@
|
|||
GIT
|
||||
remote: https://github.com/rails/sprockets
|
||||
revision: f4d3dae71ef29c44b75a49cfbf8032cce07b423a
|
||||
branch: 3.x
|
||||
specs:
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailer (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actionview (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actionview_precompiler (0.4.0)
|
||||
actionview (>= 6.0.a)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activejob (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activesupport (7.0.8)
|
||||
activemodel (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activerecord (7.1.3.4)
|
||||
activemodel (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
timeout (>= 0.4.0)
|
||||
activesupport (7.1.3.4)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.583.0)
|
||||
aws-sdk-core (3.130.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-partitions (1.894.0)
|
||||
aws-sdk-core (3.191.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.56.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sdk-sns (1.53.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sdk-sns (1.72.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.2.0)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
rouge (>= 1.0.0)
|
||||
bigdecimal (3.1.6)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.17.1)
|
||||
bigdecimal (3.1.8)
|
||||
binding_of_caller (1.0.1)
|
||||
debug_inspector (>= 1.2.0)
|
||||
bootsnap (1.18.3)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.1.6)
|
||||
builder (3.3.0)
|
||||
bullet (7.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
byebug (11.1.3)
|
||||
|
@ -105,22 +105,22 @@ GEM
|
|||
chunky_png (1.4.0)
|
||||
coderay (1.1.3)
|
||||
colored2 (4.0.0)
|
||||
concurrent-ruby (1.2.3)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
cose (1.3.0)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
cppjieba_rb (0.4.2)
|
||||
crack (0.4.6)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.16.0)
|
||||
css_parser (1.17.1)
|
||||
addressable
|
||||
csv (3.3.0)
|
||||
date (3.3.4)
|
||||
debug_inspector (1.2.0)
|
||||
diff-lcs (1.5.0)
|
||||
diff-lcs (1.5.1)
|
||||
diffy (3.4.2)
|
||||
digest (3.1.1)
|
||||
discourse-fonts (0.0.9)
|
||||
|
@ -130,41 +130,54 @@ GEM
|
|||
discourse_dev_assets (0.0.4)
|
||||
faker (~> 2.16)
|
||||
literate_randomizer
|
||||
docile (1.4.0)
|
||||
docile (1.4.1)
|
||||
drb (2.2.1)
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.12.0)
|
||||
excon (0.109.0)
|
||||
erubi (1.13.0)
|
||||
excon (0.111.0)
|
||||
execjs (2.9.1)
|
||||
exifr (1.4.0)
|
||||
fabrication (2.31.0)
|
||||
faker (2.23.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
fakeweb (1.3.0)
|
||||
faraday (2.9.0)
|
||||
faraday (2.10.0)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday-net_http (3.1.0)
|
||||
logger
|
||||
faraday-net_http (3.1.1)
|
||||
net-http
|
||||
faraday-retry (2.2.0)
|
||||
faraday-retry (2.2.1)
|
||||
faraday (~> 2.0)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.3.0)
|
||||
ffi (1.16.3)
|
||||
fastimage (2.3.1)
|
||||
ffi (1.17.0-aarch64-linux-gnu)
|
||||
ffi (1.17.0-arm64-darwin)
|
||||
ffi (1.17.0-x86_64-darwin)
|
||||
ffi (1.17.0-x86_64-linux-gnu)
|
||||
fspath (3.1.2)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (3.25.3-aarch64-linux)
|
||||
google-protobuf (3.25.3-arm64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-linux)
|
||||
google-protobuf (4.27.2-aarch64-linux)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.27.2-arm64-darwin)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.27.2-x86_64-darwin)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.27.2-x86_64-linux)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
guess_html_encoding (0.0.11)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.1.0)
|
||||
hashie (5.0.0)
|
||||
highline (3.0.1)
|
||||
highline (3.1.0)
|
||||
reline
|
||||
htmlentities (4.3.4)
|
||||
http_accept_language (2.1.1)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_optim (0.31.3)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
|
@ -174,34 +187,40 @@ GEM
|
|||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (3.4.0)
|
||||
in_threads (1.6.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
iso8601 (0.13.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
json-schema (4.1.1)
|
||||
json (2.7.2)
|
||||
json-schema (4.3.1)
|
||||
addressable (>= 2.8)
|
||||
json_schemer (2.1.1)
|
||||
json_schemer (2.3.0)
|
||||
bigdecimal
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
simpleidn (~> 0.2)
|
||||
jwt (2.7.1)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
kgio (2.11.4)
|
||||
language_server-protocol (3.17.0.3)
|
||||
libv8-node (18.16.0.0-aarch64-linux)
|
||||
libv8-node (18.16.0.0-arm64-darwin)
|
||||
libv8-node (18.16.0.0-x86_64-darwin)
|
||||
libv8-node (18.16.0.0-x86_64-linux)
|
||||
listen (3.8.0)
|
||||
libv8-node (18.19.0.0-aarch64-linux)
|
||||
libv8-node (18.19.0.0-arm64-darwin)
|
||||
libv8-node (18.19.0.0-x86_64-darwin)
|
||||
libv8-node (18.19.0.0-x86_64-linux)
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
literate_randomizer (0.4.0)
|
||||
logger (1.6.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
logstash-event (1.2.02)
|
||||
logstash-logger (0.26.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (2.16.0)
|
||||
logster (2.20.0)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
|
@ -214,46 +233,49 @@ GEM
|
|||
net-smtp
|
||||
matrix (0.4.2)
|
||||
maxminddb (0.1.22)
|
||||
memory_profiler (1.0.1)
|
||||
memory_profiler (1.0.2)
|
||||
message_bus (4.3.8)
|
||||
rack (>= 1.1.3)
|
||||
method_source (1.0.0)
|
||||
messageformat-wrapper (1.1.0)
|
||||
mini_racer (>= 0.6.3)
|
||||
method_source (1.1.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_racer (0.8.0)
|
||||
libv8-node (~> 18.16.0.0)
|
||||
mini_racer (0.9.0)
|
||||
libv8-node (~> 18.19.0.0)
|
||||
mini_scheduler (0.16.0)
|
||||
sidekiq (>= 4.2.3, < 7.0)
|
||||
mini_sql (1.5.0)
|
||||
mini_suffix (0.3.3)
|
||||
ffi (~> 1.9)
|
||||
minio_runner (0.1.2)
|
||||
minitest (5.21.2)
|
||||
mocha (2.1.0)
|
||||
minitest (5.24.1)
|
||||
mocha (2.4.5)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
mustache (1.1.1)
|
||||
mutex_m (0.2.0)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.9.1)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.0-aarch64-linux)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-arm64-darwin)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-darwin)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
|
@ -267,7 +289,7 @@ GEM
|
|||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 4)
|
||||
oj (3.16.3)
|
||||
oj (3.16.4)
|
||||
bigdecimal (>= 3.0)
|
||||
omniauth (1.9.2)
|
||||
hashie (>= 3.4.6)
|
||||
|
@ -295,10 +317,10 @@ GEM
|
|||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
optimist (3.1.0)
|
||||
parallel (1.24.0)
|
||||
parallel_tests (4.4.0)
|
||||
parallel (1.25.1)
|
||||
parallel_tests (4.7.1)
|
||||
parallel
|
||||
parser (3.3.0.5)
|
||||
parser (3.3.4.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.4)
|
||||
|
@ -310,20 +332,30 @@ GEM
|
|||
pry-byebug (3.10.1)
|
||||
byebug (~> 11.0)
|
||||
pry (>= 0.13, < 0.15)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.4)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
pry-stack_explorer (0.6.1)
|
||||
binding_of_caller (~> 1.0)
|
||||
pry (~> 0.13)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-mini-profiler (3.3.0)
|
||||
racc (1.8.0)
|
||||
rack (2.2.9)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
|
@ -331,70 +363,78 @@ GEM
|
|||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails_failover (2.0.1)
|
||||
activerecord (>= 6.1, <= 7.1)
|
||||
rails_failover (2.1.1)
|
||||
activerecord (>= 6.1, < 8.0)
|
||||
concurrent-ruby
|
||||
railties (>= 6.1, <= 7.1)
|
||||
rails_multisite (5.0.0)
|
||||
railties (>= 6.1, < 8.0)
|
||||
rails_multisite (6.0.0)
|
||||
activerecord (>= 6.0)
|
||||
railties (>= 6.0)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
method_source
|
||||
railties (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
raindrops (0.20.1)
|
||||
rake (13.1.0)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rbtrace (0.5.1)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
optimist (>= 3.0.0)
|
||||
rchardet (1.8.0)
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.0)
|
||||
redis (4.8.1)
|
||||
redis-namespace (1.11.0)
|
||||
redis (>= 4)
|
||||
regexp_parser (2.9.0)
|
||||
request_store (1.5.1)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.9)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
rexml (3.2.6)
|
||||
rexml (3.3.2)
|
||||
strscan
|
||||
rinku (2.0.6)
|
||||
rotp (6.3.0)
|
||||
rouge (4.2.0)
|
||||
rouge (4.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec (3.12.0)
|
||||
rspec-core (~> 3.12.0)
|
||||
rspec-expectations (~> 3.12.0)
|
||||
rspec-mocks (~> 3.12.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
rrule (0.6.0)
|
||||
activesupport (>= 2.3)
|
||||
rspec (3.13.0)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-html-matchers (0.10.0)
|
||||
nokogiri (~> 1)
|
||||
rspec (>= 3.0.0.a)
|
||||
rspec-mocks (3.12.6)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.1.1)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.12)
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.1)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.1)
|
||||
rss (0.3.0)
|
||||
rexml
|
||||
rswag-specs (2.13.0)
|
||||
|
@ -404,58 +444,70 @@ GEM
|
|||
rspec-core (>= 2.14)
|
||||
rtlcss (0.2.1)
|
||||
mini_racer (>= 0.6.3)
|
||||
rubocop (1.60.2)
|
||||
rubocop (1.65.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.30.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.20.0)
|
||||
rubocop-ast (1.31.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.21.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-discourse (3.6.0)
|
||||
rubocop-discourse (3.8.1)
|
||||
activesupport (>= 6.1)
|
||||
rubocop (>= 1.59.0)
|
||||
rubocop-rspec (>= 2.25.0)
|
||||
rubocop-factory_bot (2.25.1)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-rspec (2.26.1)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop-capybara (>= 2.0.0)
|
||||
rubocop-factory_bot (>= 2.0.0)
|
||||
rubocop-rails (>= 2.25.0)
|
||||
rubocop-rspec (>= 3.0.1)
|
||||
rubocop-rspec_rails (>= 2.30.0)
|
||||
rubocop-factory_bot (2.26.1)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rails (2.25.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.0.3)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec_rails (2.30.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec (~> 3, >= 3.0.1)
|
||||
ruby-prof (1.7.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-readability (0.7.0)
|
||||
ruby-readability (0.7.1)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
sanitize (6.1.0)
|
||||
sanitize (6.1.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
sass-embedded (1.70.0-aarch64-linux-gnu)
|
||||
google-protobuf (~> 3.25)
|
||||
sass-embedded (1.70.0-arm64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
sass-embedded (1.70.0-x86_64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
sass-embedded (1.70.0-x86_64-linux-gnu)
|
||||
google-protobuf (~> 3.25)
|
||||
sassc-embedded (1.70.0)
|
||||
sass-embedded (~> 1.70)
|
||||
selenium-devtools (0.121.0)
|
||||
sass-embedded (1.77.5-aarch64-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.77.5-arm64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.77.5-x86_64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.77.5-x86_64-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sassc-embedded (1.77.7)
|
||||
sass-embedded (~> 1.77)
|
||||
selenium-devtools (0.126.0)
|
||||
selenium-webdriver (~> 4.2)
|
||||
selenium-webdriver (4.17.0)
|
||||
selenium-webdriver (4.23.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
shoulda-matchers (6.1.0)
|
||||
shoulda-matchers (6.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.5.12)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
|
@ -467,30 +519,35 @@ GEM
|
|||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
simpleidn (0.2.1)
|
||||
unf (~> 0.1.4)
|
||||
simpleidn (0.2.3)
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (3.7.3)
|
||||
base64
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.5.1)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.7.1-aarch64-linux)
|
||||
sqlite3 (1.7.1-arm64-darwin)
|
||||
sqlite3 (1.7.1-x86_64-darwin)
|
||||
sqlite3 (1.7.1-x86_64-linux)
|
||||
sqlite3 (2.0.2-aarch64-linux-gnu)
|
||||
sqlite3 (2.0.2-arm64-darwin)
|
||||
sqlite3 (2.0.2-x86_64-darwin)
|
||||
sqlite3 (2.0.2-x86_64-linux-gnu)
|
||||
sshkey (3.0.0)
|
||||
stackprof (0.2.26)
|
||||
stringio (3.1.1)
|
||||
strscan (3.1.0)
|
||||
syntax_tree (6.2.0)
|
||||
prettier_print (>= 1.2.0)
|
||||
syntax_tree-disable_ternary (1.0.0)
|
||||
test-prof (1.3.1)
|
||||
thor (1.3.0)
|
||||
test-prof (1.3.3.1)
|
||||
thor (1.3.1)
|
||||
timeout (0.4.1)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2023.4)
|
||||
tzinfo-data (1.2024.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
|
@ -503,20 +560,21 @@ GEM
|
|||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (0.13.0)
|
||||
version_gem (1.1.3)
|
||||
version_gem (1.1.4)
|
||||
web-push (3.0.1)
|
||||
jwt (~> 2.0)
|
||||
openssl (~> 3.0)
|
||||
webmock (3.19.1)
|
||||
webmock (3.23.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket (1.2.10)
|
||||
webrick (1.8.1)
|
||||
websocket (1.2.11)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yaml-lint (0.1.2)
|
||||
yard (0.9.34)
|
||||
zeitwerk (2.6.12)
|
||||
yard (0.9.36)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
|
@ -524,6 +582,7 @@ PLATFORMS
|
|||
arm64-darwin-21
|
||||
arm64-darwin-22
|
||||
arm64-darwin-23
|
||||
arm64-darwin-24
|
||||
x86_64-darwin-18
|
||||
x86_64-darwin-19
|
||||
x86_64-darwin-20
|
||||
|
@ -531,14 +590,14 @@ PLATFORMS
|
|||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
actionmailer (< 7.1)
|
||||
actionpack (< 7.1)
|
||||
actionview (< 7.1)
|
||||
actionmailer (~> 7.1.0)
|
||||
actionpack (~> 7.1.0)
|
||||
actionview (~> 7.1.0)
|
||||
actionview_precompiler
|
||||
active_model_serializers (~> 0.8.3)
|
||||
activemodel (< 7.1)
|
||||
activerecord (< 7.1)
|
||||
activesupport (< 7.1)
|
||||
activemodel (~> 7.1.0)
|
||||
activerecord (~> 7.1.0)
|
||||
activesupport (~> 7.1.0)
|
||||
addressable
|
||||
annotate
|
||||
aws-sdk-s3
|
||||
|
@ -577,12 +636,12 @@ DEPENDENCIES
|
|||
htmlentities
|
||||
http_accept_language
|
||||
image_optim
|
||||
iso8601
|
||||
json
|
||||
json_schemer
|
||||
listen
|
||||
lograge
|
||||
logstash-event
|
||||
logstash-logger
|
||||
logster
|
||||
loofah
|
||||
lru_redux
|
||||
|
@ -591,13 +650,13 @@ DEPENDENCIES
|
|||
maxminddb
|
||||
memory_profiler
|
||||
message_bus
|
||||
messageformat-wrapper
|
||||
mini_mime
|
||||
mini_racer
|
||||
mini_scheduler
|
||||
mini_sql
|
||||
mini_suffix
|
||||
minio_runner
|
||||
minitest
|
||||
mocha
|
||||
multi_json
|
||||
mustache
|
||||
|
@ -618,6 +677,7 @@ DEPENDENCIES
|
|||
pg
|
||||
pry-byebug
|
||||
pry-rails
|
||||
pry-stack_explorer
|
||||
puma
|
||||
rack
|
||||
rack-mini-profiler
|
||||
|
@ -625,17 +685,18 @@ DEPENDENCIES
|
|||
rails-dom-testing
|
||||
rails_failover
|
||||
rails_multisite
|
||||
railties (< 7.1)
|
||||
railties (~> 7.1.0)
|
||||
rake
|
||||
rb-fsevent
|
||||
rbtrace
|
||||
rchardet
|
||||
redcarpet
|
||||
redis
|
||||
redis (< 5.0)
|
||||
redis-namespace
|
||||
rinku
|
||||
rotp
|
||||
rqrcode
|
||||
rrule
|
||||
rspec
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
|
@ -653,7 +714,7 @@ DEPENDENCIES
|
|||
shoulda-matchers
|
||||
sidekiq
|
||||
simplecov
|
||||
sprockets!
|
||||
sprockets (~> 3.7.3)
|
||||
sprockets-rails
|
||||
sqlite3
|
||||
sshkey
|
||||
|
@ -672,4 +733,4 @@ DEPENDENCIES
|
|||
yard
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
2.5.9
|
||||
|
|
|
@ -14,7 +14,7 @@ To learn more, visit [**discourse.org**](https://www.discourse.org) and join our
|
|||
|
||||
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://github-production-user-asset-6210df.s3.amazonaws.com/5862206/261580781-1413ac96-5d08-40b2-bc8e-27c3f2d3bfe6.png" width="720px"></a>
|
||||
|
||||
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
|
||||
<a href="https://twittercommunity.com/"><img alt="X Community" src="https://github.com/discourse/discourse/assets/2790986/ebb63eee-1927-4060-ada1-cf1bc774084c.png" width="720px"></a>
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1681963/52239118-b304f800-289b-11e9-9904-16450680d9ec.jpg" alt="Mobile" width="414">
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"licenses": {
|
||||
"blueOak": "bronze",
|
||||
"spdx": [
|
||||
"CC0-1.0",
|
||||
"CC-BY-3.0",
|
||||
"CC-BY-4.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"ISC"
|
||||
]
|
||||
},
|
||||
"packages": {
|
||||
"cli-table": "0.3.11",
|
||||
"component-bind": "1.0.0",
|
||||
"component-inherit": "0.0.3",
|
||||
"duplex": "1.0.0",
|
||||
"glob": "3.1.21",
|
||||
"indexof": "0.0.1",
|
||||
"inherits": "1.0.2",
|
||||
"jsonify": "0.0.1",
|
||||
"line-stream": "0.0.0",
|
||||
"messageformat": "0.1.5",
|
||||
"regenerator-transform": "0.10.1",
|
||||
"source-map": "0.1.43",
|
||||
"sourcemap-validator": "1.1.1",
|
||||
"jspreadsheet-ce": "4.13.4",
|
||||
"@jspreadsheet/formula": "2.0.2"
|
||||
},
|
||||
"corrections": true,
|
||||
"ignore": [
|
||||
{
|
||||
"author": "Discourse"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,15 +2,18 @@ import RestAdapter from "discourse/adapters/rest";
|
|||
|
||||
export default class Theme extends RestAdapter {
|
||||
jsonMode = true;
|
||||
|
||||
basePath() {
|
||||
return "/admin/";
|
||||
}
|
||||
|
||||
afterFindAll(results) {
|
||||
let map = {};
|
||||
|
||||
results.forEach((theme) => {
|
||||
map[theme.id] = theme;
|
||||
});
|
||||
|
||||
results.forEach((theme) => {
|
||||
let mapped = theme.get("child_themes") || [];
|
||||
mapped = mapped.map((t) => map[t.id]);
|
||||
|
@ -20,6 +23,7 @@ export default class Theme extends RestAdapter {
|
|||
mappedParents = mappedParents.map((t) => map[t.id]);
|
||||
theme.set("parentThemes", mappedParents);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,22 @@ export default class WebHookEvent extends RestAdapter {
|
|||
basePath() {
|
||||
return "/admin/api/";
|
||||
}
|
||||
|
||||
appendQueryParams(path, findArgs, extension) {
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(findArgs)) {
|
||||
if (value && key !== "webhookId") {
|
||||
urlSearchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = urlSearchParams.toString();
|
||||
let url = `${path}/${findArgs.webhookId}${extension || ""}`;
|
||||
|
||||
if (queryString) {
|
||||
url = `${url}?${queryString}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<div class="ace">{{this.content}}</div>
|
|
@ -0,0 +1,23 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigAreaCard extends Component {
|
||||
@tracked collapsed = false;
|
||||
|
||||
get computedHeading() {
|
||||
if (this.args.heading) {
|
||||
return I18n.t(this.args.heading);
|
||||
}
|
||||
return this.args.translatedHeading;
|
||||
}
|
||||
|
||||
<template>
|
||||
<section class="admin-config-area-card" ...attributes>
|
||||
<h3 class="admin-config-area-card__title">{{this.computedHeading}}</h3>
|
||||
<div class="admin-config-area-card__content">
|
||||
{{yield}}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import Form from "discourse/components/form";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
import UserChooser from "select-kit/components/user-chooser";
|
||||
|
||||
export default class AdminConfigAreasAboutContactInformation extends Component {
|
||||
@service site;
|
||||
@service toasts;
|
||||
|
||||
@tracked
|
||||
contactGroupId = this.site.groups.find(
|
||||
(group) => group.name === this.data.contactGroupName
|
||||
)?.id;
|
||||
|
||||
@cached
|
||||
get data() {
|
||||
return {
|
||||
communityOwner: this.args.contactInformation.communityOwner.value,
|
||||
contactEmail: this.args.contactInformation.contactEmail.value,
|
||||
contactURL: this.args.contactInformation.contactURL.value,
|
||||
contactGroupName: this.args.contactInformation.contactGroupName.value,
|
||||
contactUsername:
|
||||
this.args.contactInformation.contactUsername.value || null,
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
setContactUsername(usernames, { set }) {
|
||||
set("contactUsername", usernames[0] || null);
|
||||
}
|
||||
|
||||
@action
|
||||
setContactGroup(groupIds, { set }) {
|
||||
this.contactGroupId = groupIds[0];
|
||||
set(
|
||||
"contactGroupName",
|
||||
this.site.groups.find((group) => group.id === groupIds[0])?.name
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async save(data) {
|
||||
try {
|
||||
this.args.setGlobalSavingStatus(true);
|
||||
await ajax("/admin/config/about.json", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
contact_information: {
|
||||
community_owner: data.communityOwner,
|
||||
contact_email: data.contactEmail,
|
||||
contact_url: data.contactURL,
|
||||
contact_username: data.contactUsername,
|
||||
contact_group_name: data.contactGroupName,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.toasts.success({
|
||||
duration: 3000,
|
||||
data: {
|
||||
message: I18n.t(
|
||||
"admin.config_areas.about.toasts.contact_information_saved"
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
} finally {
|
||||
this.args.setGlobalSavingStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<Form @data={{this.data}} @onSubmit={{this.save}} as |form|>
|
||||
<form.Field
|
||||
@name="communityOwner"
|
||||
@title={{i18n "admin.config_areas.about.community_owner"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.community_owner_help"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.community_owner_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="contactEmail"
|
||||
@title={{i18n "admin.config_areas.about.contact_email"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.contact_email_help"}}
|
||||
@type="email"
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.contact_email_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="contactURL"
|
||||
@title={{i18n "admin.config_areas.about.contact_url"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.contact_url_help"}}
|
||||
@type="url"
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.contact_url_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="contactUsername"
|
||||
@title={{i18n "admin.config_areas.about.site_contact_name"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.site_contact_group_help"}}
|
||||
@onSet={{this.setContactUsername}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<UserChooser
|
||||
@value={{field.value}}
|
||||
@options={{hash maximum=1}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="contactGroupName"
|
||||
@title={{i18n "admin.config_areas.about.site_contact_group"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.site_contact_group_help"}}
|
||||
@onSet={{this.setContactGroup}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<GroupChooser
|
||||
@content={{this.site.groups}}
|
||||
@value={{this.contactGroupId}}
|
||||
@options={{hash maximum=1}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
<form.Submit
|
||||
@label="admin.config_areas.about.update"
|
||||
@disabled={{@globalSavingStatus}}
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import Form from "discourse/components/form";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigAreasAboutGeneralSettings extends Component {
|
||||
@service toasts;
|
||||
|
||||
name = this.args.generalSettings.title.value;
|
||||
summary = this.args.generalSettings.siteDescription.value;
|
||||
extendedDescription = this.args.generalSettings.extendedSiteDescription.value;
|
||||
aboutBannerImage = this.args.generalSettings.aboutBannerImage.value;
|
||||
|
||||
@cached
|
||||
get data() {
|
||||
return {
|
||||
name: this.args.generalSettings.title.value,
|
||||
summary: this.args.generalSettings.siteDescription.value,
|
||||
extendedDescription:
|
||||
this.args.generalSettings.extendedSiteDescription.value,
|
||||
aboutBannerImage: this.args.generalSettings.aboutBannerImage.value,
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
async save(data) {
|
||||
try {
|
||||
this.args.setGlobalSavingStatus(true);
|
||||
await ajax("/admin/config/about.json", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
general_settings: {
|
||||
name: data.name,
|
||||
summary: data.summary,
|
||||
extended_description: data.extendedDescription,
|
||||
about_banner_image: data.aboutBannerImage,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.toasts.success({
|
||||
duration: 3000,
|
||||
data: {
|
||||
message: I18n.t(
|
||||
"admin.config_areas.about.toasts.general_settings_saved"
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
} finally {
|
||||
this.args.setGlobalSavingStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setImage(upload, { set }) {
|
||||
set("aboutBannerImage", upload.url);
|
||||
}
|
||||
|
||||
<template>
|
||||
<Form @data={{this.data}} @onSubmit={{this.save}} as |form|>
|
||||
<form.Field
|
||||
@name="name"
|
||||
@title={{i18n "admin.config_areas.about.community_name"}}
|
||||
@validation="required"
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.community_name_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="summary"
|
||||
@title={{i18n "admin.config_areas.about.community_summary"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="extendedDescription"
|
||||
@title={{i18n "admin.config_areas.about.community_description"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Composer />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="aboutBannerImage"
|
||||
@title={{i18n "admin.config_areas.about.banner_image"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.banner_image_help"}}
|
||||
@onSet={{this.setImage}}
|
||||
as |field|
|
||||
>
|
||||
<field.Image />
|
||||
</form.Field>
|
||||
|
||||
<form.Submit
|
||||
@label="admin.config_areas.about.update"
|
||||
@disabled={{@globalSavingStatus}}
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import Form from "discourse/components/form";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigAreasAboutYourOrganization extends Component {
|
||||
@service toasts;
|
||||
|
||||
companyName = this.args.yourOrganization.companyName.value;
|
||||
governingLaw = this.args.yourOrganization.governingLaw.value;
|
||||
cityForDisputes = this.args.yourOrganization.cityForDisputes.value;
|
||||
|
||||
@cached
|
||||
get data() {
|
||||
return {
|
||||
companyName: this.args.yourOrganization.companyName.value,
|
||||
governingLaw: this.args.yourOrganization.governingLaw.value,
|
||||
cityForDisputes: this.args.yourOrganization.cityForDisputes.value,
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
async save(data) {
|
||||
this.args.setGlobalSavingStatus(true);
|
||||
try {
|
||||
await ajax("/admin/config/about.json", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
your_organization: {
|
||||
company_name: data.companyName,
|
||||
governing_law: data.governingLaw,
|
||||
city_for_disputes: data.cityForDisputes,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.toasts.success({
|
||||
duration: 30000,
|
||||
data: {
|
||||
message: I18n.t(
|
||||
"admin.config_areas.about.toasts.your_organization_saved"
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
} finally {
|
||||
this.args.setGlobalSavingStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<Form @data={{this.data}} @onSubmit={{this.save}} as |form|>
|
||||
<form.Field
|
||||
@name="companyName"
|
||||
@title={{i18n "admin.config_areas.about.company_name"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.company_name_help"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.company_name_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
<form.Alert @type="error">
|
||||
{{i18n "admin.config_areas.about.company_name_warning"}}
|
||||
</form.Alert>
|
||||
|
||||
<form.Field
|
||||
@name="governingLaw"
|
||||
@title={{i18n "admin.config_areas.about.governing_law"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.governing_law_help"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.governing_law_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="cityForDisputes"
|
||||
@title={{i18n "admin.config_areas.about.city_for_disputes"}}
|
||||
@subtitle={{i18n "admin.config_areas.about.city_for_disputes_help"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.config_areas.about.city_for_disputes_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Submit
|
||||
@label="admin.config_areas.about.update"
|
||||
@disabled={{@globalSavingStatus}}
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<div
|
||||
class="admin-config-area-sidebar-experiment"
|
||||
{{did-insert this.loadDefaultNavConfig}}
|
||||
>
|
||||
<h4>Sidebar Experiment</h4>
|
||||
<p>Changes you make here will be applied to the admin sidebar and persist
|
||||
between reloads
|
||||
<em>on this device only</em>. Note that in addition to the
|
||||
<code>text</code>
|
||||
and
|
||||
<code>route</code>
|
||||
options, you can also specify a
|
||||
<code>icon</code>
|
||||
or a
|
||||
<code>href</code>, if you want to link to a specific page but don't know the
|
||||
Ember route. You can also use the Ember Inspector extension to find route
|
||||
names, for example for plugin routes which are not auto-generated here.
|
||||
<br /><br />
|
||||
<code>routeModels</code>
|
||||
is an array of values that correspond to parts of the route; for example the
|
||||
topic route may be
|
||||
<code>/t/:id</code>, so to get a link to topic with ID 123 you would do
|
||||
<code>routeModels: [123]</code>.</p>
|
||||
|
||||
<p>All configuration options for a section and its links looks like this:</p>
|
||||
|
||||
<pre><code>{{this.exampleJson}}</code></pre>
|
||||
|
||||
<DButton
|
||||
@action={{this.resetToDefault}}
|
||||
@translatedLabel="Reset to Default"
|
||||
/>
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@action={{this.applyConfig}}
|
||||
@translatedLabel="Apply Config"
|
||||
/>
|
||||
|
||||
<div class="admin-config-area-sidebar-experiment__editor">
|
||||
<AceEditor
|
||||
@content={{this.editedNavConfig}}
|
||||
@editorId="admin-config-area-sidebar-experiment"
|
||||
@save={{this.applyNav}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
|
@ -1,130 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
|
||||
import {
|
||||
buildAdminSidebar,
|
||||
useAdminNavConfig,
|
||||
} from "discourse/lib/sidebar/admin-sidebar";
|
||||
import { resetPanelSections } from "discourse/lib/sidebar/custom-sections";
|
||||
import { ADMIN_PANEL } from "discourse/lib/sidebar/panels";
|
||||
|
||||
// TODO (martin) (2024-02-01) Remove this experimental UI.
|
||||
export default class AdminConfigAreaSidebarExperiment extends Component {
|
||||
@service adminSidebarExperimentStateManager;
|
||||
@service toasts;
|
||||
@service router;
|
||||
@tracked editedNavConfig;
|
||||
|
||||
validRouteNames = new Set();
|
||||
|
||||
get defaultAdminNav() {
|
||||
return JSON.stringify(ADMIN_NAV_MAP, null, 2);
|
||||
}
|
||||
|
||||
get exampleJson() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: "section-name",
|
||||
text: "Section Name",
|
||||
links: [
|
||||
{
|
||||
name: "admin-revamp",
|
||||
route: "admin-revamp",
|
||||
routeModels: [123],
|
||||
text: "Revamp",
|
||||
href: "https://forum.site.com/t/123",
|
||||
icon: "rocket",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
loadDefaultNavConfig() {
|
||||
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
|
||||
this.editedNavConfig = savedConfig
|
||||
? JSON.stringify(savedConfig, null, 2)
|
||||
: this.defaultAdminNav;
|
||||
}
|
||||
|
||||
@action
|
||||
resetToDefault() {
|
||||
this.editedNavConfig = this.defaultAdminNav;
|
||||
this.#saveConfig(ADMIN_NAV_MAP);
|
||||
}
|
||||
|
||||
@action
|
||||
applyConfig() {
|
||||
let config = null;
|
||||
try {
|
||||
config = JSON.parse(this.editedNavConfig);
|
||||
} catch {
|
||||
this.toasts.error({
|
||||
duration: 3000,
|
||||
data: {
|
||||
message: "There was an error, make sure the structure is valid JSON.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let invalidRoutes = [];
|
||||
config.forEach((section) => {
|
||||
section.links.forEach((link) => {
|
||||
if (!link.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.validRouteNames.has(link.route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Using the private `_routerMicrolib` is not ideal, but Ember doesn't provide
|
||||
// any other way for us to easily check for route validity.
|
||||
try {
|
||||
// eslint-disable-next-line ember/no-private-routing-service
|
||||
this.router._router._routerMicrolib.recognizer.handlersFor(
|
||||
link.route
|
||||
);
|
||||
this.validRouteNames.add(link.route);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("[AdminSidebarExperiment]", err);
|
||||
invalidRoutes.push(link.route);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (invalidRoutes.length) {
|
||||
this.toasts.error({
|
||||
duration: 3000,
|
||||
data: {
|
||||
message: `There was an error with one or more of the routes provided: ${invalidRoutes.join(
|
||||
", "
|
||||
)}`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.#saveConfig(config);
|
||||
}
|
||||
|
||||
#saveConfig(config) {
|
||||
this.adminSidebarExperimentStateManager.navConfig = config;
|
||||
resetPanelSections(
|
||||
ADMIN_PANEL,
|
||||
useAdminNavConfig(config),
|
||||
buildAdminSidebar
|
||||
);
|
||||
this.toasts.success({
|
||||
duration: 3000,
|
||||
data: { message: "Sidebar navigation applied successfully!" },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import AdminConfigAreasAboutContactInformation from "admin/components/admin-config-area-cards/about/contact-information";
|
||||
import AdminConfigAreasAboutGeneralSettings from "admin/components/admin-config-area-cards/about/general-settings";
|
||||
import AdminConfigAreasAboutYourOrganization from "admin/components/admin-config-area-cards/about/your-organization";
|
||||
|
||||
export default class AdminConfigAreasAbout extends Component {
|
||||
@tracked saving = false;
|
||||
|
||||
get generalSettings() {
|
||||
return {
|
||||
title: this.#lookupSettingFromData("title"),
|
||||
siteDescription: this.#lookupSettingFromData("site_description"),
|
||||
extendedSiteDescription: this.#lookupSettingFromData(
|
||||
"extended_site_description"
|
||||
),
|
||||
aboutBannerImage: this.#lookupSettingFromData("about_banner_image"),
|
||||
};
|
||||
}
|
||||
|
||||
get contactInformation() {
|
||||
return {
|
||||
communityOwner: this.#lookupSettingFromData("community_owner"),
|
||||
contactEmail: this.#lookupSettingFromData("contact_email"),
|
||||
contactURL: this.#lookupSettingFromData("contact_url"),
|
||||
contactUsername: this.#lookupSettingFromData("site_contact_username"),
|
||||
contactGroupName: this.#lookupSettingFromData("site_contact_group_name"),
|
||||
};
|
||||
}
|
||||
|
||||
get yourOrganization() {
|
||||
return {
|
||||
companyName: this.#lookupSettingFromData("company_name"),
|
||||
governingLaw: this.#lookupSettingFromData("governing_law"),
|
||||
cityForDisputes: this.#lookupSettingFromData("city_for_disputes"),
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
setSavingStatus(status) {
|
||||
this.saving = status;
|
||||
}
|
||||
|
||||
#lookupSettingFromData(name) {
|
||||
return this.args.data.findBy("setting", name);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-config-area">
|
||||
<h2>{{i18n "admin.config_areas.about.header"}}</h2>
|
||||
<div class="admin-config-area__primary-content">
|
||||
<AdminConfigAreaCard
|
||||
@heading="admin.config_areas.about.general_settings"
|
||||
@primaryActionLabel="admin.config_areas.about.update"
|
||||
class="admin-config-area-about__general-settings-section"
|
||||
>
|
||||
<AdminConfigAreasAboutGeneralSettings
|
||||
@generalSettings={{this.generalSettings}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</AdminConfigAreaCard>
|
||||
<AdminConfigAreaCard
|
||||
@heading="admin.config_areas.about.contact_information"
|
||||
@primaryActionLabel="admin.config_areas.about.update"
|
||||
class="admin-config-area-about__contact-information-section"
|
||||
>
|
||||
<AdminConfigAreasAboutContactInformation
|
||||
@contactInformation={{this.contactInformation}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</AdminConfigAreaCard>
|
||||
<AdminConfigAreaCard
|
||||
@heading="admin.config_areas.about.your_organization"
|
||||
@primaryActionLabel="admin.config_areas.about.update"
|
||||
class="admin-config-area-about__your-organization-section"
|
||||
>
|
||||
<AdminConfigAreasAboutYourOrganization
|
||||
@yourOrganization={{this.yourOrganization}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import AdminConfigHeader from "admin/components/admin-config-header";
|
||||
import AdminFlagItem from "admin/components/admin-flag-item";
|
||||
|
||||
export default class AdminConfigAreasFlags extends Component {
|
||||
@service site;
|
||||
@tracked flags = this.site.flagTypes;
|
||||
|
||||
@bind
|
||||
isFirstFlag(flag) {
|
||||
return this.flags.indexOf(flag) === 1;
|
||||
}
|
||||
|
||||
@bind
|
||||
isLastFlag(flag) {
|
||||
return this.flags.indexOf(flag) === this.flags.length - 1;
|
||||
}
|
||||
|
||||
@action
|
||||
moveFlagCallback(flag, direction) {
|
||||
const fallbackFlags = [...this.flags];
|
||||
|
||||
const flags = this.flags;
|
||||
|
||||
const flagIndex = flags.indexOf(flag);
|
||||
const targetFlagIndex = direction === "up" ? flagIndex - 1 : flagIndex + 1;
|
||||
|
||||
const targetFlag = flags[targetFlagIndex];
|
||||
|
||||
flags[flagIndex] = targetFlag;
|
||||
flags[targetFlagIndex] = flag;
|
||||
|
||||
this.flags = flags;
|
||||
|
||||
return ajax(`/admin/config/flags/${flag.id}/reorder/${direction}`, {
|
||||
type: "PUT",
|
||||
}).catch((error) => {
|
||||
this.flags = fallbackFlags;
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteFlagCallback(flag) {
|
||||
return ajax(`/admin/config/flags/${flag.id}`, {
|
||||
type: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
this.flags.removeObject(flag);
|
||||
})
|
||||
.catch((error) => popupAjaxError(error));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="container admin-flags">
|
||||
<AdminConfigHeader
|
||||
@name="flags"
|
||||
@heading="admin.config_areas.flags.header"
|
||||
@subheading="admin.config_areas.flags.subheader"
|
||||
@primaryActionRoute="adminConfig.flags.new"
|
||||
@primaryActionCssClass="admin-flags__header-add-flag"
|
||||
@primaryActionIcon="plus"
|
||||
@primaryActionLabel="admin.config_areas.flags.add"
|
||||
/>
|
||||
<table class="admin-flags__items grid">
|
||||
<thead>
|
||||
<th>{{i18n "admin.config_areas.flags.description"}}</th>
|
||||
<th>{{i18n "admin.config_areas.flags.enabled"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.flags as |flag|}}
|
||||
<AdminFlagItem
|
||||
@flag={{flag}}
|
||||
@moveFlagCallback={{this.moveFlagCallback}}
|
||||
@deleteFlagCallback={{this.deleteFlagCallback}}
|
||||
@isFirstFlag={{this.isFirstFlag flag}}
|
||||
@isLastFlag={{this.isLastFlag flag}}
|
||||
/>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class AdminFlagItem extends Component {
|
||||
get headerCssClass() {
|
||||
return `admin-${this.args.name}__header`;
|
||||
}
|
||||
<template>
|
||||
<div class={{this.headerCssClass}}>
|
||||
<h2>{{i18n @heading}}</h2>
|
||||
{{#if @primaryActionRoute}}
|
||||
<LinkTo
|
||||
@route={{@primaryActionRoute}}
|
||||
class={{concatClass
|
||||
"btn-primary"
|
||||
"btn"
|
||||
"btn-icon-text"
|
||||
@primaryActionCssClass
|
||||
}}
|
||||
>
|
||||
{{dIcon @primaryActionIcon}}
|
||||
{{i18n @primaryActionLabel}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if @subheading}}
|
||||
<h3>{{i18n @subheading}}</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { SYSTEM_FLAG_IDS } from "discourse/lib/constants";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
export default class AdminFlagItem extends Component {
|
||||
@service dialog;
|
||||
@service router;
|
||||
|
||||
@tracked enabled = this.args.flag.enabled;
|
||||
@tracked isSaving = false;
|
||||
|
||||
get canMove() {
|
||||
return this.args.flag.id !== SYSTEM_FLAG_IDS.notify_user;
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return (
|
||||
!Object.values(SYSTEM_FLAG_IDS).includes(this.args.flag.id) &&
|
||||
!this.args.flag.is_used
|
||||
);
|
||||
}
|
||||
|
||||
get editTitle() {
|
||||
return this.canEdit
|
||||
? "admin.config_areas.flags.form.edit_flag"
|
||||
: "admin.config_areas.flags.form.non_editable";
|
||||
}
|
||||
|
||||
get deleteTitle() {
|
||||
return this.canEdit
|
||||
? "admin.config_areas.flags.form.edit_flag"
|
||||
: "admin.config_areas.flags.form.non_editable";
|
||||
}
|
||||
|
||||
@action
|
||||
toggleFlagEnabled(flag) {
|
||||
this.enabled = !this.enabled;
|
||||
this.isSaving = true;
|
||||
|
||||
return ajax(`/admin/config/flags/${flag.id}/toggle`, {
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
this.args.flag.enabled = this.enabled;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.enabled = !this.enabled;
|
||||
return popupAjaxError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
}
|
||||
|
||||
@action
|
||||
moveUp() {
|
||||
this.isSaving = true;
|
||||
this.dMenu.close();
|
||||
this.args.moveFlagCallback(this.args.flag, "up").finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
moveDown() {
|
||||
this.isSaving = true;
|
||||
this.dMenu.close();
|
||||
this.args.moveFlagCallback(this.args.flag, "down").finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
}
|
||||
@action
|
||||
edit() {
|
||||
this.router.transitionTo("adminConfig.flags.edit", this.args.flag);
|
||||
}
|
||||
|
||||
@action
|
||||
delete() {
|
||||
this.isSaving = true;
|
||||
this.dialog.yesNoConfirm({
|
||||
message: i18n("admin.config_areas.flags.delete_confirm", {
|
||||
name: this.args.flag.name,
|
||||
}),
|
||||
didConfirm: () => {
|
||||
this.args.deleteFlagCallback(this.args.flag).finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
},
|
||||
didCancel: () => {
|
||||
this.isSaving = false;
|
||||
},
|
||||
});
|
||||
this.dMenu.close();
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr
|
||||
class={{concatClass
|
||||
"admin-flag-item"
|
||||
@flag.name_key
|
||||
(if this.isSaving "saving")
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<p class="admin-flag-item__name">{{@flag.name}}</p>
|
||||
<p class="admin-flag-item__description">{{htmlSafe
|
||||
@flag.description
|
||||
}}</p>
|
||||
</td>
|
||||
<td>
|
||||
<div class="admin-flag-item__options">
|
||||
<DToggleSwitch
|
||||
@state={{this.enabled}}
|
||||
class="admin-flag-item__toggle {{@flag.name_key}}"
|
||||
{{on "click" (fn this.toggleFlagEnabled @flag)}}
|
||||
/>
|
||||
|
||||
<DButton
|
||||
class="btn btn-secondary admin-flag-item__edit"
|
||||
@action={{this.edit}}
|
||||
@label="admin.config_areas.flags.edit"
|
||||
@disabled={{not this.canEdit}}
|
||||
@title={{this.editTitle}}
|
||||
/>
|
||||
|
||||
{{#if this.canMove}}
|
||||
<DMenu
|
||||
@identifier="flag-menu"
|
||||
@title={{i18n "admin.config_areas.flags.more_options.title"}}
|
||||
@icon="ellipsis-v"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#unless @isFirstFlag}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.flags.more_options.move_up"
|
||||
@icon="arrow-up"
|
||||
@class="btn-transparent admin-flag-item__move-up"
|
||||
@action={{this.moveUp}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/unless}}
|
||||
{{#unless @isLastFlag}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.flags.more_options.move_down"
|
||||
@icon="arrow-down"
|
||||
@class="btn-transparent admin-flag-item__move-down"
|
||||
@action={{this.moveDown}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/unless}}
|
||||
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.flags.delete"
|
||||
@icon="trash-alt"
|
||||
class="btn-transparent admin-flag-item__delete"
|
||||
@action={{this.delete}}
|
||||
@disabled={{not this.canEdit}}
|
||||
@title={{this.deleteTitle}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { TextArea } from "@ember/legacy-built-in-components";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import withEventValue from "discourse/helpers/with-event-value";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import MultiSelect from "select-kit/components/multi-select";
|
||||
|
||||
export default class AdminFlagsForm extends Component {
|
||||
@service router;
|
||||
@service site;
|
||||
|
||||
@tracked enabled = true;
|
||||
@tracked requireMessage = false;
|
||||
@tracked name;
|
||||
@tracked description;
|
||||
@tracked appliesTo;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.isUpdate) {
|
||||
this.name = this.args.flag.name;
|
||||
this.description = this.args.flag.description;
|
||||
this.appliesTo = this.args.flag.applies_to;
|
||||
this.requireMessage = this.args.flag.require_message;
|
||||
this.enabled = this.args.flag.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
get isUpdate() {
|
||||
return this.args.flag;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return (
|
||||
!isEmpty(this.name) &&
|
||||
!isEmpty(this.description) &&
|
||||
!isEmpty(this.appliesTo)
|
||||
);
|
||||
}
|
||||
|
||||
get header() {
|
||||
return this.isUpdate
|
||||
? "admin.config_areas.flags.form.edit_header"
|
||||
: "admin.config_areas.flags.form.add_header";
|
||||
}
|
||||
|
||||
get appliesToValues() {
|
||||
return this.site.valid_flag_applies_to_types.map((type) => {
|
||||
return {
|
||||
name: I18n.t(
|
||||
`admin.config_areas.flags.form.${type
|
||||
.toLowerCase()
|
||||
.replace("::", "_")}`
|
||||
),
|
||||
id: type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.isUpdate ? this.update() : this.create();
|
||||
}
|
||||
|
||||
@action
|
||||
onToggleRequireMessage(e) {
|
||||
this.requireMessage = e.target.checked;
|
||||
}
|
||||
|
||||
@action
|
||||
onToggleEnabled(e) {
|
||||
this.enabled = e.target.checked;
|
||||
}
|
||||
|
||||
@bind
|
||||
create() {
|
||||
return ajax(`/admin/config/flags`, {
|
||||
type: "POST",
|
||||
data: this.#formData,
|
||||
})
|
||||
.then((response) => {
|
||||
this.site.flagTypes.push(response.flag);
|
||||
this.router.transitionTo("adminConfig.flags");
|
||||
})
|
||||
.catch((error) => {
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
update() {
|
||||
return ajax(`/admin/config/flags/${this.args.flag.id}`, {
|
||||
type: "PUT",
|
||||
data: this.#formData,
|
||||
})
|
||||
.then((response) => {
|
||||
this.args.flag.name = response.flag.name;
|
||||
this.args.flag.description = response.flag.description;
|
||||
this.args.flag.applies_to = response.flag.applies_to;
|
||||
this.args.flag.require_message = response.flag.require_message;
|
||||
this.args.flag.enabled = response.flag.enabled;
|
||||
this.router.transitionTo("adminConfig.flags");
|
||||
})
|
||||
.catch((error) => {
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
get #formData() {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
applies_to: this.appliesTo,
|
||||
require_message: this.requireMessage,
|
||||
enabled: this.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-config-area">
|
||||
<h2>{{i18n "admin.config_areas.flags.header"}}</h2>
|
||||
<LinkTo
|
||||
@route="adminConfig.flags"
|
||||
class="btn-default btn btn-icon-text btn-back"
|
||||
>
|
||||
{{dIcon "chevron-left"}}
|
||||
{{i18n "admin.config_areas.flags.back"}}
|
||||
</LinkTo>
|
||||
<div class="admin-config-area__primary-content admin-flag-form">
|
||||
<AdminConfigAreaCard @heading={{this.header}}>
|
||||
<div class="control-group">
|
||||
<label for="name">
|
||||
{{i18n "admin.config_areas.flags.form.name"}}
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
value={{this.name}}
|
||||
maxlength="200"
|
||||
class="admin-flag-form__name"
|
||||
{{on "input" (withEventValue (fn (mut this.name)))}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="description">
|
||||
{{i18n "admin.config_areas.flags.form.description"}}
|
||||
</label>
|
||||
<TextArea
|
||||
@value={{this.description}}
|
||||
maxlength="1000"
|
||||
class="admin-flag-form__description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="applies-to">
|
||||
{{i18n "admin.config_areas.flags.form.applies_to"}}
|
||||
</label>
|
||||
<MultiSelect
|
||||
@value={{this.appliesTo}}
|
||||
@content={{this.appliesToValues}}
|
||||
@options={{hash allowAny=false}}
|
||||
class="admin-flag-form__applies-to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="checkbox-label admin-flag-form__require-reason">
|
||||
<input
|
||||
{{on "input" this.onToggleRequireMessage}}
|
||||
type="checkbox"
|
||||
checked={{this.requireMessage}}
|
||||
/>
|
||||
<div>
|
||||
{{i18n "admin.config_areas.flags.form.require_message"}}
|
||||
<div class="admin-flag-form__require-message-description">
|
||||
{{i18n
|
||||
"admin.config_areas.flags.form.require_message_description"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="checkbox-label admin-flag-form__enabled">
|
||||
<input
|
||||
{{on "input" this.onToggleEnabled}}
|
||||
type="checkbox"
|
||||
checked={{this.enabled}}
|
||||
/>
|
||||
{{i18n "admin.config_areas.flags.form.enabled"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info admin_flag_form__info">
|
||||
{{dIcon "info-circle"}}
|
||||
{{i18n "admin.config_areas.flags.form.alert"}}
|
||||
</div>
|
||||
|
||||
<DButton
|
||||
@action={{this.save}}
|
||||
@label="admin.config_areas.flags.form.save"
|
||||
@ariaLabel="admin.config_areas.flags.form.save"
|
||||
@disabled={{not this.isValid}}
|
||||
class="btn-primary admin-flag-form__save"
|
||||
/>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -34,7 +34,7 @@ export default class AdminPenaltyPostAction extends Component {
|
|||
@afterRender
|
||||
_focusEditTextarea() {
|
||||
const elem = this.element;
|
||||
const body = elem.closest(".modal-body");
|
||||
const body = elem.closest(".d-modal__body");
|
||||
body.scrollTo(0, body.clientHeight);
|
||||
elem.querySelector(".post-editor").focus();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminPluginConfigArea extends Component {
|
||||
@service adminPluginNavManager;
|
||||
|
||||
linkText(navLink) {
|
||||
if (navLink.label) {
|
||||
return I18n.t(navLink.label);
|
||||
} else {
|
||||
return navLink.text;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.adminPluginNavManager.isSidebarMode}}
|
||||
<nav class="admin-nav admin-plugin-inner-sidebar-nav pull-left">
|
||||
<ul class="nav nav-stacked">
|
||||
{{#each
|
||||
this.adminPluginNavManager.currentConfigNav.links
|
||||
as |navLink|
|
||||
}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"admin-plugin-inner-sidebar-nav__item"
|
||||
navLink.route
|
||||
}}
|
||||
>
|
||||
<LinkTo
|
||||
@route={{navLink.route}}
|
||||
@model={{navLink.model}}
|
||||
title={{this.linkText navLink}}
|
||||
>
|
||||
{{this.linkText navLink}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{/if}}
|
||||
<section class="admin-plugin-config-area">
|
||||
{{yield}}
|
||||
</section>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
const AdminPluginConfigMetadata = <template>
|
||||
<div class="admin-plugin-config-page__metadata">
|
||||
<div class="admin-plugin-config-area__metadata-title">
|
||||
<h2>
|
||||
{{@plugin.nameTitleized}}
|
||||
</h2>
|
||||
<p>
|
||||
{{@plugin.about}}
|
||||
{{#if @plugin.linkUrl}}
|
||||
|
|
||||
<a href={{@plugin.linkUrl}} rel="noopener noreferrer" target="_blank">
|
||||
{{i18n "admin.plugins.learn_more"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default AdminPluginConfigMetadata;
|
|
@ -0,0 +1,55 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import AdminPluginConfigArea from "./admin-plugin-config-area";
|
||||
import AdminPluginConfigMetadata from "./admin-plugin-config-metadata";
|
||||
import AdminPluginConfigTopNav from "./admin-plugin-config-top-nav";
|
||||
|
||||
export default class AdminPluginConfigPage extends Component {
|
||||
@service currentUser;
|
||||
@service adminPluginNavManager;
|
||||
|
||||
get mainAreaClasses() {
|
||||
let classes = ["admin-plugin-config-page__main-area"];
|
||||
|
||||
if (this.adminPluginNavManager.isSidebarMode) {
|
||||
classes.push("-with-inner-sidebar");
|
||||
} else {
|
||||
classes.push("-without-inner-sidebar");
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-plugin-config-page">
|
||||
<DBreadcrumbsContainer />
|
||||
|
||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/plugins"
|
||||
@label={{i18n "admin.plugins.title"}}
|
||||
/>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/plugins/{{@plugin.name}}"
|
||||
@label={{@plugin.nameTitleized}}
|
||||
/>
|
||||
|
||||
<AdminPluginConfigMetadata @plugin={{@plugin}} />
|
||||
|
||||
{{#if this.adminPluginNavManager.isTopMode}}
|
||||
<AdminPluginConfigTopNav />
|
||||
{{/if}}
|
||||
|
||||
<div class="admin-plugin-config-page__content">
|
||||
<div class={{this.mainAreaClasses}}>
|
||||
<AdminPluginConfigArea>
|
||||
{{yield}}
|
||||
</AdminPluginConfigArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
|
||||
import NavItem from "discourse/components/nav-item";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class AdminPluginConfigTopNav extends Component {
|
||||
@service adminPluginNavManager;
|
||||
|
||||
linkText(navLink) {
|
||||
if (navLink.label) {
|
||||
return i18n(navLink.label);
|
||||
} else {
|
||||
return navLink.text;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-nav-submenu">
|
||||
<HorizontalOverflowNav
|
||||
class="plugin-nav admin-plugin-config-page__top-nav"
|
||||
>
|
||||
{{#each this.adminPluginNavManager.currentConfigNav.links as |navLink|}}
|
||||
<NavItem
|
||||
@route={{navLink.route}}
|
||||
@i18nLabel={{this.linkText navLink}}
|
||||
title={{this.linkText navLink}}
|
||||
class="admin-plugin-config-page__top-nav-item"
|
||||
>
|
||||
{{this.linkText navLink}}
|
||||
</NavItem>
|
||||
{{/each}}
|
||||
</HorizontalOverflowNav>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import SiteSettingFilter from "discourse/lib/site-setting-filter";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import AdminSiteSettingsFilterControls from "admin/components/admin-site-settings-filter-controls";
|
||||
import SiteSetting from "admin/components/site-setting";
|
||||
|
||||
export default class AdminPluginFilteredSiteSettings extends Component {
|
||||
@service currentUser;
|
||||
@tracked visibleSettings;
|
||||
@tracked loading = true;
|
||||
|
||||
siteSettingFilter = new SiteSettingFilter(this.args.settings);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.filterChanged({ filter: "", onlyOverridden: false });
|
||||
}
|
||||
|
||||
@action
|
||||
filterChanged(filterData) {
|
||||
this._debouncedOnChangeFilter(filterData);
|
||||
}
|
||||
|
||||
get noResults() {
|
||||
return isEmpty(this.visibleSettings) && !this.loading;
|
||||
}
|
||||
|
||||
_debouncedOnChangeFilter(filterData) {
|
||||
cancel(this.onChangeFilterHandler);
|
||||
this.onChangeFilterHandler = discourseDebounce(
|
||||
this,
|
||||
this.filterSettings,
|
||||
filterData,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
filterSettings(filterData) {
|
||||
this.args.onFilterChanged(filterData);
|
||||
this.visibleSettings = this.siteSettingFilter.filterSettings(
|
||||
filterData.filter,
|
||||
{
|
||||
includeAllCategory: false,
|
||||
onlyOverridden: filterData.onlyOverridden,
|
||||
}
|
||||
)[0]?.siteSettings;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
<template>
|
||||
<AdminSiteSettingsFilterControls
|
||||
@onChangeFilter={{this.filterChanged}}
|
||||
@initialFilter={{@initialFilter}}
|
||||
/>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
<section class="form-horizontal settings">
|
||||
{{#each this.visibleSettings as |setting|}}
|
||||
<SiteSetting @setting={{setting}} />
|
||||
{{/each}}
|
||||
|
||||
{{#if this.noResults}}
|
||||
{{i18n "admin.site_settings.no_results"}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</ConditionalLoadingSpinner>
|
||||
</template>
|
||||
}
|
|
@ -3,8 +3,9 @@ import { concat, fn, hash } from "@ember/helper";
|
|||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
@ -14,6 +15,7 @@ import PluginCommitHash from "./plugin-commit-hash";
|
|||
export default class AdminPluginsListItem extends Component {
|
||||
@service session;
|
||||
@service currentUser;
|
||||
@service sidebarState;
|
||||
|
||||
@action
|
||||
async togglePluginEnabled(plugin) {
|
||||
|
@ -30,20 +32,43 @@ export default class AdminPluginsListItem extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
get isAdminSearchFiltered() {
|
||||
if (!this.sidebarState.filter) {
|
||||
return false;
|
||||
}
|
||||
return this.args.plugin.nameTitleizedLower.match(this.sidebarState.filter);
|
||||
}
|
||||
|
||||
get showPluginSettingsButton() {
|
||||
return this.currentUser.admin && this.args.plugin.hasSettings;
|
||||
}
|
||||
|
||||
get disablePluginSettingsButton() {
|
||||
return (
|
||||
this.showPluginSettingsButton && this.args.plugin.hasOnlyEnabledSetting
|
||||
);
|
||||
}
|
||||
|
||||
get settingsButtonTitle() {
|
||||
if (this.disablePluginSettingsButton) {
|
||||
return i18n("admin.plugins.settings_disabled");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr data-plugin-name={{@plugin.name}}>
|
||||
<td class="admin-plugins-list__row">
|
||||
<tr
|
||||
data-plugin-name={{@plugin.name}}
|
||||
class={{concat
|
||||
"admin-plugins-list__row"
|
||||
(if this.isAdminSearchFiltered "-admin-search-filtered")
|
||||
}}
|
||||
>
|
||||
<td class="admin-plugins-list__name-details">
|
||||
<div class="admin-plugins-list__name-with-badges">
|
||||
<div class="admin-plugins-list__name">
|
||||
{{#if @plugin.linkUrl}}
|
||||
<a
|
||||
href={{@plugin.linkUrl}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>{{@plugin.nameTitleized}}</a>
|
||||
{{else}}
|
||||
{{@plugin.nameTitleized}}
|
||||
{{/if}}
|
||||
{{@plugin.nameTitleized}}
|
||||
</div>
|
||||
|
||||
<div class="badges">
|
||||
|
@ -53,6 +78,12 @@ export default class AdminPluginsListItem extends Component {
|
|||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<PluginOutlet
|
||||
@name="admin-plugin-list-name-badge-after"
|
||||
@connectorTagName="span"
|
||||
@outletArgs={{hash plugin=@plugin}}
|
||||
/>
|
||||
</div>
|
||||
<div class="admin-plugins-list__author">
|
||||
{{@plugin.author}}
|
||||
|
@ -66,41 +97,69 @@ export default class AdminPluginsListItem extends Component {
|
|||
target="_blank"
|
||||
>
|
||||
{{i18n "admin.plugins.learn_more"}}
|
||||
{{icon "external-link-alt"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="admin-plugins-list__version">
|
||||
<div class="label">{{i18n "admin.plugins.version"}}</div>
|
||||
{{@plugin.version}}<br />
|
||||
<PluginCommitHash @plugin={{@plugin}} />
|
||||
<PluginOutlet
|
||||
@name="admin-plugin-list-item-version"
|
||||
@outletArgs={{hash plugin=@plugin}}
|
||||
>
|
||||
<div class="label">{{i18n "admin.plugins.version"}}</div>
|
||||
{{@plugin.version}}<br />
|
||||
<PluginCommitHash @plugin={{@plugin}} />
|
||||
</PluginOutlet>
|
||||
</td>
|
||||
<td class="admin-plugins-list__enabled">
|
||||
<div class="label">{{i18n "admin.plugins.enabled"}}</div>
|
||||
{{#if @plugin.enabledSetting}}
|
||||
<DToggleSwitch
|
||||
@state={{@plugin.enabled}}
|
||||
{{on "click" (fn this.togglePluginEnabled @plugin)}}
|
||||
/>
|
||||
{{else}}
|
||||
<DToggleSwitch @state={{@plugin.enabled}} disabled={{true}} />
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="admin-plugin-list-item-enabled"
|
||||
@outletArgs={{hash plugin=@plugin}}
|
||||
>
|
||||
<div class="label">{{i18n "admin.plugins.enabled"}}</div>
|
||||
{{#if @plugin.enabledSetting}}
|
||||
<DToggleSwitch
|
||||
@state={{@plugin.enabled}}
|
||||
{{on "click" (fn this.togglePluginEnabled @plugin)}}
|
||||
/>
|
||||
{{else}}
|
||||
<DToggleSwitch @state={{@plugin.enabled}} disabled={{true}} />
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
</td>
|
||||
<td class="admin-plugins-list__settings">
|
||||
{{#if this.currentUser.admin}}
|
||||
{{#if @plugin.hasSettings}}
|
||||
<LinkTo
|
||||
class="btn-default btn btn-icon-text"
|
||||
@route="adminSiteSettingsCategory"
|
||||
@model={{@plugin.settingCategoryName}}
|
||||
@query={{hash filter=(concat "plugin:" @plugin.name)}}
|
||||
data-plugin-setting-button={{@plugin.name}}
|
||||
>
|
||||
{{icon "cog"}}
|
||||
{{i18n "admin.plugins.change_settings_short"}}
|
||||
</LinkTo>
|
||||
<PluginOutlet
|
||||
@name="admin-plugin-list-item-settings"
|
||||
@outletArgs={{hash plugin=@plugin}}
|
||||
>
|
||||
{{#if this.showPluginSettingsButton}}
|
||||
{{#if @plugin.useNewShowRoute}}
|
||||
<LinkTo
|
||||
class="btn btn-text btn-small"
|
||||
@route="adminPlugins.show"
|
||||
@model={{@plugin}}
|
||||
@disabled={{this.disablePluginSettingsButton}}
|
||||
title={{this.settingsButtonTitle}}
|
||||
data-plugin-setting-button={{@plugin.name}}
|
||||
>
|
||||
{{i18n "admin.plugins.change_settings_short"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
class="btn btn-text btn-small"
|
||||
@route="adminSiteSettingsCategory"
|
||||
@model={{@plugin.settingCategoryName}}
|
||||
@query={{hash filter=(concat "plugin:" @plugin.name)}}
|
||||
@disabled={{this.disablePluginSettingsButton}}
|
||||
title={{this.settingsButtonTitle}}
|
||||
data-plugin-setting-button={{@plugin.name}}
|
||||
>
|
||||
{{i18n "admin.plugins.change_settings_short"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class AdminReports extends Component {
|
||||
@service siteSettings;
|
||||
@tracked reports = null;
|
||||
@tracked filter = "";
|
||||
@tracked isLoading = false;
|
||||
|
||||
@bind
|
||||
loadReports() {
|
||||
this.isLoading = true;
|
||||
ajax("/admin/reports")
|
||||
.then((json) => {
|
||||
this.reports = json.reports;
|
||||
})
|
||||
.finally(() => (this.isLoading = false));
|
||||
}
|
||||
|
||||
get filteredReports() {
|
||||
if (!this.reports) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let filteredReports = this.reports;
|
||||
if (this.filter) {
|
||||
const lowerCaseFilter = this.filter.toLowerCase();
|
||||
filteredReports = filteredReports.filter((report) => {
|
||||
return (
|
||||
(report.title || "").toLowerCase().includes(lowerCaseFilter) ||
|
||||
(report.description || "").toLowerCase().includes(lowerCaseFilter)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenReports = (this.siteSettings.dashboard_hidden_reports || "")
|
||||
.split("|")
|
||||
.filter(Boolean);
|
||||
filteredReports = filteredReports.filter(
|
||||
(report) => !hiddenReports.includes(report.type)
|
||||
);
|
||||
|
||||
return filteredReports;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div {{didInsert this.loadReports}}>
|
||||
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
|
||||
<div class="admin-reports-header">
|
||||
<h2>{{i18n "admin.reports.title"}}</h2>
|
||||
<Input
|
||||
class="admin-reports-header__filter"
|
||||
placeholder={{i18n "admin.filter_reports"}}
|
||||
@value={{this.filter}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
{{dIcon "book"}}
|
||||
{{htmlSafe (i18n "admin.reports.meta_doc")}}
|
||||
</div>
|
||||
|
||||
<ul class="admin-reports-list">
|
||||
{{#each this.filteredReports as |report|}}
|
||||
<li class="admin-reports-list__report">
|
||||
<LinkTo @route="adminReports.show" @model={{report.type}}>
|
||||
<h3
|
||||
class="admin-reports-list__report-title"
|
||||
>{{report.title}}</h3>
|
||||
{{#if report.description}}
|
||||
<p class="admin-reports-list__report-description">
|
||||
{{report.description}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class AdminSiteSettingsFilterControls extends Component {
|
||||
@tracked filter = this.args.initialFilter || "";
|
||||
@tracked onlyOverridden = false;
|
||||
@tracked isMenuOpen = false;
|
||||
|
||||
@action
|
||||
clearFilter() {
|
||||
this.filter = "";
|
||||
this.onlyOverridden = false;
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeFilter() {
|
||||
this.args.onChangeFilter({
|
||||
filter: this.filter,
|
||||
onlyOverridden: this.onlyOverridden,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeFilterInput(event) {
|
||||
this.filter = event.target.value;
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
onToggleOverridden(event) {
|
||||
this.onlyOverridden = event.target.checked;
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
runInitialFilter() {
|
||||
if (this.args.initialFilter !== this.filter) {
|
||||
this.filter = this.args.initialFilter;
|
||||
}
|
||||
this.onChangeFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
this.args.onToggleMenu();
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="admin-controls admin-site-settings-filter-controls"
|
||||
{{didInsert this.runInitialFilter}}
|
||||
{{didUpdate this.runInitialFilter @initialFilter}}
|
||||
>
|
||||
<div class="controls">
|
||||
<div class="inline-form">
|
||||
{{#if @showMenu}}
|
||||
<DButton
|
||||
@action={{this.toggleMenu}}
|
||||
@icon={{if this.isMenuOpen "times" "bars"}}
|
||||
class="menu-toggle"
|
||||
/>
|
||||
{{/if}}
|
||||
<input
|
||||
{{on "input" this.onChangeFilterInput}}
|
||||
id="setting-filter"
|
||||
class="no-blur admin-site-settings-filter-controls__input"
|
||||
placeholder={{i18n "type_to_filter"}}
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
value={{this.filter}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.clearFilter}}
|
||||
@label="admin.site_settings.clear_filter"
|
||||
id="clear-filter"
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search controls">
|
||||
<label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.onlyOverridden}}
|
||||
class="toggle-overridden"
|
||||
id="setting-filter-toggle-overridden"
|
||||
{{on "click" this.onToggleOverridden}}
|
||||
/>
|
||||
{{i18n "admin.settings.show_overriden"}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -52,7 +52,7 @@ export default class AdminThemeEditor extends Component {
|
|||
if (fieldName && fieldName === "color_definitions") {
|
||||
const example =
|
||||
":root {\n" +
|
||||
" --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\n" +
|
||||
" --mytheme-tertiary-or-highlight: #{dark-light-choose($tertiary, $highlight)};\n" +
|
||||
"}";
|
||||
|
||||
return I18n.t("admin.customize.theme.color_definitions.placeholder", {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<ComboBox
|
||||
@content={{this.fieldTypes}}
|
||||
@value={{this.buffered.field_type}}
|
||||
@onChange={{action (mut this.buffered.field_type)}}
|
||||
@onChange={{fn (mut this.buffered.field_type)}}
|
||||
/>
|
||||
</AdminFormRow>
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
<Input
|
||||
@value={{this.buffered.description}}
|
||||
class="user-field-desc"
|
||||
maxlength="255"
|
||||
maxlength="1000"
|
||||
/>
|
||||
</AdminFormRow>
|
||||
|
||||
|
@ -30,29 +30,74 @@
|
|||
</AdminFormRow>
|
||||
{{/if}}
|
||||
|
||||
<AdminFormRow @wrapLabel="true" @type="checkbox">
|
||||
<Input @type="checkbox" @checked={{this.buffered.editable}} />
|
||||
<span>{{i18n "admin.user_fields.editable.title"}}</span>
|
||||
<AdminFormRow @label="admin.user_fields.requirement.title">
|
||||
<label class="optional">
|
||||
<RadioButton
|
||||
@value="optional"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<span>{{i18n "admin.user_fields.requirement.optional.title"}}</span>
|
||||
</label>
|
||||
|
||||
<label class="for_all_users">
|
||||
<RadioButton
|
||||
@value="for_all_users"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<div class="label-text">
|
||||
<span>{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.title"
|
||||
}}</span>
|
||||
<div class="description">{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.description"
|
||||
}}</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="on_signup">
|
||||
<RadioButton
|
||||
@value="on_signup"
|
||||
@name="requirement"
|
||||
@selection={{this.buffered.requirement}}
|
||||
@onChange={{action "changeRequirementType"}}
|
||||
/>
|
||||
<div class="label-text">
|
||||
<span>{{i18n "admin.user_fields.requirement.on_signup.title"}}</span>
|
||||
<div class="description">{{i18n
|
||||
"admin.user_fields.requirement.on_signup.description"
|
||||
}}</div>
|
||||
</div>
|
||||
</label>
|
||||
</AdminFormRow>
|
||||
|
||||
<AdminFormRow @wrapLabel="true" @type="checkbox">
|
||||
<Input @type="checkbox" @checked={{this.buffered.required}} />
|
||||
<span>{{i18n "admin.user_fields.required.title"}}</span>
|
||||
</AdminFormRow>
|
||||
<AdminFormRow @label="admin.user_fields.preferences">
|
||||
<label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.buffered.editable}}
|
||||
disabled={{this.editableDisabled}}
|
||||
/>
|
||||
<span>{{i18n "admin.user_fields.editable.title"}}</span>
|
||||
</label>
|
||||
|
||||
<AdminFormRow @wrapLabel="true" @type="checkbox">
|
||||
<Input @type="checkbox" @checked={{this.buffered.show_on_profile}} />
|
||||
<span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
|
||||
</AdminFormRow>
|
||||
<label>
|
||||
<Input @type="checkbox" @checked={{this.buffered.show_on_profile}} />
|
||||
<span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
|
||||
</label>
|
||||
|
||||
<AdminFormRow @wrapLabel="true" @type="checkbox">
|
||||
<Input @type="checkbox" @checked={{this.buffered.show_on_user_card}} />
|
||||
<span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
|
||||
</AdminFormRow>
|
||||
<label>
|
||||
<Input @type="checkbox" @checked={{this.buffered.show_on_user_card}} />
|
||||
<span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
|
||||
</label>
|
||||
|
||||
<AdminFormRow @wrapLabel="true" @type="checkbox">
|
||||
<Input @type="checkbox" @checked={{this.buffered.searchable}} />
|
||||
<span>{{i18n "admin.user_fields.searchable.title"}}</span>
|
||||
<label>
|
||||
<Input @type="checkbox" @checked={{this.buffered.searchable}} />
|
||||
<span>{{i18n "admin.user_fields.searchable.title"}}</span>
|
||||
</label>
|
||||
</AdminFormRow>
|
||||
|
||||
<PluginOutlet
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { Promise } from "rsvp";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n, propertyEqual } from "discourse/lib/computed";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
|
@ -12,6 +13,7 @@ import UserField from "admin/models/user-field";
|
|||
|
||||
export default Component.extend(bufferedProperty("userField"), {
|
||||
adminCustomUserFields: service(),
|
||||
dialog: service(),
|
||||
|
||||
tagName: "",
|
||||
isEditing: false,
|
||||
|
@ -44,16 +46,13 @@ export default Component.extend(bufferedProperty("userField"), {
|
|||
},
|
||||
|
||||
@discourseComputed(
|
||||
"userField.{editable,required,show_on_profile,show_on_user_card,searchable}"
|
||||
"userField.{editable,show_on_profile,show_on_user_card,searchable}"
|
||||
)
|
||||
flags(userField) {
|
||||
const ret = [];
|
||||
if (userField.editable) {
|
||||
ret.push(I18n.t("admin.user_fields.editable.enabled"));
|
||||
}
|
||||
if (userField.required) {
|
||||
ret.push(I18n.t("admin.user_fields.required.enabled"));
|
||||
}
|
||||
if (userField.show_on_profile) {
|
||||
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
|
||||
}
|
||||
|
@ -67,14 +66,35 @@ export default Component.extend(bufferedProperty("userField"), {
|
|||
return ret.join(", ");
|
||||
},
|
||||
|
||||
@discourseComputed("buffered.requirement")
|
||||
editableDisabled(requirement) {
|
||||
return requirement === "for_all_users";
|
||||
},
|
||||
|
||||
@action
|
||||
save() {
|
||||
changeRequirementType(requirement) {
|
||||
this.buffered.set("requirement", requirement);
|
||||
this.buffered.set("editable", requirement === "for_all_users");
|
||||
},
|
||||
|
||||
async _confirmChanges() {
|
||||
return new Promise((resolve) => {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("admin.user_fields.requirement.confirmation"),
|
||||
didCancel: () => resolve(false),
|
||||
didConfirm: () => resolve(true),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
async save() {
|
||||
const attrs = this.buffered.getProperties(
|
||||
"name",
|
||||
"description",
|
||||
"field_type",
|
||||
"editable",
|
||||
"required",
|
||||
"requirement",
|
||||
"show_on_profile",
|
||||
"show_on_user_card",
|
||||
"searchable",
|
||||
|
@ -82,6 +102,16 @@ export default Component.extend(bufferedProperty("userField"), {
|
|||
...this.adminCustomUserFields.additionalProperties
|
||||
);
|
||||
|
||||
let confirm = true;
|
||||
|
||||
if (attrs.requirement === "for_all_users") {
|
||||
confirm = await this._confirmChanges();
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.userField
|
||||
.save(attrs)
|
||||
.then(() => {
|
||||
|
|
|
@ -17,4 +17,7 @@
|
|||
<span class="case-sensitive">{{i18n
|
||||
"admin.watched_words.case_sensitive"
|
||||
}}</span>
|
||||
{{/if}}
|
||||
{{#if this.isHtml}}
|
||||
<span class="html">{{i18n "admin.watched_words.html"}}</span>
|
||||
{{/if}}
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { alias, equal } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
@ -11,12 +11,10 @@ export default class AdminWatchedWord extends Component {
|
|||
@service dialog;
|
||||
|
||||
@equal("actionKey", "replace") isReplace;
|
||||
|
||||
@equal("actionKey", "tag") isTag;
|
||||
|
||||
@equal("actionKey", "link") isLink;
|
||||
|
||||
@alias("word.case_sensitive") isCaseSensitive;
|
||||
@alias("word.html") isHtml;
|
||||
|
||||
@discourseComputed("word.replacement")
|
||||
tags(replacement) {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
|
||||
@tagName("")
|
||||
export default class CancelLink extends Component {}
|
|
@ -1,7 +1,6 @@
|
|||
import { and, not } from "truth-helpers";
|
||||
import CookText from "discourse/components/cook-text";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import not from "truth-helpers/helpers/not";
|
||||
|
||||
const DashboardNewFeatureItem = <template>
|
||||
<div class="admin-new-feature-item">
|
||||
|
@ -15,6 +14,11 @@ const DashboardNewFeatureItem = <template>
|
|||
<h3>
|
||||
{{@item.title}}
|
||||
</h3>
|
||||
{{#if @item.discourse_version}}
|
||||
<div class="admin-new-feature-item__new-feature-version">
|
||||
{{@item.discourse_version}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if @item.screenshot_url}}
|
||||
|
|
|
@ -1,21 +1,42 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import DashboardNewFeatureItem from "admin/components/dashboard-new-feature-item";
|
||||
|
||||
export default class DashboardNewFeatures extends Component {
|
||||
@service currentUser;
|
||||
|
||||
@tracked newFeatures = null;
|
||||
@tracked groupedNewFeatures = null;
|
||||
@tracked isLoaded = false;
|
||||
|
||||
@bind
|
||||
loadNewFeatures() {
|
||||
ajax("/admin/dashboard/whats-new.json")
|
||||
.then((json) => {
|
||||
this.newFeatures = json.new_features;
|
||||
const items = json.new_features.reduce((acc, feature) => {
|
||||
const key = moment(feature.released_at || feature.created_at).format(
|
||||
"YYYY-MM"
|
||||
);
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(feature);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.groupedNewFeatures = Object.keys(items).map((date) => {
|
||||
return {
|
||||
date: moment
|
||||
.tz(date, this.currentUser.user_option.timezone)
|
||||
.format("MMMM YYYY"),
|
||||
features: items[date],
|
||||
};
|
||||
});
|
||||
this.isLoaded = true;
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -24,10 +45,14 @@ export default class DashboardNewFeatures extends Component {
|
|||
}
|
||||
|
||||
<template>
|
||||
<div class="section-body" {{didInsert this.loadNewFeatures}}>
|
||||
{{#if this.newFeatures}}
|
||||
{{#each this.newFeatures as |feature|}}
|
||||
<DashboardNewFeatureItem @item={{feature}} />
|
||||
<div class="admin-config-area" {{didInsert this.loadNewFeatures}}>
|
||||
{{#if this.groupedNewFeatures}}
|
||||
{{#each this.groupedNewFeatures as |groupedFeatures|}}
|
||||
<AdminConfigAreaCard @translatedHeading={{groupedFeatures.date}}>
|
||||
{{#each groupedFeatures.features as |feature|}}
|
||||
<DashboardNewFeatureItem @item={{feature}} />
|
||||
{{/each}}
|
||||
</AdminConfigAreaCard>
|
||||
{{/each}}
|
||||
{{else if this.isLoaded}}
|
||||
{{htmlSafe
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PeriodChooser from "select-kit/components/period-chooser";
|
||||
import CustomDateRangeModal from "../components/modal/custom-date-range";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { reads } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
|
|
|
@ -21,11 +21,30 @@
|
|||
<td class="editing-input">
|
||||
<div class="label">{{i18n "admin.embedding.category"}}</div>
|
||||
<CategoryChooser
|
||||
@value={{this.categoryId}}
|
||||
@onChange={{action (mut this.categoryId)}}
|
||||
@value={{this.category.id}}
|
||||
@onChangeCategory={{fn (mut this.category)}}
|
||||
class="small"
|
||||
/>
|
||||
</td>
|
||||
<td class="editing-input">
|
||||
<div class="label">{{i18n "admin.embedding.tags"}}</div>
|
||||
<TagChooser
|
||||
@tags={{this.tags}}
|
||||
@everyTag={{true}}
|
||||
@excludeSynonyms={{true}}
|
||||
@unlimitedTagCount={{true}}
|
||||
@onChange={{fn (mut this.tags)}}
|
||||
@options={{hash filterPlaceholder="category.tags_placeholder"}}
|
||||
/>
|
||||
</td>
|
||||
<td class="editing-input">
|
||||
<div class="label">{{i18n "admin.embedding.user"}}</div>
|
||||
<UserChooser
|
||||
@value={{this.user}}
|
||||
@onChange={{action "onUserChange"}}
|
||||
@options={{hash maximum=1 excludeCurrentUser=false}}
|
||||
/>
|
||||
</td>
|
||||
<td class="editing-controls">
|
||||
<DButton
|
||||
@icon="check"
|
||||
|
@ -53,7 +72,13 @@
|
|||
</td>
|
||||
<td>
|
||||
<div class="label">{{i18n "admin.embedding.category"}}</div>
|
||||
{{category-badge this.host.category allowUncategorized=true}}
|
||||
{{category-badge this.category allowUncategorized=true}}
|
||||
</td>
|
||||
<td>
|
||||
{{this.tags}}
|
||||
</td>
|
||||
<td>
|
||||
{{this.user}}
|
||||
</td>
|
||||
<td class="controls">
|
||||
<DButton @icon="pencil-alt" @action={{this.edit}} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { or } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
@ -18,6 +18,8 @@ export default class EmbeddableHost extends Component.extend(
|
|||
editToggled = false;
|
||||
categoryId = null;
|
||||
category = null;
|
||||
tags = null;
|
||||
user = null;
|
||||
|
||||
@or("host.isNew", "editToggled") editing;
|
||||
|
||||
|
@ -28,7 +30,9 @@ export default class EmbeddableHost extends Component.extend(
|
|||
const categoryId = host.category_id || this.site.uncategorized_category_id;
|
||||
const category = Category.findById(categoryId);
|
||||
|
||||
host.set("category", category);
|
||||
this.set("category", category);
|
||||
this.set("tags", host.tags || []);
|
||||
this.set("user", host.user);
|
||||
}
|
||||
|
||||
@discourseComputed("buffered.host", "host.isSaving")
|
||||
|
@ -38,10 +42,12 @@ export default class EmbeddableHost extends Component.extend(
|
|||
|
||||
@action
|
||||
edit() {
|
||||
this.set("categoryId", this.get("host.category.id"));
|
||||
this.set("editToggled", true);
|
||||
}
|
||||
|
||||
@action
|
||||
onUserChange(user) {
|
||||
this.set("user", user);
|
||||
}
|
||||
@action
|
||||
save() {
|
||||
if (this.cantSave) {
|
||||
|
@ -53,14 +59,16 @@ export default class EmbeddableHost extends Component.extend(
|
|||
"allowed_paths",
|
||||
"class_name"
|
||||
);
|
||||
props.category_id = this.categoryId;
|
||||
props.category_id = this.category.id;
|
||||
props.tags = this.tags;
|
||||
props.user =
|
||||
Array.isArray(this.user) && this.user.length > 0 ? this.user[0] : null;
|
||||
|
||||
const host = this.host;
|
||||
|
||||
host
|
||||
.save(props)
|
||||
.then(() => {
|
||||
host.set("category", Category.findById(this.categoryId));
|
||||
this.set("editToggled", false);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
import FormTemplateValidationOptionsModal from "admin/components/modal/form-template-validation-options";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { classNames } from "@ember-decorators/component";
|
||||
import $ from "jquery";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class BadgePreview extends Component {
|
|||
}
|
||||
|
||||
get queryPlanHtml() {
|
||||
let output = `<pre class="badge-query-plan">`;
|
||||
let output = `<pre>`;
|
||||
this.args.model.badge.query_plan.forEach((linehash) => {
|
||||
output += escapeExpression(linehash["QUERY PLAN"]);
|
||||
output += "<br>";
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<ComboBox
|
||||
@content={{@model.baseColorSchemes}}
|
||||
@value={{this.selectedBaseThemeId}}
|
||||
@onChange={{action (mut this.selectedBaseThemeId)}}
|
||||
@onChange={{fn (mut this.selectedBaseThemeId)}}
|
||||
@valueProperty="base_scheme_id"
|
||||
/>
|
||||
</:body>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Component from "@glimmer/component";
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { A } from "@ember/array";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import I18n from "discourse-i18n";
|
||||
|
|
|
@ -31,138 +31,148 @@
|
|||
</div>
|
||||
{{/unless}}
|
||||
<div class="install-theme-content">
|
||||
{{#if this.popular}}
|
||||
<div class="popular-theme-items">
|
||||
{{#each this.themes as |theme|}}
|
||||
<div class="popular-theme-item" data-name={{theme.name}}>
|
||||
<div class="popular-theme-name">
|
||||
<a
|
||||
href={{theme.meta_url}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{#if theme.component}}
|
||||
{{d-icon
|
||||
"puzzle-piece"
|
||||
title="admin.customize.theme.component"
|
||||
}}
|
||||
<ConditionalLoadingSection
|
||||
@isLoading={{this.loading}}
|
||||
@title={{i18n "admin.customize.theme.installing_message"}}
|
||||
>
|
||||
{{#if this.popular}}
|
||||
<div class="popular-theme-items">
|
||||
{{#each this.themes as |theme|}}
|
||||
<div class="popular-theme-item" data-name={{theme.name}}>
|
||||
<div class="popular-theme-name">
|
||||
<a
|
||||
href={{theme.meta_url}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{#if theme.component}}
|
||||
{{d-icon
|
||||
"puzzle-piece"
|
||||
title="admin.customize.theme.component"
|
||||
}}
|
||||
{{/if}}
|
||||
{{theme.name}}
|
||||
</a>
|
||||
<div class="popular-theme-description">
|
||||
{{theme.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popular-theme-buttons">
|
||||
{{#if theme.installed}}
|
||||
<span>{{i18n "admin.customize.theme.installed"}}</span>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn-small"
|
||||
@label="admin.customize.theme.install"
|
||||
@disabled={{this.installDisabled}}
|
||||
@icon="upload"
|
||||
@action={{fn this.installThemeFromList theme.value}}
|
||||
/>
|
||||
|
||||
{{#if theme.preview}}
|
||||
<a
|
||||
href={{theme.preview}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{d-icon "desktop"}}
|
||||
{{i18n "admin.customize.theme.preview"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{theme.name}}
|
||||
</a>
|
||||
<div class="popular-theme-description">
|
||||
{{theme.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popular-theme-buttons">
|
||||
{{#if theme.installed}}
|
||||
<span>{{i18n "admin.customize.theme.installed"}}</span>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn-small"
|
||||
@label="admin.customize.theme.install"
|
||||
@disabled={{this.installDisabled}}
|
||||
@icon="upload"
|
||||
@action={{fn this.installThemeFromList theme.value}}
|
||||
/>
|
||||
|
||||
{{#if theme.preview}}
|
||||
<a
|
||||
href={{theme.preview}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{d-icon "desktop"}}
|
||||
{{i18n "admin.customize.theme.preview"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.local}}
|
||||
<div class="inputs">
|
||||
<input
|
||||
{{on "change" this.uploadLocaleFile}}
|
||||
type="file"
|
||||
id="file-input"
|
||||
accept=".dcstyle.json,application/json,.tar.gz,application/x-gzip,.zip,application/zip"
|
||||
/>
|
||||
<br />
|
||||
<span class="description">
|
||||
{{i18n "admin.customize.theme.import_file_tip"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.remote}}
|
||||
<div class="inputs">
|
||||
<div class="repo">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.import_web_tip"}}
|
||||
</div>
|
||||
<Input
|
||||
@value={{this.uploadUrl}}
|
||||
placeholder={{this.urlPlaceholder}}
|
||||
/>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.local}}
|
||||
<div class="inputs">
|
||||
<input
|
||||
{{on "change" this.uploadLocaleFile}}
|
||||
type="file"
|
||||
id="file-input"
|
||||
accept=".dcstyle.json,application/json,.tar.gz,application/x-gzip,.zip,application/zip"
|
||||
/>
|
||||
<br />
|
||||
<span class="description">
|
||||
{{i18n "admin.customize.theme.import_file_tip"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.remote}}
|
||||
<div class="inputs">
|
||||
<div class="repo">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.import_web_tip"}}
|
||||
</div>
|
||||
<Input
|
||||
@value={{this.uploadUrl}}
|
||||
placeholder={{this.urlPlaceholder}}
|
||||
<DButton
|
||||
class="btn-small advanced-repo"
|
||||
@action={{this.toggleAdvanced}}
|
||||
@label="admin.customize.theme.import_web_advanced"
|
||||
/>
|
||||
{{#if this.advancedVisible}}
|
||||
<div class="branch">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.remote_branch"}}
|
||||
</div>
|
||||
<Input @value={{this.branch}} placeholder="main" />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.showPublicKey}}
|
||||
<div class="public-key">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.public_key"}}
|
||||
</div>
|
||||
<div class="public-key-text-wrapper">
|
||||
<Textarea
|
||||
class="public-key-value"
|
||||
readonly={{true}}
|
||||
@value={{this.publicKey}}
|
||||
{{did-insert this.generatePublicKey}}
|
||||
/>
|
||||
<CopyButton @selector="textarea.public-key-value" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.create}}
|
||||
<div class="inputs">
|
||||
<div class="label">{{i18n
|
||||
"admin.customize.theme.create_name"
|
||||
}}</div>
|
||||
<Input @value={{this.name}} placeholder={{this.placeholder}} />
|
||||
<div class="label">{{i18n
|
||||
"admin.customize.theme.create_type"
|
||||
}}</div>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@content={{this.createTypes}}
|
||||
@value={{this.selectedType}}
|
||||
@onChange={{this.updateSelectedType}}
|
||||
/>
|
||||
</div>
|
||||
<DButton
|
||||
class="btn-small advanced-repo"
|
||||
@action={{this.toggleAdvanced}}
|
||||
@label="admin.customize.theme.import_web_advanced"
|
||||
/>
|
||||
{{#if this.advancedVisible}}
|
||||
<div class="branch">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.remote_branch"}}
|
||||
</div>
|
||||
<Input @value={{this.branch}} placeholder="main" />
|
||||
{{/if}}
|
||||
{{#if this.directRepoInstall}}
|
||||
<div class="repo">
|
||||
<div class="label">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"admin.customize.theme.direct_install_tip"
|
||||
name=this.uploadName
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.showPublicKey}}
|
||||
<div class="public-key">
|
||||
<div class="label">
|
||||
{{i18n "admin.customize.theme.public_key"}}
|
||||
</div>
|
||||
<div class="public-key-text-wrapper">
|
||||
<Textarea
|
||||
class="public-key-value"
|
||||
readonly={{true}}
|
||||
@value={{this.publicKey}}
|
||||
{{did-insert this.generatePublicKey}}
|
||||
/>
|
||||
<CopyButton @selector="textarea.public-key-value" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.create}}
|
||||
<div class="inputs">
|
||||
<div class="label">{{i18n "admin.customize.theme.create_name"}}</div>
|
||||
<Input @value={{this.name}} placeholder={{this.placeholder}} />
|
||||
<div class="label">{{i18n "admin.customize.theme.create_type"}}</div>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@content={{this.createTypes}}
|
||||
@value={{this.selectedType}}
|
||||
@onChange={{this.updateSelectedType}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.directRepoInstall}}
|
||||
<div class="repo">
|
||||
<div class="label">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"admin.customize.theme.direct_install_tip" name=this.uploadName
|
||||
)
|
||||
}}
|
||||
<pre><code>{{this.uploadUrl}}</code></pre>
|
||||
</div>
|
||||
<pre><code>{{this.uploadUrl}}</code></pre>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ConditionalLoadingSection>
|
||||
</div>
|
||||
</:body>
|
||||
<:footer>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { POPULAR_THEMES } from "discourse-common/lib/popular-themes";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
@label="admin.user.suspend_duration"
|
||||
@clearable={{false}}
|
||||
@input={{this.penalizeUntil}}
|
||||
@onChangeInput={{action (mut this.penalizeUntil)}}
|
||||
@onChangeInput={{fn (mut this.penalizeUntil)}}
|
||||
class="suspend-until"
|
||||
/>
|
||||
{{else if (eq @model.penaltyType "silence")}}
|
||||
|
@ -20,7 +20,7 @@
|
|||
@label="admin.user.silence_duration"
|
||||
@clearable={{false}}
|
||||
@input={{this.penalizeUntil}}
|
||||
@onChangeInput={{action (mut this.penalizeUntil)}}
|
||||
@onChangeInput={{fn (mut this.penalizeUntil)}}
|
||||
class="silence-until"
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class StartBackup extends Component {
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
{{#each this.images as |image|}}
|
||||
<a href class="selectable-avatar" {{on "click" (fn this.remove image)}}>
|
||||
{{bound-avatar-template image "huge"}}
|
||||
<span class="selectable-avatar__remove">{{d-icon
|
||||
"times-circle"
|
||||
}}</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.site_settings.uploaded_image_list.empty"}}</p>
|
||||
|
|
|
@ -9,7 +9,8 @@ export default class UploadedImageList extends Component {
|
|||
: [];
|
||||
|
||||
@action
|
||||
remove(url) {
|
||||
remove(url, event) {
|
||||
event.preventDefault();
|
||||
this.images.removeObject(url);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import {
|
||||
createWatchedWordRegExp,
|
||||
toWatchedWord,
|
||||
} from "discourse-common/utils/watched-words";
|
||||
|
||||
export default class WatchedWordTest extends Component {
|
||||
@tracked value;
|
||||
|
@ -31,7 +27,10 @@ export default class WatchedWordTest extends Component {
|
|||
if (this.isReplace || this.isLink) {
|
||||
const matches = [];
|
||||
this.args.model.watchedWord.words.forEach((word) => {
|
||||
const regexp = createWatchedWordRegExp(word);
|
||||
const regexp = new RegExp(
|
||||
word.regexp,
|
||||
word.case_sensitive ? "gu" : "gui"
|
||||
);
|
||||
let match;
|
||||
|
||||
while ((match = regexp.exec(this.value)) !== null) {
|
||||
|
@ -42,38 +41,44 @@ export default class WatchedWordTest extends Component {
|
|||
}
|
||||
});
|
||||
return matches;
|
||||
} else if (this.isTag) {
|
||||
const matches = {};
|
||||
}
|
||||
|
||||
if (this.isTag) {
|
||||
const matches = new Map();
|
||||
this.args.model.watchedWord.words.forEach((word) => {
|
||||
const regexp = createWatchedWordRegExp(word);
|
||||
const regexp = new RegExp(
|
||||
word.regexp,
|
||||
word.case_sensitive ? "gu" : "gui"
|
||||
);
|
||||
let match;
|
||||
|
||||
while ((match = regexp.exec(this.value)) !== null) {
|
||||
if (!matches[match[1]]) {
|
||||
matches[match[1]] = new Set();
|
||||
if (!matches.has(match[1])) {
|
||||
matches.set(match[1], new Set());
|
||||
}
|
||||
|
||||
let tags = matches[match[1]];
|
||||
word.replacement.split(",").forEach((tag) => {
|
||||
tags.add(tag);
|
||||
});
|
||||
const tags = matches.get(match[1]);
|
||||
word.replacement.split(",").forEach((tag) => tags.add(tag));
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(matches).map((entry) => ({
|
||||
match: entry[0],
|
||||
tags: Array.from(entry[1]),
|
||||
return Array.from(matches, ([match, tagsSet]) => ({
|
||||
match,
|
||||
tags: Array.from(tagsSet),
|
||||
}));
|
||||
} else {
|
||||
let matches = [];
|
||||
this.args.model.watchedWord.compiledRegularExpression.forEach(
|
||||
(regexp) => {
|
||||
const wordRegexp = createWatchedWordRegExp(toWatchedWord(regexp));
|
||||
matches.push(...(this.value.match(wordRegexp) || []));
|
||||
}
|
||||
}
|
||||
|
||||
let matches = [];
|
||||
this.args.model.watchedWord.compiledRegularExpression.forEach((entry) => {
|
||||
const [regexp, options] = Object.entries(entry)[0];
|
||||
const wordRegexp = new RegExp(
|
||||
regexp,
|
||||
options.case_sensitive ? "gu" : "gui"
|
||||
);
|
||||
|
||||
return matches;
|
||||
}
|
||||
matches.push(...(this.value.match(wordRegexp) || []));
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<ComboBox
|
||||
@content={{this.permalinkTypes}}
|
||||
@value={{this.permalinkType}}
|
||||
@onChange={{action (mut this.permalinkType)}}
|
||||
@onChange={{fn (mut this.permalinkType)}}
|
||||
class="permalink-type"
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import { fmt } from "discourse/lib/computed";
|
||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
|
@ -28,6 +28,7 @@ export default class PermalinkForm extends Component {
|
|||
{ id: "category_id", name: I18n.t("admin.permalink.category_id") },
|
||||
{ id: "tag_name", name: I18n.t("admin.permalink.tag_name") },
|
||||
{ id: "external_url", name: I18n.t("admin.permalink.external_url") },
|
||||
{ id: "user_id", name: I18n.t("admin.permalink.user_id") },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class PluginCommitHash extends Component {
|
||||
get shortCommitHash() {
|
||||
return this.args.plugin.commitHash?.slice(0, 7);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @plugin.commitHash}}
|
||||
<a
|
||||
href={{@plugin.commitUrl}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="current commit-hash"
|
||||
title={{@plugin.commitHash}}
|
||||
>{{this.shortCommitHash}}</a>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{{#if this.commitHash}}
|
||||
<a
|
||||
href={{@plugin.commitUrl}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="current commit-hash"
|
||||
title={{this.commitHash}}
|
||||
>{{this.shortCommitHash}}</a>
|
||||
{{/if}}
|
|
@ -1,11 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class PluginCommitHash extends Component {
|
||||
get shortCommitHash() {
|
||||
return this.commitHash?.slice(0, 7);
|
||||
}
|
||||
|
||||
get commitHash() {
|
||||
return this.args.plugin.commitHash;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
@onChange={{this.onChange}}
|
||||
@options={{hash
|
||||
allowAny=this.filter.allow_any
|
||||
autoInsertNoneItem=this.filter.auto_insert_none_item
|
||||
filterable=true
|
||||
none="admin.dashboard.reports.groups"
|
||||
}}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
@onChange={{this.onChange}}
|
||||
@options={{hash
|
||||
allowAny=this.filter.allow_any
|
||||
autoInsertNoneItem=this.filter.auto_insert_none_item
|
||||
filterable=true
|
||||
none="admin.dashboard.report_filter_any"
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { gt } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import I18n from "discourse-i18n";
|
||||
import Tree from "admin/components/schema-theme-setting/editor/tree";
|
||||
import FieldInput from "admin/components/schema-theme-setting/field";
|
||||
|
||||
export default class SchemaThemeSettingNewEditor extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked history = [];
|
||||
@tracked activeIndex = 0;
|
||||
@tracked activeDataPaths = [];
|
||||
@tracked activeSchemaPaths = [];
|
||||
@tracked saveButtonDisabled = false;
|
||||
@tracked validationErrorMessage;
|
||||
inputFieldObserver = new Map();
|
||||
|
||||
data = cloneJSON(this.args.setting.value);
|
||||
schema = this.args.setting.objects_schema;
|
||||
|
||||
@action
|
||||
onChildClick(index, propertyName, parentNodeIndex) {
|
||||
this.history.pushObject({
|
||||
dataPaths: [...this.activeDataPaths],
|
||||
schemaPaths: [...this.activeSchemaPaths],
|
||||
index: this.activeIndex,
|
||||
});
|
||||
|
||||
this.activeIndex = index;
|
||||
this.activeDataPaths.pushObjects([parentNodeIndex, propertyName]);
|
||||
this.activeSchemaPaths.pushObject(propertyName);
|
||||
this.inputFieldObserver.clear();
|
||||
}
|
||||
|
||||
@action
|
||||
updateIndex(index) {
|
||||
this.activeIndex = index;
|
||||
}
|
||||
|
||||
@action
|
||||
generateSchemaTitle(object, schema, index) {
|
||||
let title;
|
||||
|
||||
if (schema.properties[schema.identifier]?.type === "categories") {
|
||||
title = this.activeData[index][schema.identifier]
|
||||
?.map((categoryId) => {
|
||||
return this.args.setting.metadata.categories[categoryId].name;
|
||||
})
|
||||
.join(", ");
|
||||
} else {
|
||||
title = object[schema.identifier];
|
||||
}
|
||||
|
||||
return title || `${schema.name} ${index + 1}`;
|
||||
}
|
||||
|
||||
get backButtonText() {
|
||||
if (this.history.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastHistory = this.history[this.history.length - 1];
|
||||
|
||||
return I18n.t("admin.customize.theme.schema.back_button", {
|
||||
name: this.generateSchemaTitle(
|
||||
this.#resolveDataFromPaths(lastHistory.dataPaths)[lastHistory.index],
|
||||
this.#resolveSchemaFromPaths(lastHistory.schemaPaths),
|
||||
lastHistory.index
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
get activeData() {
|
||||
return this.#resolveDataFromPaths(this.activeDataPaths);
|
||||
}
|
||||
|
||||
#resolveDataFromPaths(paths) {
|
||||
if (paths.length === 0) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
let data = this.data;
|
||||
|
||||
paths.forEach((path) => {
|
||||
data = data[path];
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
get activeSchema() {
|
||||
return this.#resolveSchemaFromPaths(this.activeSchemaPaths);
|
||||
}
|
||||
|
||||
#resolveSchemaFromPaths(paths) {
|
||||
if (paths.length === 0) {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
let schema = this.schema;
|
||||
|
||||
paths.forEach((path) => {
|
||||
schema = schema.properties[path].schema;
|
||||
});
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
@action
|
||||
registerInputFieldObserver(index, callback) {
|
||||
this.inputFieldObserver[index] = callback;
|
||||
}
|
||||
|
||||
@action
|
||||
unregisterInputFieldObserver(index) {
|
||||
delete this.inputFieldObserver[index];
|
||||
}
|
||||
|
||||
descriptions(fieldName, key) {
|
||||
// The `property_descriptions` metadata is an object with keys in the following format as an example:
|
||||
//
|
||||
// {
|
||||
// some_property.description: <some description>,
|
||||
// some_property.label: <some label>,
|
||||
// some_objects_property.some_other_property.description: <some description>,
|
||||
// some_objects_property.some_other_property.label: <some label>,
|
||||
// }
|
||||
const descriptions = this.args.setting.metadata?.property_descriptions;
|
||||
|
||||
if (!descriptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeSchemaPaths.length > 0) {
|
||||
key = `${this.activeSchemaPaths.join(".")}.${fieldName}.${key}`;
|
||||
} else {
|
||||
key = `${fieldName}.${key}`;
|
||||
}
|
||||
|
||||
return descriptions[key];
|
||||
}
|
||||
|
||||
fieldLabel(fieldName) {
|
||||
return this.descriptions(fieldName, "label") || fieldName;
|
||||
}
|
||||
|
||||
fieldDescription(fieldName) {
|
||||
return this.descriptions(fieldName, "description");
|
||||
}
|
||||
|
||||
get fields() {
|
||||
const list = [];
|
||||
const activeObject = this.activeData[this.activeIndex];
|
||||
|
||||
if (activeObject) {
|
||||
for (const [name, spec] of Object.entries(this.activeSchema.properties)) {
|
||||
if (spec.type === "objects") {
|
||||
continue;
|
||||
}
|
||||
|
||||
list.push({
|
||||
name,
|
||||
spec,
|
||||
value: activeObject[name],
|
||||
description: this.fieldDescription(name),
|
||||
label: this.fieldLabel(name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@action
|
||||
clickBack() {
|
||||
const {
|
||||
dataPaths: lastDataPaths,
|
||||
schemaPaths: lastSchemaPaths,
|
||||
index: lastIndex,
|
||||
} = this.history.popObject();
|
||||
|
||||
this.activeDataPaths = lastDataPaths;
|
||||
this.activeSchemaPaths = lastSchemaPaths;
|
||||
this.activeIndex = lastIndex;
|
||||
this.inputFieldObserver.clear();
|
||||
}
|
||||
|
||||
@action
|
||||
addChildItem(propertyName, parentNodeIndex) {
|
||||
this.activeData[parentNodeIndex][propertyName].pushObject({});
|
||||
|
||||
this.onChildClick(
|
||||
this.activeData[parentNodeIndex][propertyName].length - 1,
|
||||
propertyName,
|
||||
parentNodeIndex
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
addItem() {
|
||||
this.activeData.pushObject({});
|
||||
this.activeIndex = this.activeData.length - 1;
|
||||
}
|
||||
|
||||
@action
|
||||
removeItem() {
|
||||
this.activeData.removeAt(this.activeIndex);
|
||||
|
||||
if (this.activeData.length > 0) {
|
||||
this.activeIndex = Math.max(this.activeIndex - 1, 0);
|
||||
} else if (this.history.length > 0) {
|
||||
this.clickBack();
|
||||
} else {
|
||||
this.activeIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
inputFieldChanged(field, newVal) {
|
||||
this.activeData[this.activeIndex][field.name] = newVal;
|
||||
|
||||
if (field.name === this.activeSchema.identifier) {
|
||||
this.inputFieldObserver[this.activeIndex]();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
saveChanges() {
|
||||
this.saveButtonDisabled = true;
|
||||
|
||||
this.args.setting
|
||||
.updateSetting(this.args.themeId, this.data)
|
||||
.then((result) => {
|
||||
this.args.setting.set("value", result[this.args.setting.setting]);
|
||||
|
||||
this.router.transitionTo(
|
||||
"adminCustomizeThemes.show",
|
||||
this.args.themeId
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||
this.validationErrorMessage = e.jqXHR.responseJSON.errors[0];
|
||||
} else {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
})
|
||||
.finally(() => (this.saveButtonDisabled = false));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="schema-theme-setting-editor">
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-theme-setting-editor__errors">
|
||||
<div class="alert alert-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="schema-theme-setting-editor__wrapper">
|
||||
<div class="schema-theme-setting-editor__navigation">
|
||||
<Tree
|
||||
@data={{this.activeData}}
|
||||
@schema={{this.activeSchema}}
|
||||
@onChildClick={{this.onChildClick}}
|
||||
@clickBack={{this.clickBack}}
|
||||
@backButtonText={{this.backButtonText}}
|
||||
@activeIndex={{this.activeIndex}}
|
||||
@updateIndex={{this.updateIndex}}
|
||||
@addItem={{this.addItem}}
|
||||
@addChildItem={{this.addChildItem}}
|
||||
@generateSchemaTitle={{this.generateSchemaTitle}}
|
||||
@registerInputFieldObserver={{this.registerInputFieldObserver}}
|
||||
@unregisterInputFieldObserver={{this.unregisterInputFieldObserver}}
|
||||
/>
|
||||
|
||||
<div class="schema-theme-setting-editor__footer">
|
||||
<DButton
|
||||
@disabled={{this.saveButtonDisabled}}
|
||||
@action={{this.saveChanges}}
|
||||
@label="save"
|
||||
class="btn-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-theme-setting-editor__fields">
|
||||
{{#each this.fields as |field|}}
|
||||
<FieldInput
|
||||
@name={{field.name}}
|
||||
@value={{field.value}}
|
||||
@spec={{field.spec}}
|
||||
@onValueChange={{fn this.inputFieldChanged field}}
|
||||
@description={{field.description}}
|
||||
@label={{field.label}}
|
||||
@setting={{@setting}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#if (gt this.fields.length 0)}}
|
||||
<DButton
|
||||
@action={{this.removeItem}}
|
||||
@icon="trash-alt"
|
||||
class="btn-danger schema-theme-setting-editor__remove-btn"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { on } from "@ember/modifier";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
|
||||
<template>
|
||||
<li
|
||||
role="link"
|
||||
class="schema-theme-setting-editor__tree-node --child"
|
||||
...attributes
|
||||
{{on "click" @onChildClick}}
|
||||
>
|
||||
<div class="schema-theme-setting-editor__tree-node-text">
|
||||
<span>{{@generateSchemaTitle @object @schema @index}}</span>
|
||||
{{dIcon "chevron-right"}}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -0,0 +1,63 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import ChildTreeNode from "admin/components/schema-theme-setting/editor/child-tree-node";
|
||||
|
||||
export default class SchemaThemeSettingNewEditorChildTree extends Component {
|
||||
@tracked expanded = true;
|
||||
|
||||
@action
|
||||
toggleVisibility() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
@action
|
||||
onChildClick(index) {
|
||||
return this.args.onChildClick(
|
||||
index,
|
||||
this.args.name,
|
||||
this.args.parentNodeIndex,
|
||||
this.args.parentNodeText
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="schema-theme-setting-editor__tree-node --heading"
|
||||
role="button"
|
||||
{{on "click" this.toggleVisibility}}
|
||||
>
|
||||
{{@name}}
|
||||
{{dIcon (if this.expanded "chevron-down" "chevron-right")}}
|
||||
</div>
|
||||
|
||||
{{#if this.expanded}}
|
||||
<ul>
|
||||
{{#each @objects as |object index|}}
|
||||
<ChildTreeNode
|
||||
@index={{index}}
|
||||
@object={{object}}
|
||||
@onChildClick={{fn this.onChildClick index}}
|
||||
@schema={{@schema}}
|
||||
@generateSchemaTitle={{@generateSchemaTitle}}
|
||||
data-test-parent-index={{@parentNodeIndex}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
<li class="schema-theme-setting-editor__tree-node --child --add-button">
|
||||
<DButton
|
||||
@action={{fn @addChildItem @name @parentNodeIndex}}
|
||||
@translatedLabel={{@schema.name}}
|
||||
@icon="plus"
|
||||
class="btn-transparent schema-theme-setting-editor__tree-add-button --child"
|
||||
data-test-parent-index={{@parentNodeIndex}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn, get } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { gt } from "truth-helpers";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import ChildTree from "admin/components/schema-theme-setting/editor/child-tree";
|
||||
|
||||
export default class SchemaThemeSettingNewEditorTreeNode extends Component {
|
||||
@tracked text;
|
||||
|
||||
childObjectsProperties = this.findChildObjectsProperties(
|
||||
this.args.schema.properties
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.#setText();
|
||||
}
|
||||
|
||||
@action
|
||||
registerInputFieldObserver() {
|
||||
this.args.registerInputFieldObserver(
|
||||
this.args.index,
|
||||
this.#setText.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
#setText() {
|
||||
this.text = this.args.generateSchemaTitle(
|
||||
this.args.object,
|
||||
this.args.schema,
|
||||
this.args.index
|
||||
);
|
||||
}
|
||||
|
||||
findChildObjectsProperties(properties) {
|
||||
const list = [];
|
||||
|
||||
for (const [name, spec] of Object.entries(properties)) {
|
||||
if (spec.type === "objects") {
|
||||
this.args.object[name] ||= [];
|
||||
|
||||
list.push({
|
||||
name,
|
||||
schema: spec.schema,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
<template>
|
||||
<li
|
||||
role="link"
|
||||
class="schema-theme-setting-editor__tree-node --parent
|
||||
{{if @active ' --active'}}"
|
||||
{{on "click" (fn @onClick @index)}}
|
||||
>
|
||||
<div class="schema-theme-setting-editor__tree-node-text">
|
||||
<span {{didInsert this.registerInputFieldObserver}}>{{this.text}}</span>
|
||||
|
||||
{{#if (gt this.childObjectsProperties.length 0)}}
|
||||
|
||||
{{dIcon (if @active "chevron-down" "chevron-right")}}
|
||||
{{else}}
|
||||
{{dIcon "chevron-right"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if @active}}
|
||||
{{#each this.childObjectsProperties as |childObjectsProperty|}}
|
||||
<ChildTree
|
||||
@name={{childObjectsProperty.name}}
|
||||
@schema={{childObjectsProperty.schema}}
|
||||
@objects={{get @object childObjectsProperty.name}}
|
||||
@parentNodeText={{this.text}}
|
||||
@parentNodeIndex={{@index}}
|
||||
@onChildClick={{@onChildClick}}
|
||||
@addChildItem={{@addChildItem}}
|
||||
@generateSchemaTitle={{@generateSchemaTitle}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</li>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { eq } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import TreeNode from "admin/components/schema-theme-setting/editor/tree-node";
|
||||
|
||||
<template>
|
||||
<ul class="schema-theme-setting-editor__tree">
|
||||
{{#if @backButtonText}}
|
||||
<li
|
||||
role="link"
|
||||
class="schema-theme-setting-editor__tree-node --back-btn"
|
||||
{{on "click" @clickBack}}
|
||||
>
|
||||
<div class="schema-theme-setting-editor__tree-node-text">
|
||||
{{dIcon "arrow-left"}}
|
||||
{{@backButtonText}}
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#each @data as |object index|}}
|
||||
<TreeNode
|
||||
@index={{index}}
|
||||
@object={{object}}
|
||||
@active={{eq @activeIndex index}}
|
||||
@onClick={{fn @updateIndex index}}
|
||||
@onChildClick={{@onChildClick}}
|
||||
@schema={{@schema}}
|
||||
@addChildItem={{@addChildItem}}
|
||||
@generateSchemaTitle={{@generateSchemaTitle}}
|
||||
@registerInputFieldObserver={{@registerInputFieldObserver}}
|
||||
@unregisterInputFieldObserver={{@unregisterInputFieldObserver}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
<li class="schema-theme-setting-editor__tree-node --parent --add-button">
|
||||
<DButton
|
||||
@action={{@addItem}}
|
||||
@translatedLabel={{@schema.name}}
|
||||
@icon="plus"
|
||||
class="btn-transparent schema-theme-setting-editor__tree-add-button --root"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div class="schema-field__input-description">
|
||||
{{@description}}
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,66 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import BooleanField from "admin/components/schema-theme-setting/types/boolean";
|
||||
import CategoriesField from "admin/components/schema-theme-setting/types/categories";
|
||||
import EnumField from "admin/components/schema-theme-setting/types/enum";
|
||||
import FloatField from "admin/components/schema-theme-setting/types/float";
|
||||
import GroupsField from "admin/components/schema-theme-setting/types/groups";
|
||||
import IntegerField from "admin/components/schema-theme-setting/types/integer";
|
||||
import StringField from "admin/components/schema-theme-setting/types/string";
|
||||
import TagsField from "admin/components/schema-theme-setting/types/tags";
|
||||
|
||||
export default class SchemaThemeSettingField extends Component {
|
||||
get component() {
|
||||
const type = this.args.spec.type;
|
||||
|
||||
switch (this.args.spec.type) {
|
||||
case "string":
|
||||
return StringField;
|
||||
case "integer":
|
||||
return IntegerField;
|
||||
case "float":
|
||||
return FloatField;
|
||||
case "boolean":
|
||||
return BooleanField;
|
||||
case "enum":
|
||||
return EnumField;
|
||||
case "categories":
|
||||
return CategoriesField;
|
||||
case "tags":
|
||||
return TagsField;
|
||||
case "groups":
|
||||
return GroupsField;
|
||||
default:
|
||||
throw new Error(`unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@cached
|
||||
get description() {
|
||||
if (!this.args.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
return htmlSafe(this.args.description.trim().replace(/\n/g, "<br>"));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="schema-field" data-name={{@name}} data-type={{@spec.type}}>
|
||||
<label class="schema-field__label">{{@label}}{{if
|
||||
@spec.required
|
||||
"*"
|
||||
}}</label>
|
||||
|
||||
<div class="schema-field__input">
|
||||
<this.component
|
||||
@value={{@value}}
|
||||
@spec={{@spec}}
|
||||
@onChange={{@onValueChange}}
|
||||
@description={{this.description}}
|
||||
@setting={{@setting}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { and, not } from "truth-helpers";
|
||||
import I18n from "discourse-i18n";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingNumberField extends Component {
|
||||
@tracked touched = false;
|
||||
@tracked value = this.args.value;
|
||||
min = this.args.spec.validations?.min;
|
||||
max = this.args.spec.validations?.max;
|
||||
required = this.args.spec.required;
|
||||
|
||||
@action
|
||||
onInput(event) {
|
||||
this.touched = true;
|
||||
let inputValue = event.currentTarget.value;
|
||||
|
||||
if (isNaN(inputValue)) {
|
||||
this.value = null;
|
||||
} else {
|
||||
this.value = this.parseValue(inputValue);
|
||||
}
|
||||
|
||||
this.args.onChange(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value - The value of the input field to parse into a number
|
||||
* @returns {number}
|
||||
*/
|
||||
parseFunc() {
|
||||
throw "Not implemented";
|
||||
}
|
||||
|
||||
get validationErrorMessage() {
|
||||
if (!this.touched) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
if (this.required) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.required");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.min && this.value < this.min) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.number.too_small", {
|
||||
count: this.min,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.max && this.value > this.max) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.number.too_large", {
|
||||
count: this.max,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<Input
|
||||
@value={{this.value}}
|
||||
{{on "input" this.onInput}}
|
||||
@type="number"
|
||||
inputmode={{this.inputmode}}
|
||||
pattern={{this.pattern}}
|
||||
step={{this.step}}
|
||||
max={{this.max}}
|
||||
min={{this.min}}
|
||||
required={{this.required}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeBoolean extends Component {
|
||||
@action
|
||||
onInput(event) {
|
||||
this.args.onChange(event.currentTarget.checked);
|
||||
}
|
||||
|
||||
<template>
|
||||
<Input @checked={{@value}} {{on "input" this.onInput}} @type="checkbox" />
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { and, not } from "truth-helpers";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import SchemaThemeSettingTypeModels from "admin/components/schema-theme-setting/types/models";
|
||||
import CategorySelector from "select-kit/components/category-selector";
|
||||
|
||||
export default class SchemaThemeSettingTypeCategories extends SchemaThemeSettingTypeModels {
|
||||
@tracked
|
||||
value =
|
||||
this.args.value?.map((categoryId) => {
|
||||
return this.args.setting.metadata.categories[categoryId];
|
||||
}) || [];
|
||||
|
||||
type = "categories";
|
||||
|
||||
onChange(categories) {
|
||||
return categories.map((category) => {
|
||||
this.args.setting.metadata.categories[category.id] ||= category;
|
||||
return category.id;
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<CategorySelector
|
||||
@categories={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
@options={{hash allowUncategorized=false maximum=this.max}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
export default class SchemaThemeSettingTypeEnum extends Component {
|
||||
@tracked
|
||||
value =
|
||||
this.args.value || (this.args.spec.required && this.args.spec.default);
|
||||
|
||||
get content() {
|
||||
return this.args.spec.choices.map((choice) => {
|
||||
return {
|
||||
name: choice,
|
||||
id: choice,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onInput(newVal) {
|
||||
this.value = newVal;
|
||||
this.args.onChange(newVal);
|
||||
}
|
||||
|
||||
<template>
|
||||
<ComboBox
|
||||
@content={{this.content}}
|
||||
@value={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
/>
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import SchemaThemeSettingNumberField from "admin/components/schema-theme-setting/number-field";
|
||||
|
||||
export default class SchemaThemeSettingTypeFloat extends SchemaThemeSettingNumberField {
|
||||
step = 0.1;
|
||||
|
||||
parseValue(value) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { service } from "@ember/service";
|
||||
import { and, not } from "truth-helpers";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import SchemaThemeSettingTypeModels from "admin/components/schema-theme-setting/types/models";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
|
||||
export default class SchemaThemeSettingTypeGroups extends SchemaThemeSettingTypeModels {
|
||||
@service site;
|
||||
|
||||
type = "groups";
|
||||
|
||||
get groupChooserOptions() {
|
||||
return {
|
||||
clearable: !this.required,
|
||||
filterable: true,
|
||||
maximum: this.max,
|
||||
};
|
||||
}
|
||||
|
||||
<template>
|
||||
<GroupChooser
|
||||
@content={{this.site.groups}}
|
||||
@value={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
@options={{this.groupChooserOptions}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import SchemaThemeSettingNumberField from "admin/components/schema-theme-setting/number-field";
|
||||
|
||||
export default class SchemaThemeSettingTypeInteger extends SchemaThemeSettingNumberField {
|
||||
inputMode = "numeric";
|
||||
pattern = "[0-9]*";
|
||||
|
||||
parseValue(value) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { isBlank } from "@ember/utils";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class SchemaThemeSettingTypeModels extends Component {
|
||||
@tracked value = this.args.value;
|
||||
|
||||
required = this.args.spec.required;
|
||||
min = this.args.spec.validations?.min;
|
||||
max = this.args.spec.validations?.max;
|
||||
type;
|
||||
|
||||
@action
|
||||
onInput(newValue) {
|
||||
this.value = newValue;
|
||||
this.args.onChange(this.onChange(newValue));
|
||||
}
|
||||
|
||||
onChange(newValue) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
get validationErrorMessage() {
|
||||
const isValueBlank = isBlank(this.value);
|
||||
|
||||
if (!this.required && isValueBlank) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.min && this.value && this.value.length < this.min) ||
|
||||
(this.required && isValueBlank)
|
||||
) {
|
||||
return I18n.t(
|
||||
`admin.customize.theme.schema.fields.${this.type}.at_least`,
|
||||
{
|
||||
count: this.min || 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { and, not } from "truth-helpers";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import I18n from "discourse-i18n";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeString extends Component {
|
||||
@tracked touched = false;
|
||||
@tracked value = this.args.value || "";
|
||||
minLength = this.args.spec.validations?.min_length;
|
||||
maxLength = this.args.spec.validations?.max_length;
|
||||
required = this.args.spec.required;
|
||||
|
||||
@action
|
||||
onInput(event) {
|
||||
this.touched = true;
|
||||
const newValue = event.currentTarget.value;
|
||||
this.args.onChange(newValue);
|
||||
this.value = newValue;
|
||||
}
|
||||
|
||||
get validationErrorMessage() {
|
||||
if (!this.touched) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueLength = this.value.length;
|
||||
|
||||
if (valueLength === 0) {
|
||||
if (this.required) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.required");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.minLength && valueLength < this.minLength) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.string.too_short", {
|
||||
count: this.minLength,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<Input
|
||||
class="--string"
|
||||
@value={{this.value}}
|
||||
{{on "input" this.onInput}}
|
||||
required={{this.required}}
|
||||
minLength={{this.minLength}}
|
||||
maxLength={{this.maxLength}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.maxLength}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"schema-field__input-count"
|
||||
(if this.validationErrorMessage " --error")
|
||||
}}
|
||||
>
|
||||
{{this.value.length}}/{{this.maxLength}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { and, not } from "truth-helpers";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import SchemaThemeSettingTypeModels from "admin/components/schema-theme-setting/types/models";
|
||||
import TagChooser from "select-kit/components/tag-chooser";
|
||||
|
||||
export default class SchemaThemeSettingTypeTags extends SchemaThemeSettingTypeModels {
|
||||
type = "tags";
|
||||
|
||||
get tagChooserOption() {
|
||||
return {
|
||||
allowAny: false,
|
||||
maximum: this.max,
|
||||
};
|
||||
}
|
||||
|
||||
<template>
|
||||
<TagChooser
|
||||
@tags={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
@options={{this.tagChooserOption}}
|
||||
class={{if this.validationErrorMessage "--invalid"}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
<ComboBox
|
||||
@content={{this.actionNames}}
|
||||
@value={{this.actionName}}
|
||||
@onChange={{action (mut this.actionName)}}
|
||||
@onChange={{fn (mut this.actionName)}}
|
||||
/>
|
||||
|
||||
<DButton
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
|
|
@ -2,7 +2,6 @@ import Component from "@ember/component";
|
|||
import { action, set } from "@ember/object";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { classNameBindings } from "@ember-decorators/component";
|
||||
import { on } from "@ember-decorators/object";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
@classNameBindings(":value-list", ":secret-value-list")
|
||||
|
@ -12,13 +11,12 @@ export default class SecretValueList extends Component {
|
|||
values = null;
|
||||
validationMessage = null;
|
||||
|
||||
@on("didReceiveAttrs")
|
||||
_setupCollection() {
|
||||
const values = this.values;
|
||||
didReceiveAttrs() {
|
||||
super.didReceiveAttrs(...arguments);
|
||||
|
||||
this.set(
|
||||
"collection",
|
||||
this._splitValues(values, this.inputDelimiter || "\n")
|
||||
this._splitValues(this.values, this.inputDelimiter || "\n")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<ComboBox
|
||||
@content={{this.validValues}}
|
||||
@value={{this.newValue}}
|
||||
@onChange={{action this.addValue}}
|
||||
@onChange={{this.addValue}}
|
||||
@valueProperty={{this.setting.computedValueProperty}}
|
||||
@nameProperty={{this.setting.computedNameProperty}}
|
||||
@options={{hash castInteger=true allowAny=false}}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { action } from "@ember/object";
|
|||
import { empty } from "@ember/object/computed";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { classNameBindings } from "@ember-decorators/component";
|
||||
import { on } from "@ember-decorators/object";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
@classNameBindings(":simple-list", ":value-list")
|
||||
|
@ -18,8 +17,8 @@ export default class SimpleList extends Component {
|
|||
choices = null;
|
||||
allowAny = false;
|
||||
|
||||
@on("didReceiveAttrs")
|
||||
_setupCollection() {
|
||||
didReceiveAttrs() {
|
||||
super.didReceiveAttrs(...arguments);
|
||||
this.set("collection", this._splitValues(this.values, this.inputDelimiter));
|
||||
this.set("isPredefinedList", !this.allowAny && !isEmpty(this.choices));
|
||||
}
|
||||
|
|
|
@ -25,26 +25,49 @@
|
|||
</div>
|
||||
|
||||
<div class="setting-value">
|
||||
{{component
|
||||
this.componentName
|
||||
setting=this.setting
|
||||
value=this.buffered.value
|
||||
validationMessage=this.validationMessage
|
||||
preview=this.preview
|
||||
isSecret=this.isSecret
|
||||
allowAny=this.allowAny
|
||||
changeValueCallback=this.changeValueCallback
|
||||
}}
|
||||
{{#if this.settingEditButton}}
|
||||
<DButton
|
||||
@action={{this.settingEditButton.action}}
|
||||
@icon={{this.settingEditButton.icon}}
|
||||
@label={{this.settingEditButton.label}}
|
||||
class="setting-value-edit-button"
|
||||
/>
|
||||
|
||||
<SiteSettings::Description @description={{this.setting.description}} />
|
||||
{{else}}
|
||||
{{component
|
||||
this.componentName
|
||||
setting=this.setting
|
||||
value=this.buffered.value
|
||||
validationMessage=this.validationMessage
|
||||
preview=this.preview
|
||||
isSecret=this.isSecret
|
||||
allowAny=this.allowAny
|
||||
changeValueCallback=this.changeValueCallback
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.dirty}}
|
||||
<div class="setting-controls">
|
||||
<DButton class="ok" @action={{this.update}} @icon="check" />
|
||||
<DButton class="cancel" @action={{this.cancel}} @icon="times" />
|
||||
<DButton
|
||||
@action={{this.update}}
|
||||
@icon="check"
|
||||
class="ok setting-controls__ok"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.cancel}}
|
||||
@icon="times"
|
||||
class="cancel setting-controls__cancel"
|
||||
/>
|
||||
</div>
|
||||
{{else if this.setting.overridden}}
|
||||
{{else if this.overridden}}
|
||||
{{#if this.setting.secret}}
|
||||
<DButton @action={{this.toggleSecret}} @icon="far-eye-slash" />
|
||||
<DButton
|
||||
@action={{this.toggleSecret}}
|
||||
@icon="far-eye-slash"
|
||||
class="setting-toggle-secret"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import Category from "discourse/models/category";
|
||||
import SettingValidationMessage from "admin/components/setting-validation-message";
|
||||
import SiteSettingsDescription from "admin/components/site-settings/description";
|
||||
import CategorySelector from "select-kit/components/category-selector";
|
||||
|
||||
export default class CategoryList extends Component {
|
||||
@tracked selectedCategories = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.pendingCategoriesRequest = Promise.resolve();
|
||||
this.valueChanged();
|
||||
}
|
||||
|
||||
get categoryIds() {
|
||||
return this.args.value.split("|").filter(Boolean);
|
||||
}
|
||||
|
||||
async updateSelectedCategories(previousRequest) {
|
||||
const categories = await Category.asyncFindByIds(this.categoryIds);
|
||||
|
||||
// This is to prevent a race. We want to ensure that the update to
|
||||
// selectedCategories for this request happens after the update for the
|
||||
// previous request.
|
||||
await previousRequest;
|
||||
|
||||
this.selectedCategories = categories;
|
||||
}
|
||||
|
||||
@action
|
||||
valueChanged() {
|
||||
const previousRequest = this.pendingCategoriesRequest;
|
||||
this.pendingCategoriesRequest =
|
||||
this.updateSelectedCategories(previousRequest);
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeSelectedCategories(value) {
|
||||
this.args.changeValueCallback((value || []).mapBy("id").join("|"));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div ...attributes {{didUpdate this.valueChanged @value}}>
|
||||
<CategorySelector
|
||||
@categories={{this.selectedCategories}}
|
||||
@onChange={{this.onChangeSelectedCategories}}
|
||||
/>
|
||||
|
||||
<SiteSettingsDescription @description={{@setting.description}} />
|
||||
<SettingValidationMessage @message={{@validationMessage}} />
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<CategorySelector
|
||||
@categories={{this.selectedCategories}}
|
||||
@onChange={{this.onChangeSelectedCategories}}
|
||||
/>
|
||||
|
||||
<div class="desc">{{html-safe this.setting.description}}</div>
|
||||
<SettingValidationMessage @message={{this.validationMessage}} />
|
|
@ -1,15 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default class CategoryList extends Component {
|
||||
@computed("value")
|
||||
get selectedCategories() {
|
||||
return Category.findByIds(this.value.split("|").filter(Boolean));
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeSelectedCategories(value) {
|
||||
this.set("value", (value || []).mapBy("id").join("|"));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<CategoryChooser
|
||||
@value={{this.value}}
|
||||
@onChange={{action (mut this.value)}}
|
||||
@onChange={{fn (mut this.value)}}
|
||||
@options={{hash allowUncategorized=true none=(eq this.setting.default "")}}
|
||||
/>
|
||||
<SettingValidationMessage @message={{this.validationMessage}} />
|
||||
<div class="desc">{{html-safe this.setting.description}}</div>
|
||||
<SiteSettings::Description @description={{this.setting.description}} />
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue