Version bump

This commit is contained in:
Martin Brennan 2022-11-14 13:09:57 +10:00
commit 4dc89cb0cc
No known key found for this signature in database
GPG Key ID: A08063EEF3EA26A4
1123 changed files with 84593 additions and 8197 deletions

View File

@ -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
View File

@ -0,0 +1,2 @@
chat:
- plugins/chat/**/*

14
.github/workflows/labeler.yml vendored Normal file
View 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 }}"

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -39,6 +39,7 @@
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/lazy-yt/
!/plugins/chat/
!/plugins/poll/
!/plugins/styleguide
/plugins/*/auto_generated/

2
.streerc Normal file
View File

@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma

View File

@ -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

View File

@ -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": [

View File

@ -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"),

View File

@ -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) => {

View File

@ -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;
}
},
});

View File

@ -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");

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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"
},

View File

@ -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);

View File

@ -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;

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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,
};
});
},

View File

@ -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"
},

View File

@ -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);

View File

@ -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
);
},

View File

@ -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;
}
}

View File

@ -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,
});
});
}
}

View File

@ -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}} />

View File

@ -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}}

View File

@ -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);
}
/**

View File

@ -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}}

View File

@ -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}}

View File

@ -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")

View File

@ -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"),

View File

@ -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;

View File

@ -19,7 +19,7 @@
</LinkTo>
</li>
{{#if this.siteSettings.enable_mentions}}
{{#if @siteSettings.enable_mentions}}
<li>
<LinkTo @route="userNotifications.mentions">
{{d-icon "at"}}

View File

@ -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);

View File

@ -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,

View File

@ -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

View File

@ -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);

View File

@ -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();
}
});
}
}

View File

@ -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
) {

View File

@ -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);
},
};

View File

@ -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,
};
}

View File

@ -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,
});
}
}
}

View File

@ -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;

View File

@ -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.
*/

View File

@ -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";
}
}
}

View File

@ -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);
}

View File

@ -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";
}
}
}

View File

@ -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";
}
}
}

View File

@ -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";
}
}
}

View File

@ -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";
}
}
}

View File

@ -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,
});
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}
});
},

View File

@ -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";

View File

@ -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] =

View File

@ -207,7 +207,7 @@ const TopicTrackingState = EmberObject.extend({
}
}
if (filterTag && !data.payload.tags.includes(filterTag)) {
if (filterTag && !data.payload.tags?.includes(filterTag)) {
return;
}

View File

@ -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")

View File

@ -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);

View File

@ -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) {

View File

@ -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}} />

View File

@ -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}}

View File

@ -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">

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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");
},
});

View File

@ -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();

View File

@ -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");
},
});

View File

@ -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`, {

View File

@ -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"

View 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;

View File

@ -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",

View File

@ -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");
});
});

View File

@ -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"
);
});
});

View File

@ -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")

View File

@ -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"),

View File

@ -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) {

View File

@ -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) {

View File

@ -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", () => {

View File

@ -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("/");

View File

@ -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");

View File

@ -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) => {

View File

@ -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>`
)
);
});

View File

@ -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(),

View File

@ -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"

View File

@ -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");

View File

@ -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(

View File

@ -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"

View File

@ -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,

View File

@ -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