Version bump
This commit is contained in:
commit
4dc89cb0cc
@ -1,10 +1,8 @@
|
||||
app/assets/javascripts/browser-update.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
app/assets/javascripts/discourse/lib/autosize.js
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
lib/highlight_js/
|
||||
lib/javascripts/messageformat-lookup.js
|
||||
lib/pretty_text/
|
||||
plugins/**/lib/javascripts/locale
|
||||
public/
|
||||
|
||||
2
.github/labeler.yml
vendored
Normal file
2
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
chat:
|
||||
- plugins/chat/**/*
|
||||
14
.github/workflows/labeler.yml
vendored
Normal file
14
.github/workflows/labeler.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
2
.github/workflows/licenses.yml
vendored
2
.github/workflows/licenses.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v3
|
||||
|
||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v3
|
||||
|
||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@ -41,8 +41,6 @@ jobs:
|
||||
target: plugins
|
||||
- build_type: frontend
|
||||
target: core # Handled by core_frontend_tests job (below)
|
||||
- build_type: system
|
||||
target: plugins # Enable once at least 1 plugin has system tests
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -83,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v3
|
||||
@ -178,7 +176,7 @@ jobs:
|
||||
|
||||
- name: Plugin System Tests
|
||||
if: matrix.build_type == 'system' && matrix.target == 'plugins'
|
||||
run: bin/system_rspec plugins/*/spec/system
|
||||
run: LOAD_PLUGINS=1 bin/system_rspec plugins/*/spec/system
|
||||
|
||||
- name: Upload failed system test screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
@ -223,7 +221,7 @@ jobs:
|
||||
TESTEM_FIREFOX_PATH: ${{ (matrix.browser == 'Firefox Evergreen') && '/opt/firefox-evergreen/firefox' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@ -234,7 +232,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -39,6 +39,7 @@
|
||||
!/plugins/discourse-narrative-bot
|
||||
!/plugins/discourse-presence
|
||||
!/plugins/lazy-yt/
|
||||
!/plugins/chat/
|
||||
!/plugins/poll/
|
||||
!/plugins/styleguide
|
||||
/plugins/*/auto_generated/
|
||||
|
||||
27
Gemfile.lock
27
Gemfile.lock
@ -89,7 +89,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
byebug (11.1.3)
|
||||
capybara (3.37.1)
|
||||
capybara (3.38.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
@ -145,7 +145,7 @@ GEM
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.11.0)
|
||||
excon (0.93.1)
|
||||
excon (0.94.0)
|
||||
execjs (2.8.1)
|
||||
exifr (1.3.10)
|
||||
fabrication (2.30.0)
|
||||
@ -182,17 +182,17 @@ GEM
|
||||
image_size (>= 1.5, < 4)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (3.1.0)
|
||||
image_size (3.2.0)
|
||||
in_threads (1.6.0)
|
||||
jmespath (1.6.1)
|
||||
jquery-rails (4.5.0)
|
||||
jquery-rails (4.5.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.6.2)
|
||||
json-schema (3.0.0)
|
||||
addressable (>= 2.8)
|
||||
json_schemer (0.2.22)
|
||||
json_schemer (0.2.23)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
@ -239,7 +239,8 @@ GEM
|
||||
mini_suffix (0.3.3)
|
||||
ffi (~> 1.9)
|
||||
minitest (5.16.3)
|
||||
mocha (1.16.0)
|
||||
mocha (2.0.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.6.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
@ -306,7 +307,7 @@ GEM
|
||||
openssl (> 2.0, < 3.1)
|
||||
optimist (3.0.1)
|
||||
parallel (1.22.1)
|
||||
parallel_tests (3.13.0)
|
||||
parallel_tests (4.0.0)
|
||||
parallel
|
||||
parser (3.1.2.1)
|
||||
ast (~> 2.4.1)
|
||||
@ -328,7 +329,7 @@ GEM
|
||||
rack (2.2.4)
|
||||
rack-mini-profiler (3.0.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.0.2)
|
||||
rack-protection (3.0.3)
|
||||
rack
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
@ -370,7 +371,7 @@ GEM
|
||||
rack (>= 1.4)
|
||||
rexml (3.2.5)
|
||||
rinku (2.0.6)
|
||||
rotp (6.2.0)
|
||||
rotp (6.2.1)
|
||||
rqrcode (2.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
@ -406,7 +407,7 @@ GEM
|
||||
json-schema (>= 2.2, < 4.0)
|
||||
railties (>= 3.1, < 7.1)
|
||||
rspec-core (>= 2.14)
|
||||
rubocop (1.37.1)
|
||||
rubocop (1.38.0)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.2.1)
|
||||
@ -421,7 +422,7 @@ GEM
|
||||
rubocop-discourse (3.0)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.14.2)
|
||||
rubocop-rspec (2.15.0)
|
||||
rubocop (~> 1.33)
|
||||
ruby-prof (1.4.3)
|
||||
ruby-progressbar (1.11.0)
|
||||
@ -442,7 +443,7 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
selenium-webdriver (4.5.0)
|
||||
selenium-webdriver (4.6.1)
|
||||
childprocess (>= 0.5, < 5.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
@ -506,7 +507,7 @@ GEM
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yaml-lint (0.0.10)
|
||||
zeitwerk (2.6.3)
|
||||
zeitwerk (2.6.6)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
|
||||
@ -21,8 +21,7 @@
|
||||
"line-stream": "0.0.0",
|
||||
"regenerator-transform": "0.10.1",
|
||||
"source-map": "0.1.43",
|
||||
"sourcemap-validator": "1.1.1",
|
||||
"xmldom": "0.1.31"
|
||||
"sourcemap-validator": "1.1.1"
|
||||
},
|
||||
"corrections": true,
|
||||
"ignore": [
|
||||
|
||||
@ -2,7 +2,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import WatchedWord from "admin/models/watched-word";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { equal, not } from "@ember/object/computed";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
@ -16,7 +16,7 @@ export default Component.extend({
|
||||
showMessage: false,
|
||||
selectedTags: null,
|
||||
isCaseSensitive: false,
|
||||
|
||||
submitDisabled: not("word"),
|
||||
canReplace: equal("actionKey", "replace"),
|
||||
canTag: equal("actionKey", "tag"),
|
||||
canLink: equal("actionKey", "link"),
|
||||
|
||||
@ -6,11 +6,13 @@ import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { or } from "@ember/object/computed";
|
||||
|
||||
export default Controller.extend({
|
||||
dialog: service(),
|
||||
loading: false,
|
||||
filter: null,
|
||||
showSearch: or("model.length", "filter"),
|
||||
|
||||
_debouncedShow() {
|
||||
Permalink.findAll(this.filter).then((result) => {
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
import { action } from "@ember/object";
|
||||
import Controller from "@ember/controller";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Controller.extend({
|
||||
@discourseComputed
|
||||
adminRoutes() {
|
||||
router: service(),
|
||||
|
||||
get adminRoutes() {
|
||||
return this.allAdminRoutes.filter((r) => this.routeExists(r.full_location));
|
||||
},
|
||||
|
||||
get brokenAdminRoutes() {
|
||||
return this.allAdminRoutes.filter(
|
||||
(r) => !this.routeExists(r.full_location)
|
||||
);
|
||||
},
|
||||
|
||||
get allAdminRoutes() {
|
||||
return this.model
|
||||
.filter((p) => p?.enabled)
|
||||
.map((p) => {
|
||||
if (p.get("enabled")) {
|
||||
return p.admin_route;
|
||||
}
|
||||
return p.admin_route;
|
||||
})
|
||||
.compact();
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
@action
|
||||
@ -21,4 +31,13 @@ export default Controller.extend({
|
||||
adminDetail.classList.toggle(state);
|
||||
});
|
||||
},
|
||||
|
||||
routeExists(routeName) {
|
||||
try {
|
||||
this.router.urlFor(routeName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
adminBackupsLogs: controller(),
|
||||
|
||||
@discourseComputed
|
||||
warningMessage() {
|
||||
// this is never shown here, but we may want to show different
|
||||
// messages in plugins
|
||||
return "";
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
yesLabel() {
|
||||
return "yes_value";
|
||||
},
|
||||
|
||||
actions: {
|
||||
startBackupWithUploads() {
|
||||
this.send("closeModal");
|
||||
|
||||
@ -43,6 +43,7 @@ export default Mixin.create({
|
||||
validationMessage: null,
|
||||
isSecret: oneWay("setting.secret"),
|
||||
setting: null,
|
||||
attributeBindings: ["setting.setting:data-setting"],
|
||||
|
||||
@discourseComputed("buffered.value", "setting.value")
|
||||
dirty(bufferVal, settingVal) {
|
||||
@ -136,6 +137,7 @@ export default Mixin.create({
|
||||
"default_email_mailing_list_mode_frequency",
|
||||
"default_email_previous_replies",
|
||||
"default_email_in_reply_to",
|
||||
"default_hide_profile_and_presence",
|
||||
"default_other_new_topic_duration_minutes",
|
||||
"default_other_auto_track_topics_after_msecs",
|
||||
"default_other_notification_level_when_replying",
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
<ComboBox @content={{this.permalinkTypes}} @value={{this.permalinkType}} @onChange={{action (mut this.permalinkType)}} @class="permalink-type" />
|
||||
|
||||
<TextField @value={{this.permalinkTypeValue}} @disabled={{this.formSubmitted}} @placeholderKey={{this.permalinkTypePlaceholder}} @autocorrect="off" @autocapitalize="off" @keyDown={{action "submitFormOnEnter"}} />
|
||||
<TextField @value={{this.permalinkTypeValue}} @disabled={{this.formSubmitted}} @class="permalink-destination" @placeholderKey={{this.permalinkTypePlaceholder}} @autocorrect="off" @autocapitalize="off" @keyDown={{action "submitFormOnEnter"}} />
|
||||
|
||||
<DButton @action={{action "onSubmit"}} @disabled={{this.formSubmitted}} @label="admin.permalink.form.add" />
|
||||
<DButton @action={{action "onSubmit"}} @disabled={{this.formSubmitted}} @class="permalink-add" @label="admin.permalink.form.add" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DButton @type="submit" @class="btn btn-primary" @action={{action "submit"}} @disabled={{this.formSubmitted}} @label="admin.watched_words.form.add" />
|
||||
<DButton @type="submit" @class="btn btn-primary" @action={{action "submit"}} @disabled={{this.submitDisabled}} @label="admin.watched_words.form.add" />
|
||||
|
||||
{{#if this.showMessage}}
|
||||
<span class="success-message">{{this.message}}</span>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
<DModalBody @title="admin.backups.operations.backup.confirm">
|
||||
<DButton @class="btn-primary backup-with-uploads" @action={{action "startBackupWithUploads"}} @label="yes_value" />
|
||||
{{#if this.warningMessage}}
|
||||
<div class="alert alert-warning">{{html-safe this.warningMessage}}</div>
|
||||
{{/if}}
|
||||
<DButton @class="btn-primary backup-with-uploads" @action={{action "startBackupWithUploads"}} @label={{this.yesLabel}} />
|
||||
<DButton @class="backup-no-uploads" @action={{action "startBackupWithoutUploads"}} @label="admin.backups.operations.backup.without_uploads" />
|
||||
<DButton @class="btn-default" @action={{action "cancel"}} @label="no_value" />
|
||||
</DModalBody>
|
||||
|
||||
@ -6,51 +6,58 @@
|
||||
<PermalinkForm @action={{action "recordAdded"}} />
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
{{#if this.model.length}}
|
||||
<div class="permalink-search">
|
||||
<TextField @value={{this.filter}} @class="url-input" @placeholderKey="admin.permalink.form.filter" @autocorrect="off" @autocapitalize="off" />
|
||||
</div>
|
||||
<table class="admin-logs-table permalinks grid">
|
||||
<thead class="heading-container">
|
||||
<th class="col heading first url">{{i18n "admin.permalink.url"}}</th>
|
||||
<th class="col heading destination">{{i18n "admin.permalink.destination"}}</th>
|
||||
<th class="col heading actions"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |pl|}}
|
||||
<tr class="admin-list-item">
|
||||
<td class="col first url">
|
||||
<FlatButton @title="admin.permalink.copy_to_clipboard" @icon="far-clipboard" @action={{action "copyUrl" pl}} />
|
||||
<span id="admin-permalink-{{pl.id}}" title={{pl.url}}>{{pl.url}}</span>
|
||||
</td>
|
||||
<td class="col destination">
|
||||
{{#if pl.topic_id}}
|
||||
<a href={{pl.topic_url}}>{{pl.topic_title}}</a>
|
||||
{{/if}}
|
||||
{{#if pl.post_id}}
|
||||
<a href={{pl.post_url}}>{{pl.post_topic_title}} #{{pl.post_number}}</a>
|
||||
{{/if}}
|
||||
{{#if pl.category_id}}
|
||||
{{category-link pl.category}}
|
||||
{{/if}}
|
||||
{{#if pl.tag_id}}
|
||||
<a href={{pl.tag_url}}>{{pl.tag_name}}</a>
|
||||
{{/if}}
|
||||
{{#if pl.external_url}}
|
||||
{{#if pl.linkIsExternal}}
|
||||
{{d-icon "external-link-alt"}}
|
||||
<div class="permalink-search">
|
||||
<TextField @value={{this.filter}} @class="url-input" @placeholderKey="admin.permalink.form.filter" @autocorrect="off" @autocapitalize="off" />
|
||||
</div>
|
||||
|
||||
<div class="permalink-results">
|
||||
{{#if this.model.length}}
|
||||
<table class="admin-logs-table permalinks grid">
|
||||
<thead class="heading-container">
|
||||
<th class="col heading first url">{{i18n "admin.permalink.url"}}</th>
|
||||
<th class="col heading destination">{{i18n "admin.permalink.destination"}}</th>
|
||||
<th class="col heading actions"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |pl|}}
|
||||
<tr class="admin-list-item">
|
||||
<td class="col first url">
|
||||
<FlatButton @title="admin.permalink.copy_to_clipboard" @icon="far-clipboard" @action={{action "copyUrl" pl}} />
|
||||
<span id="admin-permalink-{{pl.id}}" title={{pl.url}}>{{pl.url}}</span>
|
||||
</td>
|
||||
<td class="col destination">
|
||||
{{#if pl.topic_id}}
|
||||
<a href={{pl.topic_url}}>{{pl.topic_title}}</a>
|
||||
{{/if}}
|
||||
<a href={{pl.external_url}}>{{pl.external_url}}</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="col action" style="text-align: right;">
|
||||
<DButton @action={{action "destroy"}} @actionParam={{pl}} @icon="far-trash-alt" @class="btn-danger" />
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
{{i18n "search.no_results"}}
|
||||
{{/if}}
|
||||
{{#if pl.post_id}}
|
||||
<a href={{pl.post_url}}>{{pl.post_topic_title}} #{{pl.post_number}}</a>
|
||||
{{/if}}
|
||||
{{#if pl.category_id}}
|
||||
{{category-link pl.category}}
|
||||
{{/if}}
|
||||
{{#if pl.tag_id}}
|
||||
<a href={{pl.tag_url}}>{{pl.tag_name}}</a>
|
||||
{{/if}}
|
||||
{{#if pl.external_url}}
|
||||
{{#if pl.linkIsExternal}}
|
||||
{{d-icon "external-link-alt"}}
|
||||
{{/if}}
|
||||
<a href={{pl.external_url}}>{{pl.external_url}}</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="col action" style="text-align: right;">
|
||||
<DButton @action={{action "destroy"}} @actionParam={{pl}} @icon="far-trash-alt" @class="btn-danger" />
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
{{#if this.filter}}
|
||||
<p class="permalink-results__no-result">{{i18n "search.no_results"}}</p>
|
||||
{{else}}
|
||||
<p class="permalink-results__no-permalinks">{{i18n "admin.permalink.no_permalinks"}}</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</ConditionalLoadingSpinner>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |plugin|}}
|
||||
<tr>
|
||||
<tr data-plugin-name={{plugin.name}}>
|
||||
<td>
|
||||
{{#if plugin.is_official}}
|
||||
{{d-icon "check-circle"
|
||||
|
||||
@ -19,5 +19,11 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-detail pull-left mobile-closed">
|
||||
{{#each this.brokenAdminRoutes as |route|}}
|
||||
<div class="alert alert-error">
|
||||
{{i18n "admin.plugins.broken_route" name=(i18n route.label)}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-resolver": "^8.0.3",
|
||||
"ember-source": "~3.28.8",
|
||||
"ember-source": "~3.28.10",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0"
|
||||
},
|
||||
|
||||
@ -9,10 +9,18 @@ import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
export default function () {
|
||||
if (isTesting()) {
|
||||
const lastArgument = arguments[arguments.length - 1];
|
||||
const hasImmediateArgument = typeof lastArgument === "boolean";
|
||||
|
||||
let args = [].slice.call(arguments, 0, hasImmediateArgument ? -2 : -1);
|
||||
|
||||
// Replace the time argument with 10ms
|
||||
let args = [].slice.call(arguments, 0, -1);
|
||||
args.push(10);
|
||||
|
||||
if (hasImmediateArgument) {
|
||||
args.push(lastArgument);
|
||||
}
|
||||
|
||||
return debounce.apply(undefined, args);
|
||||
} else {
|
||||
return debounce(...arguments);
|
||||
|
||||
@ -88,7 +88,7 @@ export function readOnly(target, name, desc) {
|
||||
};
|
||||
}
|
||||
|
||||
export function debounce(delay) {
|
||||
export function debounce(delay, immediate = false) {
|
||||
return function (target, name, descriptor) {
|
||||
return {
|
||||
enumerable: descriptor.enumerable,
|
||||
@ -97,7 +97,13 @@ export function debounce(delay) {
|
||||
initializer() {
|
||||
const originalFunction = descriptor.value;
|
||||
const debounced = function (...args) {
|
||||
return discourseDebounce(this, originalFunction, ...args, delay);
|
||||
return discourseDebounce(
|
||||
this,
|
||||
originalFunction,
|
||||
...args,
|
||||
delay,
|
||||
immediate
|
||||
);
|
||||
};
|
||||
|
||||
return debounced;
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-source": "~3.28.8",
|
||||
"ember-source": "~3.28.10",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0"
|
||||
},
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-resolver": "^8.0.3",
|
||||
"ember-source": "~3.28.8",
|
||||
"ember-source": "~3.28.10",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0"
|
||||
},
|
||||
|
||||
@ -113,18 +113,22 @@ module.exports = {
|
||||
directoryName,
|
||||
"test/javascripts"
|
||||
);
|
||||
const configDirectory = path.resolve(root, directoryName, "config");
|
||||
const hasJs = fs.existsSync(jsDirectory);
|
||||
const hasAdminJs = fs.existsSync(adminJsDirectory);
|
||||
const hasTests = fs.existsSync(testDirectory);
|
||||
const hasConfig = fs.existsSync(configDirectory);
|
||||
return {
|
||||
pluginName,
|
||||
directoryName,
|
||||
jsDirectory,
|
||||
adminJsDirectory,
|
||||
testDirectory,
|
||||
configDirectory,
|
||||
hasJs,
|
||||
hasAdminJs,
|
||||
hasTests,
|
||||
hasConfig,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-resolver": "^8.0.3",
|
||||
"ember-source": "~3.28.8",
|
||||
"ember-source": "~3.28.10",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0"
|
||||
},
|
||||
|
||||
@ -51,7 +51,7 @@ export default Component.extend({
|
||||
this.set("actions", connectorClass.actions);
|
||||
|
||||
for (const [name, action] of Object.entries(this.actions)) {
|
||||
this.set(name, action);
|
||||
this.set(name, action.bind(this));
|
||||
}
|
||||
|
||||
const merged = buildArgsWithDeprecations(args, deprecatedArgs);
|
||||
|
||||
@ -42,10 +42,12 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("backupEnabled", "secondFactorMethod")
|
||||
showToggleMethodLink(backupEnabled, secondFactorMethod) {
|
||||
@discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod")
|
||||
showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) {
|
||||
return (
|
||||
backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
backupEnabled &&
|
||||
totpEnabled &&
|
||||
secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -1,31 +1,32 @@
|
||||
import { inject as service } from "@ember/service";
|
||||
import { canDisplayCategory } from "discourse/lib/sidebar/helpers";
|
||||
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default class SidebarAnonymousCategoriesSection extends SidebarCommonCategoriesSection {
|
||||
@service site;
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
if (!this.siteSettings.default_sidebar_categories) {
|
||||
this.shouldSortCategoriesByDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
get categories() {
|
||||
let categories = this.site.categoriesList;
|
||||
|
||||
if (this.siteSettings.default_sidebar_categories) {
|
||||
const defaultCategoryIds = this.siteSettings.default_sidebar_categories
|
||||
.split("|")
|
||||
.map((categoryId) => parseInt(categoryId, 10));
|
||||
|
||||
categories = categories.filter((category) =>
|
||||
defaultCategoryIds.includes(category.id)
|
||||
return Category.findByIds(
|
||||
this.siteSettings.default_sidebar_categories
|
||||
.split("|")
|
||||
.map((categoryId) => parseInt(categoryId, 10))
|
||||
);
|
||||
} else {
|
||||
categories = categories
|
||||
.filter(
|
||||
(category) =>
|
||||
canDisplayCategory(category, this.siteSettings) &&
|
||||
!category.parent_category_id
|
||||
)
|
||||
return this.site.categoriesList
|
||||
.filter((category) => {
|
||||
return (
|
||||
!category.parent_category_id &&
|
||||
canDisplayCategory(category.id, this.siteSettings)
|
||||
);
|
||||
})
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,29 +2,60 @@ import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
import Category from "discourse/models/category";
|
||||
import CategorySectionLink from "discourse/lib/sidebar/user/categories-section/category-section-link";
|
||||
import { canDisplayCategory } from "discourse/lib/sidebar/helpers";
|
||||
|
||||
export default class SidebarCommonCategoriesSection extends Component {
|
||||
@service topicTrackingState;
|
||||
@service siteSettings;
|
||||
@service site;
|
||||
|
||||
// Override in child
|
||||
shouldSortCategoriesByDefault = true;
|
||||
|
||||
/**
|
||||
* Override in child
|
||||
*
|
||||
* @returns {Object[]} An array of Category objects
|
||||
*/
|
||||
get categories() {}
|
||||
|
||||
get sortedCategories() {
|
||||
if (!this.shouldSortCategoriesByDefault) {
|
||||
return this.categories;
|
||||
}
|
||||
|
||||
let categories = this.site.categories;
|
||||
|
||||
if (!this.siteSettings.fixed_category_positions) {
|
||||
categories = categories.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
const categoryIds = this.categories.map((category) => category.id);
|
||||
|
||||
return Category.sortCategories(categories).reduce(
|
||||
(filteredCategories, category) => {
|
||||
if (
|
||||
categoryIds.includes(category.id) &&
|
||||
canDisplayCategory(category.id, this.siteSettings)
|
||||
) {
|
||||
filteredCategories.push(category);
|
||||
}
|
||||
|
||||
return filteredCategories;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get sectionLinks() {
|
||||
return this.categories
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.reduce((links, category) => {
|
||||
links.push(
|
||||
new CategorySectionLink({
|
||||
category,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
currentUser: this.currentUser,
|
||||
})
|
||||
);
|
||||
|
||||
return links;
|
||||
}, []);
|
||||
return this.sortedCategories.map((category) => {
|
||||
return new CategorySectionLink({
|
||||
category,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
currentUser: this.currentUser,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@
|
||||
@model={{sectionLink.model}}
|
||||
@models={{sectionLink.models}}
|
||||
@prefixType={{sectionLink.prefixType}}
|
||||
@prefixValue={{sectionLink.prefixValue}} />
|
||||
@prefixValue={{sectionLink.prefixValue}}
|
||||
@suffixCSSClass={{sectionLink.suffixCSSClass}}
|
||||
@suffixValue={{sectionLink.suffixValue}}
|
||||
@suffixType={{sectionLink.suffixType}}/>
|
||||
{{/each}}
|
||||
|
||||
<Sidebar::MoreSectionLinks @sectionLinks={{this.moreSectionLinks}} @secondarySectionLinks={{this.moreSecondarySectionLinks}} />
|
||||
|
||||
@ -21,7 +21,10 @@
|
||||
@prefixType={{sectionLink.prefixType}}
|
||||
@prefixValue={{sectionLink.prefixValue}}
|
||||
@prefixColor={{sectionLink.prefixColor}}
|
||||
@prefixElementColors={{sectionLink.prefixElementColors}} >
|
||||
@prefixElementColors={{sectionLink.prefixElementColors}}
|
||||
@suffixCSSClass={{sectionLink.suffixCSSClass}}
|
||||
@suffixValue={{sectionLink.suffixValue}}
|
||||
@suffixType={{sectionLink.suffixType}} >
|
||||
</Sidebar::SectionLink>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
import Category from "discourse/models/category";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
|
||||
import { canDisplayCategory } from "discourse/lib/sidebar/helpers";
|
||||
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
|
||||
|
||||
export default class SidebarUserCategoriesSection extends SidebarCommonCategoriesSection {
|
||||
@ -24,10 +25,9 @@ export default class SidebarUserCategoriesSection extends SidebarCommonCategorie
|
||||
this.topicTrackingState.offStateChange(this.callbackId);
|
||||
}
|
||||
|
||||
@cached
|
||||
get categories() {
|
||||
return this.currentUser.sidebarCategories.filter((category) => {
|
||||
return canDisplayCategory(category, this.siteSettings);
|
||||
});
|
||||
return Category.findByIds(this.currentUser.sidebarCategoryIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
{{#each customSection.links as |link|}}
|
||||
<Sidebar::SectionLink
|
||||
@linkName={{link.name}}
|
||||
@class={{link.classNames}}
|
||||
@route={{link.route}}
|
||||
@model={{link.model}}
|
||||
@models={{link.models}}
|
||||
|
||||
@ -17,7 +17,10 @@
|
||||
@prefixType={{sectionLink.prefixType}}
|
||||
@prefixValue={{sectionLink.prefixValue}}
|
||||
@badgeText={{sectionLink.badgeText}}
|
||||
@models={{sectionLink.models}} >
|
||||
@models={{sectionLink.models}}
|
||||
@suffixCSSClass={{sectionLink.suffixCSSClass}}
|
||||
@suffixValue={{sectionLink.suffixValue}}
|
||||
@suffixType={{sectionLink.suffixType}} >
|
||||
</Sidebar::SectionLink>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
|
||||
@ -238,7 +238,7 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
this.currentUser.on("status-changed", this, "queueRerender");
|
||||
}
|
||||
|
||||
if (!this.siteSettings.enable_onboarding_popups) {
|
||||
if (!this.siteSettings.enable_user_tips) {
|
||||
if (
|
||||
this.currentUser &&
|
||||
!this.get("currentUser.read_first_notification")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { alias, and, or } from "@ember/object/computed";
|
||||
import { alias, or } from "@ember/object/computed";
|
||||
import { computed } from "@ember/object";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
@ -54,8 +54,6 @@ export default Component.extend({
|
||||
|
||||
inviteDisabled: or("topic.archived", "topic.closed", "topic.deleted"),
|
||||
|
||||
showEditOnFooter: and("topic.isPrivateMessage", "site.can_tag_pms"),
|
||||
|
||||
@discourseComputed("topic.message_archived")
|
||||
archiveIcon: (archived) => (archived ? "envelope" : "folder"),
|
||||
|
||||
|
||||
@ -7,8 +7,11 @@ import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item"
|
||||
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
|
||||
import Topic from "discourse/models/topic";
|
||||
import { mergeSortedLists } from "discourse/lib/utilities";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
@service store;
|
||||
|
||||
get dismissTypes() {
|
||||
return this.filterByTypes;
|
||||
}
|
||||
@ -22,7 +25,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
}
|
||||
|
||||
get showDismiss() {
|
||||
return this.#unreadMessaagesNotifications > 0;
|
||||
return this.#unreadMessagesNotifications > 0;
|
||||
}
|
||||
|
||||
get dismissTitle() {
|
||||
@ -37,7 +40,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
return "user-menu/messages-list-empty-state";
|
||||
}
|
||||
|
||||
get #unreadMessaagesNotifications() {
|
||||
get #unreadMessagesNotifications() {
|
||||
const key = `grouped_unread_notifications.${this.site.notification_types.private_message}`;
|
||||
// we're retrieving the value with get() so that Ember tracks the property
|
||||
// and re-renders the UI when it changes.
|
||||
@ -66,7 +69,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
);
|
||||
});
|
||||
|
||||
const topics = data.topics.map((t) => Topic.create(t));
|
||||
const topics = data.topics.map((t) => this.store.createRecord("topic", t));
|
||||
await Topic.applyTransformations(topics);
|
||||
|
||||
const readNotifications = await Notification.initializeNotifications(
|
||||
@ -100,7 +103,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
modalController.set(
|
||||
"confirmationMessage",
|
||||
I18n.t("notifications.dismiss_confirmation.body.messages", {
|
||||
count: this.#unreadMessaagesNotifications,
|
||||
count: this.#unreadMessagesNotifications,
|
||||
})
|
||||
);
|
||||
return modalController;
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
||||
{{#if this.siteSettings.enable_mentions}}
|
||||
{{#if @siteSettings.enable_mentions}}
|
||||
<li>
|
||||
<LinkTo @route="userNotifications.mentions">
|
||||
{{d-icon "at"}}
|
||||
|
||||
@ -2,6 +2,7 @@ import Controller, { inject as controller } from "@ember/controller";
|
||||
import { alias, equal, not } from "@ember/object/computed";
|
||||
import { action } from "@ember/object";
|
||||
import Category from "discourse/models/category";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
@ -21,6 +22,24 @@ export default Controller.extend({
|
||||
|
||||
loadedAllItems: not("discoveryTopics.model.canLoadMore"),
|
||||
|
||||
@discourseComputed(
|
||||
"router.currentRouteName",
|
||||
"router.currentRoute.queryParams.f",
|
||||
"site.show_welcome_topic_banner"
|
||||
)
|
||||
showEditWelcomeTopicBanner(
|
||||
currentRouteName,
|
||||
hasParams,
|
||||
showWelcomeTopicBanner
|
||||
) {
|
||||
return (
|
||||
this.currentUser?.staff &&
|
||||
currentRouteName === "discovery.latest" &&
|
||||
showWelcomeTopicBanner &&
|
||||
!hasParams
|
||||
);
|
||||
},
|
||||
|
||||
@action
|
||||
loadingBegan() {
|
||||
this.set("loading", true);
|
||||
|
||||
@ -24,20 +24,6 @@ const controllerOpts = {
|
||||
showTopicPostBadges: not("new"),
|
||||
redirectedReason: alias("currentUser.redirected_to_top.reason"),
|
||||
|
||||
@discourseComputed(
|
||||
"model.filter",
|
||||
"site.show_welcome_topic_banner",
|
||||
"model.listParams.f"
|
||||
)
|
||||
showEditWelcomeTopicBanner(filter, showWelcomeTopicBanner, hasListParams) {
|
||||
return (
|
||||
this.currentUser?.staff &&
|
||||
filter === "latest" &&
|
||||
showWelcomeTopicBanner &&
|
||||
!hasListParams
|
||||
);
|
||||
},
|
||||
|
||||
expandGloballyPinned: false,
|
||||
expandAllPinned: false,
|
||||
|
||||
|
||||
@ -223,21 +223,30 @@ export default Controller.extend(ModalFunctionality, {
|
||||
this.clearFlash();
|
||||
|
||||
if (
|
||||
(result.security_key_enabled || result.totp_enabled) &&
|
||||
(result.security_key_enabled ||
|
||||
result.totp_enabled ||
|
||||
result.backup_enabled) &&
|
||||
!this.secondFactorRequired
|
||||
) {
|
||||
let secondFactorMethod;
|
||||
if (result.security_key_enabled) {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.SECURITY_KEY;
|
||||
} else if (result.totp_enabled) {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.TOTP;
|
||||
} else {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.BACKUP_CODE;
|
||||
}
|
||||
this.setProperties({
|
||||
otherMethodAllowed: result.multiple_second_factor_methods,
|
||||
secondFactorRequired: true,
|
||||
showLoginButtons: false,
|
||||
backupEnabled: result.backup_enabled,
|
||||
showSecondFactor: result.totp_enabled,
|
||||
totpEnabled: result.totp_enabled,
|
||||
showSecondFactor: result.totp_enabled || result.backup_enabled,
|
||||
showSecurityKey: result.security_key_enabled,
|
||||
secondFactorMethod: result.security_key_enabled
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP,
|
||||
securityKeyChallenge: result.challenge,
|
||||
securityKeyAllowedCredentialIds: result.allowed_credential_ids,
|
||||
secondFactorMethod,
|
||||
});
|
||||
|
||||
// only need to focus the 2FA input for TOTP
|
||||
|
||||
@ -420,7 +420,7 @@ export default Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
resetSeenPopups() {
|
||||
resetSeenUserTips() {
|
||||
this.model.set("skip_new_user_tips", false);
|
||||
this.model.set("seen_popups", null);
|
||||
this.model.set("user_option.skip_new_user_tips", false);
|
||||
|
||||
@ -27,6 +27,7 @@ export default class extends Controller {
|
||||
@action
|
||||
save() {
|
||||
const initialSidebarCategoryIds = this.model.sidebarCategoryIds;
|
||||
const initialSidebarListDestination = this.model.sidebar_list_destination;
|
||||
|
||||
this.model.set(
|
||||
"sidebarCategoryIds",
|
||||
@ -59,6 +60,9 @@ export default class extends Controller {
|
||||
})
|
||||
.finally(() => {
|
||||
this.model.set("sidebar_tag_names", []);
|
||||
if (initialSidebarListDestination !== this.newSidebarListDestination) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,8 +564,8 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
return this.get("model.details").removeAllowedGroup(group);
|
||||
},
|
||||
|
||||
deleteTopic() {
|
||||
this.deleteTopic();
|
||||
deleteTopic(opts = {}) {
|
||||
this.deleteTopic(opts);
|
||||
},
|
||||
|
||||
// Archive a PM (as opposed to archiving a topic)
|
||||
@ -1522,7 +1522,11 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
this.model.recover();
|
||||
},
|
||||
|
||||
deleteTopic(opts) {
|
||||
deleteTopic(opts = {}) {
|
||||
if (opts.force_destroy) {
|
||||
return this.model.destroy(this.currentUser, opts);
|
||||
}
|
||||
|
||||
if (
|
||||
this.model.views > this.siteSettings.min_topic_views_for_delete_confirm
|
||||
) {
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { extendThemeCookie } from "discourse/lib/theme-selector";
|
||||
import { extendColorSchemeCookies } from "discourse/lib/color-scheme-picker";
|
||||
import { later } from "@ember/runloop";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
const DELAY = isTesting() ? 0 : 5000;
|
||||
|
||||
export default {
|
||||
name: "handle-cookies",
|
||||
|
||||
initialize() {
|
||||
// No need to block boot for this housekeeping - we can defer it a few seconds
|
||||
later(() => {
|
||||
extendThemeCookie();
|
||||
extendColorSchemeCookies();
|
||||
}, DELAY);
|
||||
},
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { addComposerUploadPreProcessor } from "discourse/components/composer-editor";
|
||||
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
export default {
|
||||
name: "register-media-optimization-upload-processor",
|
||||
@ -11,10 +12,15 @@ export default {
|
||||
UppyMediaOptimization,
|
||||
({ isMobileDevice }) => {
|
||||
return {
|
||||
optimizeFn: (data, opts) =>
|
||||
container
|
||||
optimizeFn: (data, opts) => {
|
||||
if (container.isDestroyed || container.isDestroying) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return container
|
||||
.lookup("service:media-optimization-worker")
|
||||
.optimizeImage(data, opts),
|
||||
.optimizeImage(data, opts);
|
||||
},
|
||||
runParallel: !isMobileDevice,
|
||||
};
|
||||
}
|
||||
|
||||
@ -89,14 +89,35 @@ export function loadColorSchemeStylesheet(
|
||||
);
|
||||
}
|
||||
|
||||
const COLOR_SCHEME_COOKIE_NAME = "color_scheme_id";
|
||||
const DARK_SCHEME_COOKIE_NAME = "dark_scheme_id";
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
|
||||
export function updateColorSchemeCookie(id, options = {}) {
|
||||
const cookieName = options.dark ? "dark_scheme_id" : "color_scheme_id";
|
||||
const cookieName = options.dark
|
||||
? DARK_SCHEME_COOKIE_NAME
|
||||
: COLOR_SCHEME_COOKIE_NAME;
|
||||
if (id) {
|
||||
cookie(cookieName, id, {
|
||||
path: "/",
|
||||
expires: 9999,
|
||||
expires: COOKIE_EXPIRY_DAYS,
|
||||
});
|
||||
} else {
|
||||
removeCookie(cookieName, { path: "/", expires: 1 });
|
||||
removeCookie(cookieName, { path: "/" });
|
||||
}
|
||||
}
|
||||
|
||||
export function extendColorSchemeCookies() {
|
||||
for (const cookieName of [
|
||||
COLOR_SCHEME_COOKIE_NAME,
|
||||
DARK_SCHEME_COOKIE_NAME,
|
||||
]) {
|
||||
const currentValue = cookie(cookieName);
|
||||
if (currentValue) {
|
||||
cookie(cookieName, currentValue, {
|
||||
path: "/",
|
||||
expires: COOKIE_EXPIRY_DAYS,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,7 +225,15 @@ class PluginApi {
|
||||
|
||||
if (canModify(klass, "member", resolverName, changes)) {
|
||||
delete changes.pluginId;
|
||||
klass.class.reopen(changes);
|
||||
|
||||
if (klass.class.reopen) {
|
||||
klass.class.reopen(changes);
|
||||
} else {
|
||||
Object.defineProperties(
|
||||
klass.class.prototype || klass.class,
|
||||
Object.getOwnPropertyDescriptors(changes)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return klass;
|
||||
|
||||
@ -9,6 +9,13 @@ export default class BaseCustomSidebarSectionLink {
|
||||
this._notImplemented();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The classnames of the section link.
|
||||
*/
|
||||
get classNames() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The Ember route which the section link should link to.
|
||||
*/
|
||||
|
||||
@ -7,6 +7,8 @@ import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sideb
|
||||
export default class EverythingSectionLink extends BaseSectionLink {
|
||||
@tracked totalUnread = 0;
|
||||
@tracked totalNew = 0;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@ -50,6 +52,9 @@ export default class EverythingSectionLink extends BaseSectionLink {
|
||||
}
|
||||
|
||||
get badgeText() {
|
||||
if (this.hideCount) {
|
||||
return;
|
||||
}
|
||||
if (this.totalUnread > 0) {
|
||||
return I18n.t("sidebar.unread_count", {
|
||||
count: this.totalUnread,
|
||||
@ -78,4 +83,18 @@ export default class EverythingSectionLink extends BaseSectionLink {
|
||||
get prefixValue() {
|
||||
return "layer-group";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this.hideCount && (this.totalUnread || this.totalNew)) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
export function canDisplayCategory(category, siteSettings) {
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export function canDisplayCategory(categoryId, siteSettings) {
|
||||
if (siteSettings.allow_uncategorized_topics) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !category.isUncategorizedCategory;
|
||||
return !Category.isUncategorized(categoryId);
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sideb
|
||||
export default class CategorySectionLink {
|
||||
@tracked totalUnread = 0;
|
||||
@tracked totalNew = 0;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
|
||||
constructor({ category, topicTrackingState, currentUser }) {
|
||||
this.category = category;
|
||||
@ -69,6 +71,9 @@ export default class CategorySectionLink {
|
||||
}
|
||||
|
||||
get badgeText() {
|
||||
if (this.hideCount) {
|
||||
return;
|
||||
}
|
||||
if (this.totalUnread > 0) {
|
||||
return I18n.t("sidebar.unread_count", {
|
||||
count: this.totalUnread,
|
||||
@ -91,4 +96,18 @@ export default class CategorySectionLink {
|
||||
}
|
||||
return "discovery.category";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this.hideCount && (this.totalUnread || this.totalNew)) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,14 @@ import I18n from "I18n";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
import BaseSectionLink from "discourse/lib/sidebar/base-community-section-link";
|
||||
import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar";
|
||||
|
||||
const USER_DRAFTS_CHANGED_EVENT = "user-drafts:changed";
|
||||
|
||||
export default class MyPostsSectionLink extends BaseSectionLink {
|
||||
@tracked draftCount = this.currentUser.draft_count;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@ -52,7 +55,7 @@ export default class MyPostsSectionLink extends BaseSectionLink {
|
||||
}
|
||||
|
||||
get badgeText() {
|
||||
if (this._hasDraft) {
|
||||
if (this._hasDraft && !this.hideCount) {
|
||||
return I18n.t("sidebar.sections.community.links.my_posts.draft_count", {
|
||||
count: this.draftCount,
|
||||
});
|
||||
@ -66,4 +69,18 @@ export default class MyPostsSectionLink extends BaseSectionLink {
|
||||
get prefixValue() {
|
||||
return "user";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this._hasDraft && this.hideCount) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sideb
|
||||
export default class TrackedSectionLink extends BaseSectionLink {
|
||||
@tracked totalUnread = 0;
|
||||
@tracked totalNew = 0;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@ -51,6 +53,9 @@ export default class TrackedSectionLink extends BaseSectionLink {
|
||||
}
|
||||
|
||||
get badgeText() {
|
||||
if (this.hideCount) {
|
||||
return;
|
||||
}
|
||||
if (this.totalUnread > 0) {
|
||||
return I18n.t("sidebar.unread_count", {
|
||||
count: this.totalUnread,
|
||||
@ -79,4 +84,18 @@ export default class TrackedSectionLink extends BaseSectionLink {
|
||||
get prefixValue() {
|
||||
return "bell";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this.hideCount && (this.totalUnread || this.totalNew)) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sideb
|
||||
export default class TagSectionLink extends BaseTagSectionLink {
|
||||
@tracked totalUnread = 0;
|
||||
@tracked totalNew = 0;
|
||||
@tracked hideCount =
|
||||
this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION;
|
||||
|
||||
constructor({ topicTrackingState, currentUser }) {
|
||||
super(...arguments);
|
||||
@ -51,6 +53,9 @@ export default class TagSectionLink extends BaseTagSectionLink {
|
||||
}
|
||||
|
||||
get badgeText() {
|
||||
if (this.hideCount) {
|
||||
return;
|
||||
}
|
||||
if (this.totalUnread > 0) {
|
||||
return I18n.t("sidebar.unread_count", {
|
||||
count: this.totalUnread,
|
||||
@ -61,4 +66,18 @@ export default class TagSectionLink extends BaseTagSectionLink {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "unread";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
if (this.hideCount && (this.totalUnread || this.totalNew)) {
|
||||
return "circle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import I18n from "I18n";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
const keySelector = "meta[name=discourse_theme_id]";
|
||||
const COOKIE_NAME = "theme_ids";
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
|
||||
export function currentThemeKey() {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -35,12 +37,22 @@ export function currentThemeId() {
|
||||
export function setLocalTheme(ids, themeSeq) {
|
||||
ids = ids.reject((id) => !id);
|
||||
if (ids && ids.length > 0) {
|
||||
cookie("theme_ids", `${ids.join(",")}|${themeSeq}`, {
|
||||
cookie(COOKIE_NAME, `${ids.join(",")}|${themeSeq}`, {
|
||||
path: "/",
|
||||
expires: 9999,
|
||||
expires: COOKIE_EXPIRY_DAYS,
|
||||
});
|
||||
} else {
|
||||
removeCookie("theme_ids", { path: "/", expires: 1 });
|
||||
removeCookie(COOKIE_NAME, { path: "/" });
|
||||
}
|
||||
}
|
||||
|
||||
export function extendThemeCookie() {
|
||||
const currentValue = cookie(COOKIE_NAME);
|
||||
if (currentValue) {
|
||||
cookie(COOKIE_NAME, currentValue, {
|
||||
path: "/",
|
||||
expires: COOKIE_EXPIRY_DAYS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -117,15 +117,6 @@ const DiscourseURL = EmberObject.extend({
|
||||
|
||||
if (!holder) {
|
||||
selector = holderId;
|
||||
|
||||
if (
|
||||
document.getElementsByClassName(
|
||||
`topic-post-visited-line post-${postNumber - 1}`
|
||||
)?.length === 1
|
||||
) {
|
||||
selector = ".small-action.topic-post-visited";
|
||||
}
|
||||
|
||||
holder = document.querySelector(selector);
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import tippy from "tippy.js";
|
||||
const instances = {};
|
||||
const queue = [];
|
||||
|
||||
export function showPopup(options) {
|
||||
hidePopup(options.id);
|
||||
export function showUserTip(options) {
|
||||
hideUserTip(options.id);
|
||||
|
||||
if (!options.reference) {
|
||||
return;
|
||||
@ -23,7 +23,7 @@ export function showPopup(options) {
|
||||
showOnCreate: true,
|
||||
hideOnClick: false,
|
||||
trigger: "manual",
|
||||
theme: "d-onboarding",
|
||||
theme: "user-tips",
|
||||
|
||||
// It must be interactive to make buttons work.
|
||||
interactive: true,
|
||||
@ -40,17 +40,15 @@ export function showPopup(options) {
|
||||
allowHTML: true,
|
||||
|
||||
content: `
|
||||
<div class='onboarding-popup-container'>
|
||||
<div class='onboarding-popup-title'>${escape(options.titleText)}</div>
|
||||
<div class='onboarding-popup-content'>${escape(
|
||||
options.contentText
|
||||
)}</div>
|
||||
<div class='onboarding-popup-buttons'>
|
||||
<div class='user-tip-container'>
|
||||
<div class='user-tip-title'>${escape(options.titleText)}</div>
|
||||
<div class='user-tip-content'>${escape(options.contentText)}</div>
|
||||
<div class='user-tip-buttons'>
|
||||
<button class="btn btn-primary btn-dismiss">${escape(
|
||||
options.primaryBtnText || I18n.t("popup.primary")
|
||||
options.primaryBtnText || I18n.t("user_tips.primary")
|
||||
)}</button>
|
||||
<button class="btn btn-flat btn-text btn-dismiss-all">${escape(
|
||||
options.secondaryBtnText || I18n.t("popup.secondary")
|
||||
options.secondaryBtnText || I18n.t("user_tips.secondary")
|
||||
)}</button>
|
||||
</div>
|
||||
</div>`,
|
||||
@ -73,12 +71,12 @@ export function showPopup(options) {
|
||||
});
|
||||
}
|
||||
|
||||
export function hidePopup(popupId) {
|
||||
const instance = instances[popupId];
|
||||
export function hideUserTip(userTipId) {
|
||||
const instance = instances[userTipId];
|
||||
if (instance && !instance.state.isDestroyed) {
|
||||
instance.destroy();
|
||||
}
|
||||
delete instances[popupId];
|
||||
delete instances[userTipId];
|
||||
}
|
||||
|
||||
function addToQueue(options) {
|
||||
@ -92,9 +90,9 @@ function addToQueue(options) {
|
||||
queue.push(options);
|
||||
}
|
||||
|
||||
export function showNextPopup() {
|
||||
export function showNextUserTip() {
|
||||
const options = queue.shift();
|
||||
if (options) {
|
||||
showPopup(options);
|
||||
showUserTip(options);
|
||||
}
|
||||
}
|
||||
@ -280,10 +280,12 @@ export default Mixin.create({
|
||||
// note: we DO NOT use afterRender here cause _positionCard may
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
discourseLater(() => {
|
||||
const firstLink = this.element.querySelector("a");
|
||||
firstLink && firstLink.focus();
|
||||
}, 350);
|
||||
if (event.pointerId === -1) {
|
||||
discourseLater(() => {
|
||||
const firstLink = this.element.querySelector("a");
|
||||
firstLink && firstLink.focus();
|
||||
}, 350);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -337,13 +337,35 @@ const Category = RestModel.extend({
|
||||
|
||||
@discourseComputed("id")
|
||||
isUncategorizedCategory(id) {
|
||||
return id === Site.currentProp("uncategorized_category_id");
|
||||
return Category.isUncategorized(id);
|
||||
},
|
||||
});
|
||||
|
||||
let _uncategorized;
|
||||
|
||||
Category.reopenClass({
|
||||
// Sort subcategories directly under parents
|
||||
sortCategories(categories) {
|
||||
const children = new Map();
|
||||
|
||||
categories.forEach((category) => {
|
||||
const parentId = parseInt(category.parent_category_id, 10) || -1;
|
||||
const group = children.get(parentId) || [];
|
||||
group.pushObject(category);
|
||||
|
||||
children.set(parentId, group);
|
||||
});
|
||||
|
||||
const reduce = (values) =>
|
||||
values.flatMap((c) => [c, reduce(children.get(c.id) || [])]).flat();
|
||||
|
||||
return reduce(children.get(-1));
|
||||
},
|
||||
|
||||
isUncategorized(categoryId) {
|
||||
return categoryId === Site.currentProp("uncategorized_category_id");
|
||||
},
|
||||
|
||||
slugEncoded() {
|
||||
let siteSettings = getOwner(this).lookup("service:site-settings");
|
||||
return siteSettings.slug_generation_method === "encoded";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import EmberObject, { get } from "@ember/object";
|
||||
import { alias, sort } from "@ember/object/computed";
|
||||
import Archetype from "discourse/models/archetype";
|
||||
import Category from "discourse/models/category";
|
||||
import PostActionType from "discourse/models/post-action-type";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import RestModel from "discourse/models/rest";
|
||||
@ -59,27 +60,14 @@ const Site = RestModel.extend({
|
||||
// Sort subcategories under parents
|
||||
@discourseComputed("categoriesByCount", "categories.[]")
|
||||
sortedCategories(categories) {
|
||||
const children = new Map();
|
||||
|
||||
categories.forEach((category) => {
|
||||
const parentId = parseInt(category.parent_category_id, 10) || -1;
|
||||
const group = children.get(parentId) || [];
|
||||
group.pushObject(category);
|
||||
|
||||
children.set(parentId, group);
|
||||
});
|
||||
|
||||
const reduce = (values) =>
|
||||
values.flatMap((c) => [c, reduce(children.get(c.id) || [])]).flat();
|
||||
|
||||
return reduce(children.get(-1));
|
||||
return Category.sortCategories(categories);
|
||||
},
|
||||
|
||||
// Returns it in the correct order, by setting
|
||||
@discourseComputed("categories.[]")
|
||||
categoriesList() {
|
||||
categoriesList(categories) {
|
||||
return this.siteSettings.fixed_category_positions
|
||||
? this.categories
|
||||
? categories
|
||||
: this.sortedCategories;
|
||||
},
|
||||
|
||||
@ -158,7 +146,7 @@ Site.reopenClass(Singleton, {
|
||||
if (result.categories) {
|
||||
let subcatMap = {};
|
||||
|
||||
result.categoriesById = {};
|
||||
result.categoriesById = new Map();
|
||||
result.categories = result.categories.map((c) => {
|
||||
if (c.parent_category_id) {
|
||||
subcatMap[c.parent_category_id] =
|
||||
|
||||
@ -207,7 +207,7 @@ const TopicTrackingState = EmberObject.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (filterTag && !data.payload.tags.includes(filterTag)) {
|
||||
if (filterTag && !data.payload.tags?.includes(filterTag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -270,15 +270,19 @@ const Topic = RestModel.extend({
|
||||
return customUrl;
|
||||
}
|
||||
|
||||
if (highestPostNumber <= lastReadPostNumber) {
|
||||
if (this.get("category.navigate_to_first_post_after_read")) {
|
||||
return this.urlForPostNumber(1);
|
||||
} else {
|
||||
return this.urlForPostNumber(lastReadPostNumber + 1);
|
||||
}
|
||||
} else {
|
||||
return this.urlForPostNumber(lastReadPostNumber + 1);
|
||||
if (
|
||||
lastReadPostNumber >= highestPostNumber &&
|
||||
this.get("category.navigate_to_first_post_after_read")
|
||||
) {
|
||||
return this.urlForPostNumber(1);
|
||||
}
|
||||
|
||||
let postNumber = lastReadPostNumber + 1;
|
||||
if (postNumber > highestPostNumber) {
|
||||
postNumber = highestPostNumber;
|
||||
}
|
||||
|
||||
return this.urlForPostNumber(postNumber);
|
||||
},
|
||||
|
||||
@discourseComputed("highest_post_number", "url")
|
||||
|
||||
@ -43,7 +43,11 @@ import Evented from "@ember/object/evented";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { hidePopup, showNextPopup, showPopup } from "discourse/lib/popup";
|
||||
import {
|
||||
hideUserTip,
|
||||
showNextUserTip,
|
||||
showUserTip,
|
||||
} from "discourse/lib/user-tips";
|
||||
|
||||
export const SECOND_FACTOR_METHODS = {
|
||||
TOTP: 1,
|
||||
@ -338,18 +342,6 @@ const User = RestModel.extend({
|
||||
},
|
||||
|
||||
sidebarTagNames: mapBy("sidebarTags", "name"),
|
||||
|
||||
@discourseComputed("sidebar_category_ids.[]")
|
||||
sidebarCategories(sidebarCategoryIds) {
|
||||
if (!sidebarCategoryIds || sidebarCategoryIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Site.current().categoriesList.filter((category) =>
|
||||
sidebarCategoryIds.includes(category.id)
|
||||
);
|
||||
},
|
||||
|
||||
sidebarListDestination: readOnly("sidebar_list_destination"),
|
||||
|
||||
changeUsername(new_username) {
|
||||
@ -1102,57 +1094,68 @@ const User = RestModel.extend({
|
||||
return [...trackedTags, ...watchedTags, ...watchingFirstPostTags];
|
||||
},
|
||||
|
||||
showPopup(options) {
|
||||
const popupTypes = Site.currentProp("onboarding_popup_types");
|
||||
if (!popupTypes[options.id]) {
|
||||
showUserTip(options) {
|
||||
const userTips = Site.currentProp("user_tips");
|
||||
if (!userTips || this.skip_new_user_tips) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userTips[options.id]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Cannot display popup with type =", options.id);
|
||||
console.warn("Cannot show user tip with type =", options.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const seenPopups = this.seen_popups || [];
|
||||
if (seenPopups.includes(popupTypes[options.id])) {
|
||||
const seenUserTips = this.seen_popups || [];
|
||||
if (
|
||||
seenUserTips.includes(-1) ||
|
||||
seenUserTips.includes(userTips[options.id])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
showPopup({
|
||||
showUserTip({
|
||||
...options,
|
||||
onDismiss: () => this.hidePopupForever(options.id),
|
||||
onDismissAll: () => this.hidePopupForever(),
|
||||
onDismiss: () => this.hideUserTipForever(options.id),
|
||||
onDismissAll: () => this.hideUserTipForever(),
|
||||
});
|
||||
},
|
||||
|
||||
hidePopupForever(popupId) {
|
||||
// Empty popupId means all popups.
|
||||
const popupTypes = Site.currentProp("onboarding_popup_types");
|
||||
if (popupId && !popupTypes[popupId]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Cannot hide popup with type =", popupId);
|
||||
hideUserTipForever(userTipId) {
|
||||
const userTips = Site.currentProp("user_tips");
|
||||
if (!userTips || this.skip_new_user_tips) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide any shown popups.
|
||||
let seenPopups = this.seen_popups || [];
|
||||
if (popupId) {
|
||||
hidePopup(popupId);
|
||||
if (!seenPopups.includes(popupTypes[popupId])) {
|
||||
seenPopups.push(popupTypes[popupId]);
|
||||
}
|
||||
} else {
|
||||
Object.keys(popupTypes).forEach(hidePopup);
|
||||
seenPopups = Object.values(popupTypes);
|
||||
// Empty userTipId means all user tips.
|
||||
if (userTipId && !userTips[userTipId]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Cannot hide user tip with type =", userTipId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show next popup in queue.
|
||||
showNextPopup();
|
||||
// Hide any shown user tips.
|
||||
let seenUserTips = this.seen_popups || [];
|
||||
if (userTipId) {
|
||||
hideUserTip(userTipId);
|
||||
if (!seenUserTips.includes(userTips[userTipId])) {
|
||||
seenUserTips.push(userTips[userTipId]);
|
||||
}
|
||||
} else {
|
||||
Object.keys(userTips).forEach(hideUserTip);
|
||||
seenUserTips = [-1];
|
||||
}
|
||||
|
||||
// Save seen popups on the server.
|
||||
// Show next user tip in queue.
|
||||
showNextUserTip();
|
||||
|
||||
// Save seen user tips on the server.
|
||||
if (!this.user_option) {
|
||||
this.set("user_option", {});
|
||||
}
|
||||
this.set("seen_popups", seenPopups);
|
||||
this.set("user_option.seen_popups", seenPopups);
|
||||
if (popupId) {
|
||||
this.set("seen_popups", seenUserTips);
|
||||
this.set("user_option.seen_popups", seenUserTips);
|
||||
if (userTipId) {
|
||||
return this.save(["seen_popups"]);
|
||||
} else {
|
||||
this.set("skip_new_user_tips", true);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
showFooter: true,
|
||||
@ -6,7 +7,7 @@ export default RestrictedUserRoute.extend({
|
||||
setupController(controller, user) {
|
||||
const props = {
|
||||
model: user,
|
||||
selectedSidebarCategories: user.sidebarCategories,
|
||||
selectedSidebarCategories: Category.findByIds(user.sidebarCategoryIds),
|
||||
};
|
||||
|
||||
if (this.siteSettings.tagging_enabled) {
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
|
||||
<p><TagChooser @tags={{this.tags}} @categoryId={{this.categoryId}} /></p>
|
||||
|
||||
<DButton @action={{this.action}} @disabled={{this.emptyTags}} @label={{concat "topics.bulk." this.label}} />
|
||||
<DButton @action={{action this.action}} @disabled={{this.emptyTags}} @label={{concat "topics.bulk." this.label}} />
|
||||
|
||||
@ -10,8 +10,7 @@
|
||||
|
||||
{{#unless this.showPositionInput}}
|
||||
<section class="field position-disabled">
|
||||
{{i18n "category.position_disabled"}}
|
||||
<a href={{get-url "/admin/site_settings/category/basic"}}>{{i18n "category.position_disabled_click"}}</a>
|
||||
{{html-safe (i18n "category.position_disabled" url=(get-url "/admin/site_settings/category/all_results?filter=fixed_category_positions"))}}
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
|
||||
<PluginOutlet @name="discovery-above" @tagName="span" @connectorTagName="div" />
|
||||
|
||||
{{#if this.showEditWelcomeTopicBanner}}
|
||||
<WelcomeTopicBanner />
|
||||
{{/if}}
|
||||
|
||||
<div class="container list-container {{if this.loading "hidden"}}">
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
|
||||
@ -2,10 +2,6 @@
|
||||
<div class="alert alert-info">{{this.redirectedReason}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showEditWelcomeTopicBanner}}
|
||||
<WelcomeTopicBanner />
|
||||
{{/if}}
|
||||
|
||||
<TopicDismissButtons @position="top" @selectedTopics={{this.selected}} @model={{this.model}} @showResetNew={{this.showResetNew}} @showDismissRead={{this.showDismissRead}} @resetNew={{action "resetNew"}} />
|
||||
|
||||
{{#if this.model.sharedDrafts}}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SecondFactorForm @secondFactorMethod={{this.secondFactorMethod}} @secondFactorToken={{this.secondFactorToken}} @class={{this.secondFactorClass}} @backupEnabled={{this.backupEnabled}} @isLogin={{true}}>
|
||||
<SecondFactorForm @secondFactorMethod={{this.secondFactorMethod}} @secondFactorToken={{this.secondFactorToken}} @class={{this.secondFactorClass}} @backupEnabled={{this.backupEnabled}} @totpEnabled={{this.totpEnabled}} @isLogin={{true}}>
|
||||
{{#if this.showSecurityKey}}
|
||||
<SecurityKeyForm @allowedCredentialIds={{this.securityKeyAllowedCredentialIds}} @challenge={{this.securityKeyChallenge}} @showSecurityKey={{this.showSecurityKey}} @showSecondFactor={{this.showSecondFactor}} @secondFactorMethod={{this.secondFactorMethod}} @otherMethodAllowed={{this.otherMethodAllowed}} @action={{action "authenticateSecurityKey"}}>
|
||||
</SecurityKeyForm>
|
||||
|
||||
@ -117,8 +117,8 @@
|
||||
<ComboBox @valueProperty="value" @content={{this.titleCountModes}} @value={{this.model.user_option.title_count_mode}} @id="user-title-count-mode" @onChange={{action (mut this.model.user_option.title_count_mode)}} />
|
||||
</div>
|
||||
<PreferenceCheckbox @labelKey="user.skip_new_user_tips.description" @checked={{this.model.user_option.skip_new_user_tips}} @class="pref-new-user-tips" />
|
||||
{{#if this.site.onboarding_popup_types}}
|
||||
<DButton @class="pref-reset-seen-popups" @action={{action "resetSeenPopups"}}>{{i18n "user.reset_seen_popups"}}</DButton>
|
||||
{{#if this.site.user_tips}}
|
||||
<DButton @class="pref-reset-seen-user-tips" @action={{action "resetSeenUserTips"}}>{{i18n "user.reset_seen_user_tips"}}</DButton>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<PluginOutlet @name="discovery-navigation-bar-above" @connectorTagName="div" />
|
||||
<div class="container">
|
||||
<section class="navigation-container tag-navigation">
|
||||
<DNavigation @filterMode={{this.filterMode}} @canCreateTopic={{this.canCreateTopic}} @hasDraft={{this.currentUser.has_topic_draft}} @createTopic={{route-action "createTopic"}} @category={{this.category}} @editCategory={{route-action "editCategory" this.category}} @tag={{this.tag}} @noSubcategories={{this.noSubcategories}} @tagNotification={{this.tagNotification}} @additionalTags={{this.additionalTags}} @showInfo={{this.showInfo}} @canCreateTopicOnTag={{this.canCreateTopicOnTag}} @changeTagNotificationLevel={{action "changeTagNotificationLevel"}} @toggleInfo={{action "toggleInfo"}} />
|
||||
<DNavigation @filterMode={{this.filterMode}} @canCreateTopic={{this.canCreateTopic}} @hasDraft={{this.currentUser.has_topic_draft}} @createTopic={{route-action "createTopic"}} @category={{this.category}} @editCategory={{route-action "editCategory" this.category}} @tag={{this.tag}} @noSubcategories={{this.noSubcategories}} @tagNotification={{this.tagNotification}} @additionalTags={{this.additionalTags}} @showInfo={{this.showInfo}} @canCreateTopicOnTag={{this.canCreateTopicOnTag}} @createTopicDisabled={{this.createTopicDisabled}} @changeTagNotificationLevel={{action "changeTagNotificationLevel"}} @toggleInfo={{action "toggleInfo"}} />
|
||||
|
||||
<PluginOutlet @name="tag-navigation" @connectorTagName="div" @args={{hash category=this.category tag=this.tag}} />
|
||||
</section>
|
||||
|
||||
@ -13,7 +13,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { logSearchLinkClick } from "discourse/lib/search";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { hidePopup } from "discourse/lib/popup";
|
||||
import { hideUserTip } from "discourse/lib/user-tips";
|
||||
|
||||
let _extraHeaderIcons = [];
|
||||
|
||||
@ -88,7 +88,7 @@ createWidget("header-notifications", {
|
||||
const count = unread + reviewables;
|
||||
if (count > 0) {
|
||||
if (this._shouldHighlightAvatar()) {
|
||||
if (this.siteSettings.enable_onboarding_popups) {
|
||||
if (this.siteSettings.enable_user_tips) {
|
||||
contents.push(h("span.ring"));
|
||||
} else {
|
||||
this._addAvatarHighlight(contents);
|
||||
@ -124,7 +124,7 @@ createWidget("header-notifications", {
|
||||
const unreadHighPriority = user.unread_high_priority_notifications;
|
||||
if (!!unreadHighPriority) {
|
||||
if (this._shouldHighlightAvatar()) {
|
||||
if (this.siteSettings.enable_onboarding_popups) {
|
||||
if (this.siteSettings.enable_user_tips) {
|
||||
contents.push(h("span.ring"));
|
||||
} else {
|
||||
this._addAvatarHighlight(contents);
|
||||
@ -198,17 +198,17 @@ createWidget("header-notifications", {
|
||||
didRenderWidget() {
|
||||
if (
|
||||
!this.currentUser ||
|
||||
!this.siteSettings.enable_onboarding_popups ||
|
||||
!this.siteSettings.enable_user_tips ||
|
||||
!this._shouldHighlightAvatar()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentUser.showPopup({
|
||||
this.currentUser.showUserTip({
|
||||
id: "first_notification",
|
||||
|
||||
titleText: I18n.t("popup.first_notification.title"),
|
||||
contentText: I18n.t("popup.first_notification.content"),
|
||||
titleText: I18n.t("user_tips.first_notification.title"),
|
||||
contentText: I18n.t("user_tips.first_notification.content"),
|
||||
|
||||
reference: document
|
||||
.querySelector(".badge-notification")
|
||||
@ -219,11 +219,11 @@ createWidget("header-notifications", {
|
||||
},
|
||||
|
||||
destroy() {
|
||||
hidePopup("first_notification");
|
||||
hideUserTip("first_notification");
|
||||
},
|
||||
|
||||
willRerenderWidget() {
|
||||
hidePopup("first_notification");
|
||||
hideUserTip("first_notification");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -367,7 +367,7 @@ export default createWidget("search-menu", {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === 65 /* a */) {
|
||||
if (e.key === "A") {
|
||||
if (document.activeElement?.classList.contains("search-link")) {
|
||||
if (document.querySelector("#reply-control.open")) {
|
||||
// add a link and focus composer
|
||||
@ -388,8 +388,8 @@ export default createWidget("search-menu", {
|
||||
}
|
||||
}
|
||||
|
||||
const up = e.which === 38;
|
||||
const down = e.which === 40;
|
||||
const up = e.key === "ArrowUp";
|
||||
const down = e.key === "ArrowDown";
|
||||
if (up || down) {
|
||||
let focused = document.activeElement.closest(".search-menu")
|
||||
? document.activeElement
|
||||
@ -443,7 +443,7 @@ export default createWidget("search-menu", {
|
||||
}
|
||||
|
||||
const searchInput = document.querySelector("#search-term");
|
||||
if (e.which === 13 && e.target === searchInput) {
|
||||
if (e.key === "Enter" && e.target === searchInput) {
|
||||
const recentEnterHit =
|
||||
this.state._lastEnterTimestamp &&
|
||||
Date.now() - this.state._lastEnterTimestamp < SECOND_ENTER_MAX_DELAY;
|
||||
@ -463,7 +463,7 @@ export default createWidget("search-menu", {
|
||||
this.state._lastEnterTimestamp = Date.now();
|
||||
}
|
||||
|
||||
if (e.target === searchInput && e.which === 8 /* backspace */) {
|
||||
if (e.target === searchInput && e.key === "Backspace") {
|
||||
if (!searchInput.value) {
|
||||
this.clearTopicContext();
|
||||
this.clearPMInboxContext();
|
||||
|
||||
@ -9,7 +9,7 @@ import discourseLater from "discourse-common/lib/later";
|
||||
import { relativeAge } from "discourse/lib/formatter";
|
||||
import renderTags from "discourse/lib/render-tags";
|
||||
import renderTopicFeaturedLink from "discourse/lib/render-topic-featured-link";
|
||||
import { hidePopup } from "discourse/lib/popup";
|
||||
import { hideUserTip } from "discourse/lib/user-tips";
|
||||
|
||||
const SCROLLER_HEIGHT = 50;
|
||||
const LAST_READ_HEIGHT = 20;
|
||||
@ -601,15 +601,15 @@ export default createWidget("topic-timeline", {
|
||||
},
|
||||
|
||||
didRenderWidget() {
|
||||
if (!this.currentUser || !this.siteSettings.enable_onboarding_popups) {
|
||||
if (!this.currentUser || !this.siteSettings.enable_user_tips) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentUser.showPopup({
|
||||
this.currentUser.showUserTip({
|
||||
id: "topic_timeline",
|
||||
|
||||
titleText: I18n.t("popup.topic_timeline.title"),
|
||||
contentText: I18n.t("popup.topic_timeline.content"),
|
||||
titleText: I18n.t("user_tips.topic_timeline.title"),
|
||||
contentText: I18n.t("user_tips.topic_timeline.content"),
|
||||
|
||||
reference: document.querySelector("div.timeline-scrollarea-wrapper"),
|
||||
|
||||
@ -618,10 +618,10 @@ export default createWidget("topic-timeline", {
|
||||
},
|
||||
|
||||
destroy() {
|
||||
hidePopup("topic_timeline");
|
||||
hideUserTip("topic_timeline");
|
||||
},
|
||||
|
||||
willRerenderWidget() {
|
||||
hidePopup("topic_timeline");
|
||||
hideUserTip("topic_timeline");
|
||||
},
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ const mergeTrees = require("broccoli-merge-trees");
|
||||
const concat = require("broccoli-concat");
|
||||
const prettyTextEngine = require("./lib/pretty-text-engine");
|
||||
const { createI18nTree } = require("./lib/translation-plugin");
|
||||
const { parsePluginClientSettings } = require("./lib/site-settings-plugin");
|
||||
const discourseScss = require("./lib/discourse-scss");
|
||||
const generateScriptsTree = require("./lib/scripts");
|
||||
const funnel = require("broccoli-funnel");
|
||||
@ -57,6 +58,13 @@ module.exports = function (defaults) {
|
||||
autoImport: {
|
||||
forbidEval: true,
|
||||
insertScriptsAt: "ember-auto-import-scripts",
|
||||
webpack: {
|
||||
// Workarounds for https://github.com/ef4/ember-auto-import/issues/519 and https://github.com/ef4/ember-auto-import/issues/478
|
||||
devtool: isProduction ? false : "source-map", // Sourcemaps contain reference to the ephemeral broccoli cache dir, which changes on every deploy
|
||||
optimization: {
|
||||
moduleIds: "size", // Consistent module references https://github.com/ef4/ember-auto-import/issues/478#issuecomment-1000526638
|
||||
},
|
||||
},
|
||||
},
|
||||
fingerprint: {
|
||||
// Handled by Rails asset pipeline
|
||||
@ -161,6 +169,7 @@ module.exports = function (defaults) {
|
||||
|
||||
return mergeTrees([
|
||||
createI18nTree(discourseRoot, vendorJs),
|
||||
parsePluginClientSettings(discourseRoot, vendorJs, app),
|
||||
app.toTree(),
|
||||
funnel(`${discourseRoot}/public/javascripts`, { destDir: "javascripts" }),
|
||||
funnel(`${vendorJs}/highlightjs`, {
|
||||
|
||||
@ -2651,9 +2651,9 @@ loader-runner@^4.2.0:
|
||||
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
|
||||
integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
||||
96
app/assets/javascripts/discourse/lib/site-settings-plugin.js
Normal file
96
app/assets/javascripts/discourse/lib/site-settings-plugin.js
Normal file
@ -0,0 +1,96 @@
|
||||
const Plugin = require("broccoli-plugin");
|
||||
const Yaml = require("js-yaml");
|
||||
const fs = require("fs");
|
||||
const concat = require("broccoli-concat");
|
||||
const mergeTrees = require("broccoli-merge-trees");
|
||||
const deepmerge = require("deepmerge");
|
||||
const glob = require("glob");
|
||||
const { shouldLoadPluginTestJs } = require("discourse/lib/plugin-js");
|
||||
|
||||
let built = false;
|
||||
|
||||
class SiteSettingsPlugin extends Plugin {
|
||||
constructor(inputNodes, inputFile, options) {
|
||||
super(inputNodes, {
|
||||
...options,
|
||||
persistentOutput: true,
|
||||
});
|
||||
}
|
||||
|
||||
build() {
|
||||
if (built) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed = {};
|
||||
|
||||
this.inputPaths.forEach((path) => {
|
||||
let inputFile;
|
||||
if (path.includes("plugins")) {
|
||||
inputFile = "settings.yml";
|
||||
} else {
|
||||
inputFile = "site_settings.yml";
|
||||
}
|
||||
const file = path + "/" + inputFile;
|
||||
let yaml;
|
||||
try {
|
||||
yaml = fs.readFileSync(file, { encoding: "UTF-8" });
|
||||
} catch (err) {
|
||||
// the plugin does not have a config file, go to the next file
|
||||
return;
|
||||
}
|
||||
const loaded = Yaml.load(yaml, { json: true });
|
||||
parsed = deepmerge(parsed, loaded);
|
||||
});
|
||||
|
||||
let clientSettings = {};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const [category, settings] of Object.entries(parsed)) {
|
||||
for (const [setting, details] of Object.entries(settings)) {
|
||||
if (details.client) {
|
||||
clientSettings[setting] = details.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
const contents = `var CLIENT_SITE_SETTINGS_WITH_DEFAULTS = ${JSON.stringify(
|
||||
clientSettings
|
||||
)}`;
|
||||
|
||||
fs.writeFileSync(`${this.outputPath}/` + "settings_out.js", contents);
|
||||
built = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function siteSettingsPlugin(...params) {
|
||||
return new SiteSettingsPlugin(...params);
|
||||
};
|
||||
|
||||
module.exports.parsePluginClientSettings = function (
|
||||
discourseRoot,
|
||||
vendorJs,
|
||||
app
|
||||
) {
|
||||
let settings = [discourseRoot + "/config"];
|
||||
|
||||
if (shouldLoadPluginTestJs()) {
|
||||
const pluginInfos = app.project
|
||||
.findAddonByName("discourse-plugins")
|
||||
.pluginInfos();
|
||||
pluginInfos.forEach(({ hasConfig, configDirectory }) => {
|
||||
if (hasConfig) {
|
||||
settings = settings.concat(glob.sync(configDirectory));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const loadedSettings = new SiteSettingsPlugin(settings, "site_settings.yml");
|
||||
|
||||
return concat(mergeTrees([loadedSettings]), {
|
||||
inputFiles: [],
|
||||
headerFiles: [],
|
||||
footerFiles: [],
|
||||
outputFile: `assets/test-site-settings.js`,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.SiteSettingsPlugin = SiteSettingsPlugin;
|
||||
@ -16,8 +16,8 @@
|
||||
"test": "ember test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/standalone": "^7.20.0",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/standalone": "^7.20.4",
|
||||
"@discourse/itsatrap": "^2.0.10",
|
||||
"@discourse/backburner.js": "^2.7.1-0",
|
||||
"@ember/jquery": "^2.0.0",
|
||||
@ -66,7 +66,7 @@
|
||||
"ember-on-resize-modifier": "^1.1.0",
|
||||
"ember-qunit": "^5.1.5",
|
||||
"ember-rfc176-data": "^0.3.17",
|
||||
"ember-source": "~3.28.8",
|
||||
"ember-source": "~3.28.10",
|
||||
"ember-test-selectors": "^6.0.0",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-plugin-qunit": "^6.2.0",
|
||||
@ -77,14 +77,14 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"message-bus-client": "^4.2.0",
|
||||
"messageformat": "0.1.5",
|
||||
"node-fetch": "^2.6.6",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pretender": "^3.4.7",
|
||||
"pretty-text": "^1.0.0",
|
||||
"qunit": "^2.19.3",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"sass": "^1.55.0",
|
||||
"sass": "^1.56.0",
|
||||
"select-kit": "^1.0.0",
|
||||
"sinon": "^14.0.1",
|
||||
"sinon": "^14.0.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"webpack": "^5.74.0",
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Admin - Permalinks", function (needs) {
|
||||
const startingData = [
|
||||
{
|
||||
id: 38,
|
||||
url: "c/feature/announcements",
|
||||
topic_id: null,
|
||||
topic_title: null,
|
||||
topic_url: null,
|
||||
post_id: null,
|
||||
post_url: null,
|
||||
post_number: null,
|
||||
post_topic_title: null,
|
||||
category_id: 67,
|
||||
category_name: "announcements",
|
||||
category_url: "/c/announcements/67",
|
||||
external_url: null,
|
||||
tag_id: null,
|
||||
tag_name: null,
|
||||
tag_url: null,
|
||||
},
|
||||
];
|
||||
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/admin/permalinks.json", (response) => {
|
||||
const result =
|
||||
response.queryParams.filter !== "feature" ? [] : startingData;
|
||||
return helper.response(200, result);
|
||||
});
|
||||
});
|
||||
|
||||
test("search permalinks with result", async function (assert) {
|
||||
await visit("/admin/customize/permalinks");
|
||||
await fillIn(".permalink-search input", "feature");
|
||||
assert.ok(
|
||||
exists(".permalink-results span[title='c/feature/announcements']"),
|
||||
"permalink is found after search"
|
||||
);
|
||||
});
|
||||
|
||||
test("search permalinks without results", async function (assert) {
|
||||
await visit("/admin/customize/permalinks");
|
||||
await fillIn(".permalink-search input", "garboogle");
|
||||
|
||||
assert.ok(
|
||||
exists(".permalink-results__no-result"),
|
||||
"no results message shown"
|
||||
);
|
||||
|
||||
assert.ok(exists(".permalink-search"), "search input still visible");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Admin - Plugins", function (needs) {
|
||||
needs.user();
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/admin/plugins", () =>
|
||||
helper.response({
|
||||
plugins: [
|
||||
{
|
||||
id: "some-test-plugin",
|
||||
name: "some-test-plugin",
|
||||
about: "Plugin description",
|
||||
version: "0.1",
|
||||
url: "https://example.com",
|
||||
admin_route: {
|
||||
location: "testlocation",
|
||||
label: "test.plugin.label",
|
||||
full_location: "adminPlugins.testlocation",
|
||||
},
|
||||
enabled: true,
|
||||
enabled_setting: "testplugin_enabled",
|
||||
has_settings: true,
|
||||
is_official: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("shows plugin list", async function (assert) {
|
||||
await visit("/admin/plugins");
|
||||
const table = query("table.admin-plugins");
|
||||
assert.strictEqual(
|
||||
table.querySelector("tr .plugin-name .name").innerText,
|
||||
"some-test-plugin",
|
||||
"displays the plugin in the table"
|
||||
);
|
||||
|
||||
assert.true(
|
||||
exists(".admin-plugins .admin-detail .alert-error"),
|
||||
"displays an error for unknown routes"
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -75,11 +75,21 @@ acceptance("Admin - Watched Words", function (needs) {
|
||||
|
||||
test("add case-sensitive words", async function (assert) {
|
||||
await visit("/admin/customize/watched_words/action/block");
|
||||
|
||||
const submitButton = query(".watched-word-form button");
|
||||
assert.strictEqual(
|
||||
submitButton.disabled,
|
||||
true,
|
||||
"Add button is disabled by default"
|
||||
);
|
||||
await click(".show-words-checkbox");
|
||||
await fillIn(".watched-word-form input", "Discourse");
|
||||
await click(".case-sensitivity-checkbox");
|
||||
await click(".watched-word-form button");
|
||||
assert.strictEqual(
|
||||
submitButton.disabled,
|
||||
false,
|
||||
"Add button should no longer be disabled after input is filled"
|
||||
);
|
||||
await click(submitButton);
|
||||
|
||||
assert
|
||||
.dom(".watched-words-list .watched-word")
|
||||
@ -87,7 +97,7 @@ acceptance("Admin - Watched Words", function (needs) {
|
||||
|
||||
await fillIn(".watched-word-form input", "discourse");
|
||||
await click(".case-sensitivity-checkbox");
|
||||
await click(".watched-word-form button");
|
||||
await click(submitButton);
|
||||
|
||||
assert
|
||||
.dom(".watched-words-list .watched-word")
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import { click, currentURL, settled, visit } from "@ember/test-helpers";
|
||||
import { set } from "@ember/object";
|
||||
|
||||
acceptance("Bootstrap Mode Notice", function (needs) {
|
||||
needs.user();
|
||||
needs.user({ admin: true });
|
||||
needs.site({ wizard_required: true });
|
||||
needs.settings({
|
||||
bootstrap_mode_enabled: true,
|
||||
@ -36,8 +35,8 @@ acceptance("Bootstrap Mode Notice", function (needs) {
|
||||
"it transitions to the wizard page"
|
||||
);
|
||||
|
||||
this.siteSettings.bootstrap_mode_enabled = false;
|
||||
await visit("/");
|
||||
set(this.siteSettings, "bootstrap_mode_enabled", false);
|
||||
await settled();
|
||||
assert.ok(
|
||||
!exists(".bootstrap-mode-notice"),
|
||||
|
||||
@ -24,7 +24,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
});
|
||||
needs.settings({
|
||||
prioritize_username_in_ux: true,
|
||||
display_name_on_post: false,
|
||||
display_name_on_posts: false,
|
||||
enable_whispers: true,
|
||||
});
|
||||
needs.site({ can_tag_topics: true });
|
||||
@ -489,7 +489,7 @@ acceptance("Prioritize Username", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
prioritize_username_in_ux: true,
|
||||
display_name_on_post: false,
|
||||
display_name_on_posts: false,
|
||||
});
|
||||
|
||||
test("Reply to post use username", async function (assert) {
|
||||
@ -517,7 +517,7 @@ acceptance("Prioritize Full Name", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
prioritize_username_in_ux: false,
|
||||
display_name_on_post: true,
|
||||
display_name_on_posts: true,
|
||||
});
|
||||
|
||||
test("Reply to post use full name", async function (assert) {
|
||||
@ -555,7 +555,7 @@ acceptance("Prioritizing Name fall back", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
prioritize_username_in_ux: false,
|
||||
display_name_on_post: true,
|
||||
display_name_on_posts: true,
|
||||
});
|
||||
|
||||
test("Quotes fall back to username if name is not present", async function (assert) {
|
||||
|
||||
@ -18,7 +18,7 @@ acceptance("Composer - editor mentions", function (needs) {
|
||||
};
|
||||
|
||||
needs.user();
|
||||
needs.settings({ enable_mentions: true });
|
||||
needs.settings({ enable_mentions: true, allow_uncategorized_topics: true });
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
if (clock) {
|
||||
|
||||
@ -11,7 +11,7 @@ import { test } from "qunit";
|
||||
|
||||
acceptance("Composer - Image Preview", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({ enable_whispers: true });
|
||||
needs.settings({ enable_whispers: true, allow_uncategorized_topics: true });
|
||||
needs.site({ can_tag_topics: true });
|
||||
needs.pretender((server, helper) => {
|
||||
server.post("/uploads/lookup-urls", () => {
|
||||
|
||||
@ -18,6 +18,7 @@ acceptance("Composer - Tags", function (needs) {
|
||||
});
|
||||
});
|
||||
needs.site({ can_tag_topics: true });
|
||||
needs.settings({ allow_uncategorized_topics: true });
|
||||
|
||||
test("staff bypass tag validation rule", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
@ -302,7 +302,7 @@ acceptance("Composer", function (needs) {
|
||||
await visit("/");
|
||||
await click("#create-topic");
|
||||
await fillIn("#reply-title", "This title doesn't matter");
|
||||
await fillIn(".d-editor-input", "custom message");
|
||||
await fillIn(".d-editor-input", "custom message that is a good length");
|
||||
await click("#reply-control button.create");
|
||||
|
||||
assert.strictEqual(
|
||||
@ -1107,6 +1107,7 @@ acceptance("Composer - Customizations", function (needs) {
|
||||
|
||||
acceptance("Composer - Focus Open and Closed", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({ allow_uncategorized_topics: true });
|
||||
|
||||
test("Focusing a composer which is not open with create topic", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
@ -81,6 +81,7 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) {
|
||||
needs.settings({
|
||||
simultaneous_uploads: 2,
|
||||
enable_rich_text_paste: true,
|
||||
allow_uncategorized_topics: true,
|
||||
});
|
||||
needs.hooks.afterEach(() => {
|
||||
uploadNumber = 1;
|
||||
@ -487,6 +488,7 @@ acceptance("Uppy Composer Attachment - Upload Error", function (needs) {
|
||||
});
|
||||
needs.settings({
|
||||
simultaneous_uploads: 2,
|
||||
allow_uncategorized_topics: true,
|
||||
});
|
||||
|
||||
test("should show an error message for the failed upload", async function (assert) {
|
||||
@ -527,6 +529,7 @@ acceptance("Uppy Composer Attachment - Upload Handler", function (needs) {
|
||||
needs.pretender(pretender);
|
||||
needs.settings({
|
||||
simultaneous_uploads: 2,
|
||||
allow_uncategorized_topics: true,
|
||||
});
|
||||
needs.hooks.beforeEach(() => {
|
||||
withPluginApi("0.8.14", (api) => {
|
||||
|
||||
@ -21,7 +21,7 @@ acceptance("Emoji", function (needs) {
|
||||
assert.strictEqual(
|
||||
normalizeHtml(query(".d-editor-preview").innerHTML.trim()),
|
||||
normalizeHtml(
|
||||
`<p>this is an emoji <img src="/images/emoji/google_classic/blonde_woman.png?v=${v}" title=":blonde_woman:" class="emoji" alt=":blonde_woman:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;"></p>`
|
||||
`<p>this is an emoji <img src="/images/emoji/twitter/blonde_woman.png?v=${v}" title=":blonde_woman:" class="emoji" alt=":blonde_woman:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;"></p>`
|
||||
)
|
||||
);
|
||||
});
|
||||
@ -36,7 +36,7 @@ acceptance("Emoji", function (needs) {
|
||||
assert.strictEqual(
|
||||
normalizeHtml(query(".d-editor-preview").innerHTML.trim()),
|
||||
normalizeHtml(
|
||||
`<p>this is an emoji <img src="/images/emoji/google_classic/blonde_woman/5.png?v=${v}" title=":blonde_woman:t5:" class="emoji" alt=":blonde_woman:t5:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;"></p>`
|
||||
`<p>this is an emoji <img src="/images/emoji/twitter/blonde_woman/5.png?v=${v}" title=":blonde_woman:t5:" class="emoji" alt=":blonde_woman:t5:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;"></p>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@ -94,7 +94,7 @@ acceptance("Group Requests", function (needs) {
|
||||
query(".group-members tr:first-child td:nth-child(1)")
|
||||
.innerText.trim()
|
||||
.replace(/\s+/g, " "),
|
||||
"Robin Ward eviltrout"
|
||||
"eviltrout Robin Ward"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query(".group-members tr:first-child td:nth-child(3)").innerText.trim(),
|
||||
|
||||
@ -123,7 +123,7 @@ acceptance("Invite accept", function (needs) {
|
||||
"submit is disabled because password is not filled"
|
||||
);
|
||||
|
||||
await fillIn("#new-account-password", "top$ecret");
|
||||
await fillIn("#new-account-password", "top$ecretzz");
|
||||
assert.notOk(
|
||||
exists(".invites-show .btn-primary:disabled"),
|
||||
"submit is enabled"
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "I18n";
|
||||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
@ -10,15 +6,6 @@ import { visit } from "@ember/test-helpers";
|
||||
acceptance("Personal Message", function (needs) {
|
||||
needs.user();
|
||||
|
||||
test("footer edit button", async function (assert) {
|
||||
await visit("/t/pm-for-testing/12");
|
||||
|
||||
assert.ok(
|
||||
!exists(".edit-message"),
|
||||
"does not show edit first post button on footer by default"
|
||||
);
|
||||
});
|
||||
|
||||
test("suggested messages", async function (assert) {
|
||||
await visit("/t/pm-for-testing/12");
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
|
||||
extraConnectorClass("user-profile-primary/hello", {
|
||||
actions: {
|
||||
sayHello() {
|
||||
this.set("hello", "hello!");
|
||||
this.set("hello", `${this.hello || ""}hello!`);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -53,6 +53,7 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
|
||||
`${PREFIX}/user-profile-primary/hello`
|
||||
] = hbs`<span class='hello-username'>{{model.username}}</span>
|
||||
<button class='say-hello' {{on "click" (action "sayHello")}}></button>
|
||||
<button class='say-hello-using-this' {{on "click" this.sayHello}}></button>
|
||||
<span class='hello-result'>{{hello}}</span>`;
|
||||
Ember.TEMPLATES[
|
||||
`${PREFIX}/user-profile-primary/hi`
|
||||
@ -87,6 +88,12 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
|
||||
"hello!",
|
||||
"actions delegate properly"
|
||||
);
|
||||
await click(".say-hello-using-this");
|
||||
assert.strictEqual(
|
||||
query(".hello-result").innerText,
|
||||
"hello!hello!",
|
||||
"actions are made available on `this` and are bound correctly"
|
||||
);
|
||||
|
||||
await click(".say-hi");
|
||||
assert.strictEqual(
|
||||
|
||||
@ -128,6 +128,9 @@ acceptance("User Preferences", function (needs) {
|
||||
await categorySelector.fillInFilter("faq");
|
||||
await savePreferences();
|
||||
|
||||
this.siteSettings.tagging_enabled = false;
|
||||
await visit("/");
|
||||
await visit("/u/eviltrout/preferences");
|
||||
assert.ok(
|
||||
!exists(".preferences-nav .nav-tags a"),
|
||||
"tags tab isn't there when tags are disabled"
|
||||
|
||||
@ -4,7 +4,13 @@ import {
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import {
|
||||
click,
|
||||
fillIn,
|
||||
settled,
|
||||
triggerKeyEvent,
|
||||
visit,
|
||||
} from "@ember/test-helpers";
|
||||
import I18n from "I18n";
|
||||
import searchFixtures from "discourse/tests/fixtures/search-fixtures";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
@ -333,7 +339,10 @@ acceptance("Search - Anonymous", function (needs) {
|
||||
|
||||
acceptance("Search - Authenticated", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({ log_search_queries: true });
|
||||
needs.settings({
|
||||
log_search_queries: true,
|
||||
allow_uncategorized_topics: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/search/query", (request) => {
|
||||
@ -476,6 +485,7 @@ acceptance("Search - Authenticated", function (needs) {
|
||||
"href"
|
||||
);
|
||||
await triggerKeyEvent(".search-menu", "keydown", "A");
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(
|
||||
query("#reply-control textarea").value,
|
||||
|
||||
@ -44,6 +44,12 @@ const RESPONSES = {
|
||||
security_keys_enabled: true,
|
||||
allowed_methods: [BACKUP_CODE],
|
||||
},
|
||||
ok010010: {
|
||||
totp_enabled: false,
|
||||
backup_enabled: true,
|
||||
security_keys_enabled: false,
|
||||
allowed_methods: [BACKUP_CODE],
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(RESPONSES).forEach((k) => {
|
||||
@ -178,6 +184,14 @@ acceptance("Second Factor Auth Page", function (needs) {
|
||||
!exists(".toggle-second-factor-method"),
|
||||
"no alternative methods are shown if only 1 method is allowed"
|
||||
);
|
||||
|
||||
// only backup codes
|
||||
await visit("/session/2fa?nonce=ok010010");
|
||||
assert.ok(exists("form.backup-code-token"), "backup code form is shown");
|
||||
assert.ok(
|
||||
!exists(".toggle-second-factor-method"),
|
||||
"no alternative methods are shown if only 1 method is allowed"
|
||||
);
|
||||
});
|
||||
|
||||
test("switching 2FA methods", async function (assert) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user