Merge branch 'main' into feature/wizard-look-and-feel-improvements
This commit is contained in:
commit
8533290a9c
60
Gemfile.lock
60
Gemfile.lock
|
@ -119,7 +119,7 @@ GEM
|
|||
css_parser (1.19.1)
|
||||
addressable
|
||||
csv (3.3.0)
|
||||
date (3.4.0)
|
||||
date (3.4.1)
|
||||
debug_inspector (1.2.0)
|
||||
diff-lcs (1.5.1)
|
||||
diffy (3.4.3)
|
||||
|
@ -161,16 +161,16 @@ GEM
|
|||
fspath (3.1.2)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (4.29.0-aarch64-linux)
|
||||
google-protobuf (4.29.1-aarch64-linux)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.29.0-arm64-darwin)
|
||||
google-protobuf (4.29.1-arm64-darwin)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.29.0-x86_64-darwin)
|
||||
google-protobuf (4.29.1-x86_64-darwin)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.29.0-x86_64-linux)
|
||||
google-protobuf (4.29.1-x86_64-linux)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
guess_html_encoding (0.0.11)
|
||||
|
@ -197,9 +197,10 @@ GEM
|
|||
reline (>= 0.4.2)
|
||||
iso8601 (0.13.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.8.2)
|
||||
json-schema (5.1.0)
|
||||
json (2.9.0)
|
||||
json-schema (5.1.1)
|
||||
addressable (~> 2.8)
|
||||
bigdecimal (~> 3.1)
|
||||
json_schemer (2.3.0)
|
||||
bigdecimal
|
||||
hana (~> 1.3)
|
||||
|
@ -217,7 +218,7 @@ GEM
|
|||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
literate_randomizer (0.4.0)
|
||||
logger (1.6.1)
|
||||
logger (1.6.2)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
|
@ -252,7 +253,7 @@ GEM
|
|||
mini_suffix (0.3.3)
|
||||
ffi (~> 1.9)
|
||||
minio_runner (0.1.2)
|
||||
minitest (5.25.2)
|
||||
minitest (5.25.4)
|
||||
mocha (2.6.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.7.5)
|
||||
|
@ -260,7 +261,7 @@ GEM
|
|||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
mustache (1.1.1)
|
||||
net-http (0.5.0)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.1)
|
||||
date
|
||||
|
@ -272,13 +273,13 @@ GEM
|
|||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.16.7-aarch64-linux)
|
||||
nokogiri (1.16.8-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
nokogiri (1.16.8-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
nokogiri (1.16.8-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
nokogiri (1.16.8-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
|
@ -343,7 +344,8 @@ GEM
|
|||
pry-stack_explorer (0.6.1)
|
||||
binding_of_caller (~> 1.0)
|
||||
pry (~> 0.13)
|
||||
psych (5.2.0)
|
||||
psych (5.2.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
|
@ -366,9 +368,9 @@ GEM
|
|||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails_failover (2.1.1)
|
||||
activerecord (>= 6.1, < 8.0)
|
||||
concurrent-ruby
|
||||
|
@ -441,7 +443,7 @@ GEM
|
|||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.1)
|
||||
rspec-support (3.13.2)
|
||||
rss (0.3.1)
|
||||
rexml
|
||||
rswag-specs (2.16.0)
|
||||
|
@ -451,21 +453,21 @@ GEM
|
|||
rspec-core (>= 2.14)
|
||||
rtlcss (0.2.1)
|
||||
mini_racer (>= 0.6.3)
|
||||
rubocop (1.69.0)
|
||||
rubocop (1.69.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rubocop-ast (>= 1.36.1, < 2.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.36.2)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.21.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-discourse (3.8.6)
|
||||
rubocop-discourse (3.9.0)
|
||||
activesupport (>= 6.1)
|
||||
rubocop (>= 1.59.0)
|
||||
rubocop-capybara (>= 2.0.0)
|
||||
|
@ -505,7 +507,7 @@ GEM
|
|||
google-protobuf (>= 3.25, < 5.0)
|
||||
sassc-embedded (1.77.7)
|
||||
sass-embedded (~> 1.77)
|
||||
securerandom (0.3.2)
|
||||
securerandom (0.4.0)
|
||||
selenium-devtools (0.131.0)
|
||||
selenium-webdriver (~> 4.2)
|
||||
selenium-webdriver (4.27.0)
|
||||
|
@ -538,10 +540,10 @@ GEM
|
|||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (2.3.1-aarch64-linux-gnu)
|
||||
sqlite3 (2.3.1-arm64-darwin)
|
||||
sqlite3 (2.3.1-x86_64-darwin)
|
||||
sqlite3 (2.3.1-x86_64-linux-gnu)
|
||||
sqlite3 (2.4.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.4.0-arm64-darwin)
|
||||
sqlite3 (2.4.0-x86_64-darwin)
|
||||
sqlite3 (2.4.0-x86_64-linux-gnu)
|
||||
sshkey (3.0.0)
|
||||
stackprof (0.2.26)
|
||||
stringio (3.1.2)
|
||||
|
@ -567,7 +569,7 @@ GEM
|
|||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.10)
|
||||
useragent (0.16.11)
|
||||
version_gem (1.1.4)
|
||||
web-push (3.0.1)
|
||||
jwt (~> 2.0)
|
||||
|
@ -576,7 +578,7 @@ GEM
|
|||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.0)
|
||||
webrick (1.9.1)
|
||||
websocket (1.2.11)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
|
|
|
@ -27,6 +27,7 @@ const FORM_FIELDS = [
|
|||
"badge_grouping_id",
|
||||
"trigger",
|
||||
"badge_type_id",
|
||||
"show_in_post_header",
|
||||
];
|
||||
|
||||
export default class AdminBadgesShowController extends Controller {
|
||||
|
@ -40,8 +41,6 @@ export default class AdminBadgesShowController extends Controller {
|
|||
@tracked model;
|
||||
@tracked previewLoading = false;
|
||||
@tracked selectedGraphicType = null;
|
||||
@tracked userBadges;
|
||||
@tracked userBadgesAll;
|
||||
|
||||
@cached
|
||||
get formData() {
|
||||
|
@ -80,6 +79,17 @@ export default class AdminBadgesShowController extends Controller {
|
|||
return this.model.system;
|
||||
}
|
||||
|
||||
@action
|
||||
postHeaderDescription(data) {
|
||||
return this.disableBadgeOnPosts(data) && !data.system;
|
||||
}
|
||||
|
||||
@action
|
||||
disableBadgeOnPosts(data) {
|
||||
const { listable, show_posts } = data;
|
||||
return !listable || !show_posts;
|
||||
}
|
||||
|
||||
setup() {
|
||||
// this is needed because the model doesnt have default values
|
||||
// Using `set` here isn't ideal, but we don't know that tracking is set up on the model yet.
|
||||
|
|
|
@ -245,7 +245,7 @@
|
|||
</field.Menu>
|
||||
</form.Field>
|
||||
|
||||
<form.CheckboxGroup as |group|>
|
||||
<form.CheckboxGroup @title={{i18n "admin.badges.usage_heading"}} as |group|>
|
||||
<group.Field
|
||||
@title={{i18n "admin.badges.allow_title"}}
|
||||
@showTitle={{false}}
|
||||
|
@ -264,7 +264,12 @@
|
|||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
</form.CheckboxGroup>
|
||||
|
||||
<form.CheckboxGroup
|
||||
@title={{i18n "admin.badges.visibility_heading"}}
|
||||
as |group|
|
||||
>
|
||||
<group.Field
|
||||
@title={{i18n "admin.badges.listable"}}
|
||||
@showTitle={{false}}
|
||||
|
@ -284,6 +289,20 @@
|
|||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
|
||||
<group.Field
|
||||
@title={{i18n "admin.badges.show_in_post_header"}}
|
||||
@showTitle={{false}}
|
||||
@name="show_in_post_header"
|
||||
@disabled={{this.disableBadgeOnPosts data}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox>
|
||||
{{#if (this.postHeaderDescription data)}}
|
||||
{{i18n "admin.badges.show_in_post_header_disabled"}}
|
||||
{{/if}}
|
||||
</field.Checkbox>
|
||||
</group.Field>
|
||||
</form.CheckboxGroup>
|
||||
</form.Section>
|
||||
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
|
||||
<PluginOutlet @name="admin-users-list-show-before" />
|
||||
|
||||
<div class="admin-users-list__controls">
|
||||
<div class="admin-users-list__search">
|
||||
<div class="d-admin-filter admin-users-list__controls">
|
||||
<div class="admin-filter__input-container admin-users-list__search">
|
||||
<input
|
||||
class="admin-filter__input"
|
||||
type="text"
|
||||
dir="auto"
|
||||
placeholder={{this.searchHint}}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"@types/jquery": "^3.5.32",
|
||||
"@types/qunit": "^2.19.12",
|
||||
"@types/rsvp": "^4.0.9",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -4,10 +4,6 @@ const DEPRECATION_WORKFLOW = [
|
|||
handler: "silence",
|
||||
matchId: "discourse.decorate-widget.hamburger-widget-links",
|
||||
},
|
||||
{
|
||||
handler: "silence",
|
||||
matchId: "discourse.fontawesome-6-upgrade",
|
||||
},
|
||||
{
|
||||
handler: "silence",
|
||||
matchId: "discourse.post-menu-widget-overrides",
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -187,7 +187,7 @@ function renderImageOrPlayableMedia(tokens, idx, options, env, slf) {
|
|||
options.discourse.previewing &&
|
||||
!options.discourse.limitedSiteSettings.enableDiffhtmlPreview
|
||||
) {
|
||||
const origSrc = token.attrGet("data-orig-src");
|
||||
const origSrc = token.attrGet("data-orig-src") || token.attrGet("src");
|
||||
const origSrcId = origSrc
|
||||
.substring(origSrc.lastIndexOf("/") + 1)
|
||||
.split(".")[0];
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"ember-cli": "~6.0.1",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"@ember/optional-features": "^2.2.0",
|
||||
"@embroider/test-setup": "^4.0.0",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/syntax": "^0.92.3",
|
||||
"@glimmer/syntax": "^0.93.1",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"ember-cli": "~6.0.1",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -215,7 +215,7 @@ export default class AboutPage extends Component {
|
|||
{{#if this.currentUser.admin}}
|
||||
<p>
|
||||
<LinkTo class="edit-about-page" @route="adminConfig.about">
|
||||
{{dIcon "pencil-alt"}}
|
||||
{{dIcon "pencil"}}
|
||||
<span>{{i18n "about.edit"}}</span>
|
||||
</LinkTo>
|
||||
</p>
|
||||
|
|
|
@ -10,6 +10,10 @@ export default class BadgeButton extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
get showName() {
|
||||
return this.args.showName ?? true;
|
||||
}
|
||||
|
||||
<template>
|
||||
<span
|
||||
title={{this.title}}
|
||||
|
@ -20,7 +24,9 @@ export default class BadgeButton extends Component {
|
|||
...attributes
|
||||
>
|
||||
{{iconOrImage @badge}}
|
||||
<span class="badge-display-name">{{@badge.name}}</span>
|
||||
{{#if this.showName}}
|
||||
<span class="badge-display-name">{{@badge.name}}</span>
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
const LIST_TYPE = {
|
||||
|
@ -40,4 +41,8 @@ export default class CategoryListItem extends Component {
|
|||
slugPath(categoryPath) {
|
||||
return categoryPath.substring("/c/".length);
|
||||
}
|
||||
|
||||
applyValueTransformer(name, value, context) {
|
||||
return applyValueTransformer(name, value, context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
@outletArgs={{hash composer=this.composer.model editorType="composer"}}
|
||||
@topicId={{this.composer.model.topic.id}}
|
||||
@categoryId={{this.composer.model.category.id}}
|
||||
@onSetup={{this.setupEditor}}
|
||||
>
|
||||
{{yield}}
|
||||
</DEditor>
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
linkSeenMentions,
|
||||
} from "discourse/lib/link-mentions";
|
||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import {
|
||||
authorizesOneOrMoreImageExtensions,
|
||||
IMAGE_MARKDOWN_REGEX,
|
||||
|
@ -139,7 +138,7 @@ export default class ComposerEditor extends Component {
|
|||
@observes("composer.focusTarget")
|
||||
setFocus() {
|
||||
if (this.composer.focusTarget === "editor") {
|
||||
putCursorAtEnd(this.element.querySelector("textarea"));
|
||||
this.textManipulation.putCursorAtEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,21 +187,9 @@ export default class ComposerEditor extends Component {
|
|||
|
||||
@on("didInsertElement")
|
||||
_composerEditorInit() {
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||
|
||||
input?.addEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
this._registerImageAltTextButtonClick(preview);
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get("composer.model.canEditTitle")) {
|
||||
putCursorAtEnd(input);
|
||||
}
|
||||
|
||||
if (this.composer.allowUpload) {
|
||||
this.uppyComposerUpload.setup(this.element);
|
||||
}
|
||||
|
@ -210,6 +197,30 @@ export default class ComposerEditor extends Component {
|
|||
this.appEvents.trigger(`${this.composerEventPrefix}:will-open`);
|
||||
}
|
||||
|
||||
@bind
|
||||
setupEditor(textManipulation) {
|
||||
this.textManipulation = textManipulation;
|
||||
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
|
||||
input?.addEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get("composer.model.canEditTitle")) {
|
||||
this.textManipulation.putCursorAtEnd();
|
||||
}
|
||||
|
||||
return () => {
|
||||
input?.removeEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"composer.model.reply",
|
||||
"composer.model.replyLength",
|
||||
|
@ -785,7 +796,6 @@ export default class ComposerEditor extends Component {
|
|||
|
||||
@on("willDestroyElement")
|
||||
_composerClosed() {
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||
|
||||
if (this.composer.allowUpload) {
|
||||
|
@ -802,11 +812,6 @@ export default class ComposerEditor extends Component {
|
|||
);
|
||||
});
|
||||
|
||||
input?.removeEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
||||
|
|
|
@ -21,7 +21,6 @@ import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
|
|||
import { linkSeenMentions } from "discourse/lib/link-mentions";
|
||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
|
||||
import { getHead } from "discourse/lib/textarea-text-manipulation";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
import {
|
||||
destroyUserStatuses,
|
||||
|
@ -36,8 +35,6 @@ import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
|||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
||||
|
||||
let _createCallbacks = [];
|
||||
|
||||
export function addToolbarCallback(func) {
|
||||
|
@ -145,8 +142,6 @@ export default class DEditor extends Component {
|
|||
|
||||
keymap["tab"] = () => this.textManipulation.indentSelection("right");
|
||||
keymap["shift+tab"] = () => this.textManipulation.indentSelection("left");
|
||||
keymap[`${PLATFORM_KEY_MODIFIER}+shift+.`] = () =>
|
||||
this.send("insertCurrentTime");
|
||||
|
||||
return keymap;
|
||||
}
|
||||
|
@ -485,34 +480,6 @@ export default class DEditor extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.includes("\n")) {
|
||||
this.textManipulation.applySurround(sel, head, "", exampleKey, opts);
|
||||
} else {
|
||||
const [hval, hlen] = getHead(head);
|
||||
if (sel.start === sel.end) {
|
||||
sel.value = i18n(`composer.${exampleKey}`);
|
||||
}
|
||||
|
||||
const trimmedPre = sel.pre.trim();
|
||||
const number = sel.value.startsWith(hval)
|
||||
? sel.value.slice(hlen)
|
||||
: `${hval}${sel.value}`;
|
||||
const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : "";
|
||||
|
||||
const trimmedPost = sel.post.trim();
|
||||
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
||||
|
||||
this.set("value", `${preLines}${number}${post}`);
|
||||
this.textManipulation.selectText(preLines.length, number.length);
|
||||
}
|
||||
}
|
||||
|
||||
_applySurround(head, tail, exampleKey, opts) {
|
||||
const selected = this.textManipulation.getSelected();
|
||||
this.textManipulation.applySurround(selected, head, tail, exampleKey, opts);
|
||||
}
|
||||
|
||||
@action
|
||||
rovingButtonBar(event) {
|
||||
let target = event.target;
|
||||
|
@ -559,6 +526,27 @@ export default class DEditor extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a toolbar event object passed to toolbar buttons.
|
||||
*
|
||||
* @typedef {Object} ToolbarEvent
|
||||
* @property {function} applySurround - Applies surrounding text
|
||||
* @property {function} formatCode - Formats as code
|
||||
* @property {function} replaceText - Replaces text
|
||||
* @property {function} selectText - Selects a range of text
|
||||
* @property {function} toggleDirection - Toggles text direction
|
||||
* @property {function} getText - Gets the text
|
||||
* @property {function} addText - Adds text
|
||||
* @property {function} applyList - Applies a list format
|
||||
* @property {*} selected - The current selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new toolbar event object
|
||||
*
|
||||
* @param {boolean} trimLeading - Whether to trim leading whitespace
|
||||
* @returns {ToolbarEvent} An object with toolbar event actions
|
||||
*/
|
||||
newToolbarEvent(trimLeading) {
|
||||
const selected = this.textManipulation.getSelected(trimLeading);
|
||||
return {
|
||||
|
@ -574,8 +562,8 @@ export default class DEditor extends Component {
|
|||
opts
|
||||
),
|
||||
applyList: (head, exampleKey, opts) =>
|
||||
this._applyList(selected, head, exampleKey, opts),
|
||||
formatCode: (...args) => this.send("formatCode", args),
|
||||
this.textManipulation.applyList(selected, head, exampleKey, opts),
|
||||
formatCode: () => this.textManipulation.formatCode(),
|
||||
addText: (text) => this.textManipulation.addText(selected, text),
|
||||
getText: () => this.value,
|
||||
toggleDirection: () => this.textManipulation.toggleDirection(),
|
||||
|
@ -628,71 +616,6 @@ export default class DEditor extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
formatCode() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.includes("\n");
|
||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||
const isFourSpacesIndent =
|
||||
this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
|
||||
|
||||
if (!hasNewLine) {
|
||||
if (selValue.length === 0 && isBlankLine) {
|
||||
if (isFourSpacesIndent) {
|
||||
const example = i18n(`composer.code_text`);
|
||||
this.set("value", `${sel.pre} ${example}${sel.post}`);
|
||||
return this.textManipulation.selectText(
|
||||
sel.pre.length + 4,
|
||||
example.length
|
||||
);
|
||||
} else {
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
"```\n",
|
||||
"\n```",
|
||||
"paste_code_text"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return this.textManipulation.applySurround(sel, "`", "`", "code_title");
|
||||
}
|
||||
} else {
|
||||
if (isFourSpacesIndent) {
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
" ",
|
||||
"",
|
||||
"code_text"
|
||||
);
|
||||
} else {
|
||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||
return this.textManipulation.addText(
|
||||
sel,
|
||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
insertCurrentTime() {
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const time = moment().format("HH:mm:ss");
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
this.textManipulation.addText(
|
||||
sel,
|
||||
`[date=${date} time=${time} timezone="${timezone}"]`
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
handleFocusIn() {
|
||||
this.set("isEditorFocused", true);
|
||||
|
@ -715,6 +638,8 @@ export default class DEditor extends Component {
|
|||
this._applyHashtagAutocomplete();
|
||||
this._applyMentionAutocomplete();
|
||||
|
||||
const destroyEditor = this.onSetup?.(textManipulation);
|
||||
|
||||
scheduleOnce("afterRender", this, this._readyNow);
|
||||
|
||||
return () => {
|
||||
|
@ -723,6 +648,8 @@ export default class DEditor extends Component {
|
|||
this.element?.removeEventListener("paste", textManipulation.paste);
|
||||
|
||||
textManipulation.autocomplete("destroy");
|
||||
|
||||
destroyEditor?.();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -741,7 +668,11 @@ export default class DEditor extends Component {
|
|||
textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.on("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.on(
|
||||
"composer:apply-surround",
|
||||
textManipulation,
|
||||
"applySurroundSelection"
|
||||
);
|
||||
this.appEvents.on(
|
||||
"composer:indent-selected-text",
|
||||
textManipulation,
|
||||
|
@ -764,7 +695,11 @@ export default class DEditor extends Component {
|
|||
textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.off("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.off(
|
||||
"composer:apply-surround",
|
||||
textManipulation,
|
||||
"applySurroundSelection"
|
||||
);
|
||||
this.appEvents.off(
|
||||
"composer:indent-selected-text",
|
||||
textManipulation,
|
||||
|
|
|
@ -12,6 +12,7 @@ import DButton from "discourse/components/d-button";
|
|||
import FlashMessage from "discourse/components/flash-message";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import element from "discourse/helpers/element";
|
||||
import htmlClass from "discourse/helpers/html-class";
|
||||
import {
|
||||
disableBodyScroll,
|
||||
enableBodyScroll,
|
||||
|
@ -261,6 +262,7 @@ export default class DModal extends Component {
|
|||
@inline={{@inline}}
|
||||
@append={{true}}
|
||||
>
|
||||
{{htmlClass "modal-open"}}
|
||||
<this.dynamicElement
|
||||
class={{concatClass
|
||||
"modal"
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
/>
|
||||
|
||||
{{#if this.site.mobileView}}
|
||||
<PluginOutlet
|
||||
@name="category-list-before-category-mobile"
|
||||
@outletArgs={{hash
|
||||
category=this.category
|
||||
listType=this.listType
|
||||
isMuted=this.isMuted
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
data-category-id={{this.category.id}}
|
||||
data-notification-level={{this.category.notificationLevelString}}
|
||||
|
@ -80,8 +88,19 @@
|
|||
'has-description'
|
||||
'no-description'
|
||||
}}
|
||||
{{this.applyValueTransformer
|
||||
'parent-category-row-class'
|
||||
(array)
|
||||
(hash category=this.category)
|
||||
}}
|
||||
{{if this.category.uploaded_logo.url 'has-logo' 'no-logo'}}"
|
||||
>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-list-before-category-section"
|
||||
@outletArgs={{hash category=this.category listType=this.listType}}
|
||||
/>
|
||||
|
||||
<td
|
||||
class="category {{if this.isMuted 'muted'}}"
|
||||
style={{category-color-variable this.category.color}}
|
||||
|
@ -141,6 +160,11 @@
|
|||
{{/if}}
|
||||
</td>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-list-before-topics-section"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
|
||||
<td class="topics">
|
||||
<div title={{this.category.statTitle}}>{{html-safe
|
||||
this.category.stat
|
||||
|
@ -154,6 +178,11 @@
|
|||
/>
|
||||
</td>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-list-after-topics-section"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
|
||||
{{#unless this.isMuted}}
|
||||
{{#if this.showTopics}}
|
||||
<td class="latest">
|
||||
|
@ -165,6 +194,10 @@
|
|||
{{/if}}
|
||||
{{/each}}
|
||||
</td>
|
||||
<PluginOutlet
|
||||
@name="category-list-after-latest-section"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</tr>
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class PostMenuEditButton extends Component {
|
|||
}}
|
||||
...attributes
|
||||
@action={{@buttonActions.editPost}}
|
||||
@icon={{if @post.wiki "far-edit" "pencil-alt"}}
|
||||
@icon={{if @post.wiki "far-edit" "pencil"}}
|
||||
@label={{if this.showLabel "post.controls.edit_action"}}
|
||||
@title="post.controls.edit"
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
<div class="author">
|
||||
<a href={{this.post.userPath}} data-user-card={{this.post.username}}>
|
||||
{{avatar this.post imageSize="large"}}
|
||||
</a>
|
||||
</div>
|
||||
<PluginOutlet
|
||||
@name="search-results-topic-avatar-wrapper"
|
||||
@outletArgs={{hash post=this.post}}
|
||||
>
|
||||
<div class="author">
|
||||
<a href={{this.post.userPath}} data-user-card={{this.post.username}}>
|
||||
{{avatar this.post imageSize="large"}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</PluginOutlet>
|
||||
|
||||
<div class="fps-topic" data-topic-id={{this.post.topic.id}}>
|
||||
<div class="topic">
|
||||
|
||||
{{#if this.bulkSelectEnabled}}
|
||||
<TrackSelected
|
||||
@selectedList={{this.selected}}
|
||||
|
@ -55,33 +62,43 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blurb container">
|
||||
<span class="date">
|
||||
{{format-date this.post.created_at format="tiny"}}
|
||||
{{#if this.post.blurb}}
|
||||
<span class="separator">-</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if this.post.blurb}}
|
||||
{{#if this.siteSettings.use_pg_headlines_for_excerpt}}
|
||||
{{html-safe this.post.blurb}}
|
||||
{{else}}
|
||||
<HighlightSearch @highlight={{this.highlightQuery}}>
|
||||
{{html-safe this.post.blurb}}
|
||||
</HighlightSearch>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.showLikeCount}}
|
||||
{{#if this.post.like_count}}
|
||||
<span class="like-count">
|
||||
<span class="value">{{this.post.like_count}}</span>
|
||||
{{d-icon "heart"}}
|
||||
<PluginOutlet
|
||||
@name="search-result-entry-blurb-wrapper"
|
||||
@outletArgs={{hash post=this.post logClick=this.logClick}}
|
||||
>
|
||||
<div class="blurb container">
|
||||
<span class="date">
|
||||
{{format-date this.post.created_at format="tiny"}}
|
||||
{{#if this.post.blurb}}
|
||||
<span class="separator">-</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
|
||||
{{#if this.post.blurb}}
|
||||
{{#if this.siteSettings.use_pg_headlines_for_excerpt}}
|
||||
{{html-safe this.post.blurb}}
|
||||
{{else}}
|
||||
<HighlightSearch @highlight={{this.highlightQuery}}>
|
||||
{{html-safe this.post.blurb}}
|
||||
</HighlightSearch>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</PluginOutlet>
|
||||
|
||||
<PluginOutlet
|
||||
@name="search-result-entry-stats-wrapper"
|
||||
@outletArgs={{hash post=this.post}}
|
||||
>
|
||||
{{#if this.showLikeCount}}
|
||||
{{#if this.post.like_count}}
|
||||
<span class="like-count">
|
||||
<span class="value">{{this.post.like_count}}</span>
|
||||
{{d-icon "heart"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="after-search-result-entry" />
|
|
@ -1,5 +1,5 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { concat, hash } from "@ember/helper";
|
||||
import { array, concat, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
|
@ -20,6 +20,7 @@ import discourseTags from "discourse/helpers/discourse-tags";
|
|||
import formatDate from "discourse/helpers/format-date";
|
||||
import topicFeaturedLink from "discourse/helpers/topic-featured-link";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
|
@ -208,6 +209,9 @@ export default class Item extends Component {
|
|||
(if @topic.pinned "pinned")
|
||||
(if @topic.closed "closed")
|
||||
this.tagClassNames
|
||||
(applyValueTransformer
|
||||
"topic-list-item-class" (array) (hash topic=@topic)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<PluginOutlet
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class UserBadge extends Component {
|
|||
|
||||
<template>
|
||||
<a class="user-card-badge-link" href={{this.badgeUrl}}>
|
||||
<BadgeButton @badge={{@badge}}>
|
||||
<BadgeButton @badge={{@badge}} @showName={{@showName}}>
|
||||
{{#if this.showGrantCount}}
|
||||
<span class="count"> (×{{@count}})</span>
|
||||
{{/if}}
|
||||
|
|
|
@ -848,12 +848,7 @@ export default class TopicController extends Controller.extend(
|
|||
return false;
|
||||
}
|
||||
|
||||
const composer = this.composer;
|
||||
let topic = this.model;
|
||||
const composerModel = composer.get("model");
|
||||
let editingFirst =
|
||||
composerModel &&
|
||||
(post.get("firstPost") || composerModel.get("editingFirstPost"));
|
||||
const topic = this.model;
|
||||
|
||||
let editingSharedDraft = false;
|
||||
let draftsCategoryId = this.get("site.shared_drafts_category_id");
|
||||
|
@ -872,22 +867,14 @@ export default class TopicController extends Controller.extend(
|
|||
opts.destinationCategoryId = topic.get("destination_category_id");
|
||||
}
|
||||
|
||||
// Reopen the composer if we're editing the same post
|
||||
const editingExisting =
|
||||
post.id === composerModel?.post?.id &&
|
||||
opts?.action === Composer.EDIT &&
|
||||
composerModel?.draftKey === opts.draftKey;
|
||||
if (editingExisting) {
|
||||
composer.unshrink();
|
||||
return;
|
||||
}
|
||||
const { composer } = this;
|
||||
const composerModel = composer.get("model");
|
||||
const editingSamePost =
|
||||
opts.post.id === composerModel?.post?.id &&
|
||||
opts.action === composerModel?.action &&
|
||||
opts.draftKey === composerModel?.draftKey;
|
||||
|
||||
// Cancel and reopen the composer for the first post
|
||||
if (editingFirst) {
|
||||
composer.cancelComposer(opts).then(() => composer.open(opts));
|
||||
} else {
|
||||
composer.open(opts);
|
||||
}
|
||||
return editingSamePost ? composer.unshrink() : composer.open(opts);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class Toolbar {
|
|||
icon: "code",
|
||||
preventFocus: true,
|
||||
trimLeading: true,
|
||||
action: (...args) => this.context.send("formatCode", args),
|
||||
perform: (e) => e.formatCode(),
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
|
|
|
@ -3,6 +3,7 @@ import { next, schedule } from "@ember/runloop";
|
|||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import $ from "jquery";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import { generateLinkifyFunction } from "discourse/lib/text";
|
||||
import { siteDir } from "discourse/lib/text-direction";
|
||||
import toMarkdown from "discourse/lib/to-markdown";
|
||||
|
@ -28,6 +29,8 @@ const OP = {
|
|||
ADDED: 2,
|
||||
};
|
||||
|
||||
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
export function getHead(head, prev) {
|
||||
|
@ -42,6 +45,7 @@ export default class TextareaTextManipulation {
|
|||
@service appEvents;
|
||||
@service siteSettings;
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
|
||||
eventPrefix;
|
||||
textarea;
|
||||
|
@ -180,6 +184,10 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
}
|
||||
|
||||
applySurroundSelection(head, tail, exampleKey, opts) {
|
||||
this.applySurround(this.getSelected(), head, tail, exampleKey, opts);
|
||||
}
|
||||
|
||||
applySurround(sel, head, tail, exampleKey, opts) {
|
||||
const pre = sel.pre;
|
||||
const post = sel.post;
|
||||
|
@ -730,6 +738,7 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
async inCodeBlock() {
|
||||
return inCodeBlock(
|
||||
this.$textarea.value ?? this.$textarea.val(),
|
||||
|
@ -737,6 +746,7 @@ export default class TextareaTextManipulation {
|
|||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleDirection() {
|
||||
let currentDir = this.$textarea.attr("dir")
|
||||
? this.$textarea.attr("dir")
|
||||
|
@ -746,6 +756,75 @@ export default class TextareaTextManipulation {
|
|||
this.$textarea.attr("dir", newDir).focus();
|
||||
}
|
||||
|
||||
@bind
|
||||
applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.includes("\n")) {
|
||||
this.applySurround(sel, head, "", exampleKey, opts);
|
||||
} else {
|
||||
const [hval, hlen] = getHead(head);
|
||||
if (sel.start === sel.end) {
|
||||
sel.value = i18n(`composer.${exampleKey}`);
|
||||
}
|
||||
|
||||
const number = sel.value.startsWith(hval)
|
||||
? sel.value.slice(hlen)
|
||||
: `${hval}${sel.value}`;
|
||||
|
||||
const preNewlines = sel.pre.trim() && "\n\n";
|
||||
const postNewlines = sel.post.trim() && "\n\n";
|
||||
|
||||
const textToInsert = `${preNewlines}${number}${postNewlines}`;
|
||||
|
||||
const preChars = sel.pre.length - sel.pre.trimEnd().length;
|
||||
const postChars = sel.post.length - sel.post.trimStart().length;
|
||||
|
||||
this._insertAt(sel.start - preChars, sel.end + postChars, textToInsert);
|
||||
this.selectText(
|
||||
sel.start + (preNewlines.length - preChars),
|
||||
number.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
formatCode() {
|
||||
const sel = this.getSelected("", { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.includes("\n");
|
||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||
const isFourSpacesIndent =
|
||||
this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
|
||||
|
||||
if (!hasNewLine) {
|
||||
if (selValue.length === 0 && isBlankLine) {
|
||||
if (isFourSpacesIndent) {
|
||||
const example = i18n(`composer.code_text`);
|
||||
this._insertAt(sel.start, sel.end, ` ${example}`);
|
||||
return this.selectText(sel.pre.length + 4, example.length);
|
||||
} else {
|
||||
return this.applySurround(sel, "```\n", "\n```", "paste_code_text");
|
||||
}
|
||||
} else {
|
||||
return this.applySurround(sel, "`", "`", "code_title");
|
||||
}
|
||||
} else {
|
||||
if (isFourSpacesIndent) {
|
||||
return this.applySurround(sel, " ", "", "code_text");
|
||||
} else {
|
||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||
return this.addText(
|
||||
sel,
|
||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
putCursorAtEnd() {
|
||||
putCursorAtEnd(this.textarea);
|
||||
}
|
||||
|
||||
autocomplete() {
|
||||
return this.$textarea.autocomplete(...arguments);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import Badge from "discourse/models/badge";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
|
@ -37,6 +38,9 @@ export function transformBasicPost(post) {
|
|||
user_id: post.user_id,
|
||||
usernameUrl: userPath(post.username),
|
||||
username: post.username,
|
||||
badgesGranted: post.badges_granted?.map(
|
||||
(badge) => Badge.createFromJson(badge)[0]
|
||||
),
|
||||
avatar_template: post.avatar_template,
|
||||
bookmarked: post.bookmarked,
|
||||
bookmarkReminderAt: post.bookmark_reminder_at,
|
||||
|
|
|
@ -13,8 +13,11 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
|||
"invite-simple-mode-topic",
|
||||
"mentions-class",
|
||||
"more-topics-tabs",
|
||||
"parent-category-row-class-mobile",
|
||||
"parent-category-row-class",
|
||||
"post-menu-buttons",
|
||||
"small-user-attrs",
|
||||
"topic-list-columns",
|
||||
"topic-list-header-sortable-column",
|
||||
"topic-list-item-class",
|
||||
]);
|
||||
|
|
|
@ -156,18 +156,22 @@ export function selectedText() {
|
|||
} else if (oneboxTest) {
|
||||
// This is a partial quote from a onebox.
|
||||
// Treat it as though the entire onebox was quoted.
|
||||
const oneboxUrl = oneboxTest.dataset.oneboxSrc;
|
||||
div.append(oneboxUrl);
|
||||
div.append(oneboxTest.dataset.oneboxSrc);
|
||||
} else {
|
||||
div.append(range.cloneContents());
|
||||
}
|
||||
}
|
||||
|
||||
div.querySelectorAll("aside.onebox[data-onebox-src]").forEach((element) => {
|
||||
const oneboxUrl = element.dataset.oneboxSrc;
|
||||
element.replaceWith(oneboxUrl);
|
||||
element.replaceWith(element.dataset.oneboxSrc);
|
||||
});
|
||||
|
||||
div
|
||||
.querySelectorAll("div.video-placeholder-container[data-video-src]")
|
||||
.forEach((element) => {
|
||||
element.replaceWith(`![|video](${element.dataset.videoSrc})`);
|
||||
});
|
||||
|
||||
return toMarkdown(div.outerHTML);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { isEmpty } from "@ember/utils";
|
|||
import { observes, on } from "@ember-decorators/object";
|
||||
import { Promise } from "rsvp";
|
||||
import { extractError, throwAjaxError } from "discourse/lib/ajax-error";
|
||||
import { propertyNotEqual } from "discourse/lib/computed";
|
||||
import { QUOTE_REGEXP } from "discourse/lib/quote";
|
||||
import { prioritizeNameFallback } from "discourse/lib/settings";
|
||||
import { emailValid, escapeExpression } from "discourse/lib/utilities";
|
||||
|
@ -73,13 +72,15 @@ const CLOSED = "closed",
|
|||
_update_serializer = {
|
||||
raw: "reply",
|
||||
topic_id: "topic.id",
|
||||
raw_old: "rawOld",
|
||||
original_text: "originalText",
|
||||
},
|
||||
_edit_topic_serializer = {
|
||||
title: "topic.title",
|
||||
categoryId: "topic.category.id",
|
||||
tags: "topic.tags",
|
||||
featuredLink: "topic.featured_link",
|
||||
original_title: "originalTitle",
|
||||
original_tags: "originalTags",
|
||||
},
|
||||
_draft_serializer = {
|
||||
reply: "reply",
|
||||
|
@ -94,6 +95,9 @@ const CLOSED = "closed",
|
|||
typingTime: "typingTime",
|
||||
postId: "post.id",
|
||||
recipients: "targetRecipients",
|
||||
original_text: "originalText",
|
||||
original_title: "originalTitle",
|
||||
original_tags: "originalTags",
|
||||
},
|
||||
_add_draft_fields = {},
|
||||
FAST_REPLY_LENGTH_THRESHOLD = 10000;
|
||||
|
@ -210,8 +214,6 @@ export default class Composer extends RestModel {
|
|||
@equal("composeState", FULLSCREEN) viewFullscreen;
|
||||
@or("viewOpen", "viewFullscreen") viewOpenOrFullscreen;
|
||||
@and("editingPost", "post.firstPost") editingFirstPost;
|
||||
@propertyNotEqual("reply", "originalText") replyDirty;
|
||||
@propertyNotEqual("title", "originalTitle") titleDirty;
|
||||
|
||||
@or(
|
||||
"creatingTopic",
|
||||
|
@ -226,6 +228,16 @@ export default class Composer extends RestModel {
|
|||
|
||||
@tracked _categoryId = null;
|
||||
|
||||
@discourseComputed("reply", "originalText")
|
||||
replyDirty(reply, original) {
|
||||
return (reply || "").trim() !== (original || "").trim();
|
||||
}
|
||||
|
||||
@discourseComputed("title", "originalTitle")
|
||||
titleDirty(title, original) {
|
||||
return (title || "").trim() !== (original || "").trim();
|
||||
}
|
||||
|
||||
@dependentKeyCompat
|
||||
get categoryId() {
|
||||
return this._categoryId;
|
||||
|
@ -787,6 +799,7 @@ export default class Composer extends RestModel {
|
|||
const category = Category.findById(categoryId);
|
||||
if (category) {
|
||||
this.set("reply", category.topic_template || "");
|
||||
this.set("originalText", category.topic_template || "");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -861,6 +874,9 @@ export default class Composer extends RestModel {
|
|||
whisper: opts.whisper,
|
||||
tags: opts.tags || [],
|
||||
noBump: opts.noBump,
|
||||
originalText: opts.originalText,
|
||||
originalTitle: opts.originalTitle,
|
||||
originalTags: opts.originalTags,
|
||||
});
|
||||
|
||||
if (opts.post) {
|
||||
|
@ -926,6 +942,13 @@ export default class Composer extends RestModel {
|
|||
reply: post.raw,
|
||||
originalText: post.raw,
|
||||
});
|
||||
|
||||
if (post.post_number === 1 && this.canEditTitle) {
|
||||
this.setProperties({
|
||||
originalTitle: post.topic.title,
|
||||
originalTags: post.topic.tags,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// edge case ... make a post then edit right away
|
||||
|
@ -945,24 +968,18 @@ export default class Composer extends RestModel {
|
|||
});
|
||||
});
|
||||
} else if (opts.action === REPLY && opts.quote) {
|
||||
this.setProperties({
|
||||
reply: opts.quote,
|
||||
originalText: opts.quote,
|
||||
});
|
||||
this.set("reply", opts.quote);
|
||||
this.set("originalText", opts.quote);
|
||||
}
|
||||
|
||||
if (opts.title) {
|
||||
this.set("title", opts.title);
|
||||
}
|
||||
|
||||
const isDraft = opts.draft || opts.skipDraftCheck;
|
||||
this.set("originalText", isDraft ? "" : this.reply);
|
||||
|
||||
if (this.canEditTitle) {
|
||||
if (isEmpty(this.title) && this.title !== "") {
|
||||
this.set("title", "");
|
||||
}
|
||||
this.set("originalTitle", this.title);
|
||||
}
|
||||
|
||||
if (!isEdit(opts.action) || !opts.post) {
|
||||
|
@ -1001,6 +1018,8 @@ export default class Composer extends RestModel {
|
|||
clearState() {
|
||||
this.setProperties({
|
||||
originalText: null,
|
||||
originalTitle: null,
|
||||
originalTags: null,
|
||||
reply: null,
|
||||
post: null,
|
||||
title: null,
|
||||
|
@ -1016,12 +1035,9 @@ export default class Composer extends RestModel {
|
|||
});
|
||||
}
|
||||
|
||||
@discourseComputed("editConflict", "originalText")
|
||||
rawOld(editConflict, originalText) {
|
||||
return editConflict ? null : originalText;
|
||||
}
|
||||
|
||||
editPost(opts) {
|
||||
this.set("composeState", SAVING);
|
||||
|
||||
const post = this.post;
|
||||
const oldCooked = post.cooked;
|
||||
let promise = Promise.resolve();
|
||||
|
@ -1054,14 +1070,19 @@ export default class Composer extends RestModel {
|
|||
}
|
||||
}
|
||||
|
||||
const props = {
|
||||
let props = {
|
||||
edit_reason: opts.editReason,
|
||||
image_sizes: opts.imageSizes,
|
||||
cooked: this.getCookedHtml(),
|
||||
};
|
||||
|
||||
this.serialize(_update_serializer, props);
|
||||
this.set("composeState", SAVING);
|
||||
|
||||
// user clicked "overwrite edits" button
|
||||
if (this.editConflict) {
|
||||
delete props.original_text;
|
||||
delete props.original_title;
|
||||
delete props.original_tags;
|
||||
}
|
||||
|
||||
const rollback = throwAjaxError((error) => {
|
||||
post.setProperties("cooked", oldCooked);
|
||||
|
@ -1071,7 +1092,8 @@ export default class Composer extends RestModel {
|
|||
}
|
||||
});
|
||||
|
||||
post.setProperties({ cooked: props.cooked, staged: true });
|
||||
const cooked = this.getCookedHtml();
|
||||
post.setProperties({ cooked, staged: true });
|
||||
this.appEvents.trigger("post-stream:refresh", { id: post.id });
|
||||
|
||||
return promise
|
||||
|
@ -1292,16 +1314,9 @@ export default class Composer extends RestModel {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
draftSaving: true,
|
||||
draftConflictUser: null,
|
||||
});
|
||||
this.set("draftSaving", true);
|
||||
|
||||
let data = this.serialize(_draft_serializer);
|
||||
|
||||
if (data.postId && !isEmpty(this.originalText)) {
|
||||
data.originalText = this.originalText;
|
||||
}
|
||||
const data = this.serialize(_draft_serializer);
|
||||
|
||||
const draftSequence = this.draftSequence;
|
||||
this.set("draftSequence", this.draftSequence + 1);
|
||||
|
|
|
@ -21,14 +21,10 @@ export default class HistoryStore extends Service {
|
|||
|
||||
#routeData = new TrackedMap();
|
||||
#uuid;
|
||||
#pendingStore;
|
||||
#pendingStore = DEBUG && isTesting() ? new TrackedMap() : null;
|
||||
|
||||
get #currentStore() {
|
||||
if (this.#pendingStore) {
|
||||
return this.#pendingStore;
|
||||
}
|
||||
|
||||
return this.#dataFor(this.#uuid);
|
||||
return this.#pendingStore || this.#dataFor(this.#uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,10 +80,7 @@ export default class HistoryStore extends Service {
|
|||
if (key === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (key === this.#uuid) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return key !== this.#uuid;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,7 +119,6 @@ export default class HistoryStore extends Service {
|
|||
|
||||
if (DEBUG && isTesting()) {
|
||||
// Can't use window.history in tests
|
||||
this.#pendingStore = new TrackedMap();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<PluginOutlet
|
||||
@name="about-wrapper"
|
||||
@outletArgs={{hash model=this.model contactInfo=this.contactInfo}}
|
||||
@outletArgs={{hash
|
||||
model=this.model
|
||||
contactInfo=this.contactInfo
|
||||
faqOverridden=this.faqOverridden
|
||||
}}
|
||||
>
|
||||
{{body-class "about-page"}}
|
||||
|
||||
|
|
|
@ -52,58 +52,61 @@
|
|||
<div class="edit-topic-title">
|
||||
<PrivateMessageGlyph @shouldShow={{this.model.isPrivateMessage}} />
|
||||
|
||||
<PluginOutlet
|
||||
@name="edit-topic-title"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
>
|
||||
<TextField
|
||||
@id="edit-title"
|
||||
@value={{this.buffered.title}}
|
||||
@maxlength={{this.siteSettings.max_topic_title_length}}
|
||||
@autofocus="true"
|
||||
/>
|
||||
</PluginOutlet>
|
||||
|
||||
{{#if this.showCategoryChooser}}
|
||||
<div class="edit-title__wrapper">
|
||||
<PluginOutlet
|
||||
@name="edit-topic-category"
|
||||
@name="edit-topic-title"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
>
|
||||
<CategoryChooser
|
||||
@value={{this.buffered.category_id}}
|
||||
@onChange={{action "topicCategoryChanged"}}
|
||||
class="small"
|
||||
<TextField
|
||||
@id="edit-title"
|
||||
@value={{this.buffered.title}}
|
||||
@maxlength={{this.siteSettings.max_topic_title_length}}
|
||||
@autofocus="true"
|
||||
/>
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
|
||||
{{#if this.showCategoryChooser}}
|
||||
<div class="edit-category__wrapper">
|
||||
<PluginOutlet
|
||||
@name="edit-topic-category"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
>
|
||||
<CategoryChooser
|
||||
@value={{this.buffered.category_id}}
|
||||
@onChange={{action "topicCategoryChanged"}}
|
||||
class="small"
|
||||
/>
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canEditTags}}
|
||||
<PluginOutlet
|
||||
@name="edit-topic-tags"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
>
|
||||
<MiniTagChooser
|
||||
@value={{this.buffered.tags}}
|
||||
@onChange={{action "topicTagsChanged"}}
|
||||
@options={{hash
|
||||
filterable=true
|
||||
categoryId=this.buffered.category_id
|
||||
minimum=this.minimumRequiredTags
|
||||
filterPlaceholder="tagging.choose_for_topic"
|
||||
useHeaderFilter=true
|
||||
}}
|
||||
/>
|
||||
</PluginOutlet>
|
||||
|
||||
<div class="edit-tags__wrapper">
|
||||
<PluginOutlet
|
||||
@name="edit-topic-tags"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
>
|
||||
<MiniTagChooser
|
||||
@value={{this.buffered.tags}}
|
||||
@onChange={{action "topicTagsChanged"}}
|
||||
@options={{hash
|
||||
filterable=true
|
||||
categoryId=this.buffered.category_id
|
||||
minimum=this.minimumRequiredTags
|
||||
filterPlaceholder="tagging.choose_for_topic"
|
||||
useHeaderFilter=true
|
||||
}}
|
||||
/>
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="edit-topic"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
/>
|
||||
</span>
|
||||
<PluginOutlet
|
||||
@name="edit-topic"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=this.model buffered=this.buffered}}
|
||||
/>
|
||||
|
||||
<div class="edit-controls">
|
||||
<DButton
|
||||
|
|
|
@ -1,95 +1,95 @@
|
|||
{{body-class "user-activity-page"}}
|
||||
|
||||
<div class="user-navigation user-navigation-secondary">
|
||||
<HorizontalOverflowNav @ariaLabel="User secondary - activity">
|
||||
<DNavigationItem
|
||||
@route="userActivity.index"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-all"
|
||||
>
|
||||
{{d-icon "bars-staggered"}}
|
||||
<span>{{i18n "user.filters.all"}}</span>
|
||||
</DNavigationItem>
|
||||
|
||||
<DNavigationItem
|
||||
@route="userActivity.topics"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-topics"
|
||||
>
|
||||
{{d-icon "list-ul"}}
|
||||
<span>{{i18n "user_action_groups.4"}}</span>
|
||||
</DNavigationItem>
|
||||
<DNavigationItem
|
||||
@route="userActivity.replies"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-replies"
|
||||
>
|
||||
{{d-icon "reply"}}
|
||||
<span>{{i18n "user_action_groups.5"}}</span>
|
||||
</DNavigationItem>
|
||||
|
||||
{{#if this.user.showRead}}
|
||||
<PluginOutlet @name="user-activity-navigation-wrapper">
|
||||
<div class="user-navigation user-navigation-secondary">
|
||||
<HorizontalOverflowNav @ariaLabel="User secondary - activity">
|
||||
<DNavigationItem
|
||||
@route="userActivity.read"
|
||||
@route="userActivity.index"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-read"
|
||||
title={{i18n "user.read_help"}}
|
||||
class="user-nav__activity-all"
|
||||
>
|
||||
{{d-icon "clock-rotate-left"}}
|
||||
<span>{{i18n "user.read"}}</span>
|
||||
{{d-icon "bars-staggered"}}
|
||||
<span>{{i18n "user.filters.all"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.showDrafts}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.drafts"
|
||||
@route="userActivity.topics"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-drafts"
|
||||
class="user-nav__activity-topics"
|
||||
>
|
||||
{{d-icon "pencil"}}
|
||||
<span>{{this.draftLabel}}</span>
|
||||
{{d-icon "list-ul"}}
|
||||
<span>{{i18n "user_action_groups.4"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.model.pending_posts_count 0)}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.pending"
|
||||
@route="userActivity.replies"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-pending"
|
||||
class="user-nav__activity-replies"
|
||||
>
|
||||
{{d-icon "clock"}}
|
||||
<span>{{this.pendingLabel}}</span>
|
||||
{{d-icon "reply"}}
|
||||
<span>{{i18n "user_action_groups.5"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
<DNavigationItem
|
||||
@route="userActivity.likesGiven"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-likes"
|
||||
>
|
||||
{{d-icon "heart"}}
|
||||
<span>{{i18n "user_action_groups.1"}}</span>
|
||||
</DNavigationItem>
|
||||
{{#if this.user.showRead}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.read"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-read"
|
||||
title={{i18n "user.read_help"}}
|
||||
>
|
||||
{{d-icon "clock-rotate-left"}}
|
||||
<span>{{i18n "user.read"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.showDrafts}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.drafts"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-drafts"
|
||||
>
|
||||
{{d-icon "pencil"}}
|
||||
<span>{{this.draftLabel}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.model.pending_posts_count 0)}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.pending"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-pending"
|
||||
>
|
||||
{{d-icon "clock"}}
|
||||
<span>{{this.pendingLabel}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.showBookmarks}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.bookmarks"
|
||||
@route="userActivity.likesGiven"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-bookmarks"
|
||||
class="user-nav__activity-likes"
|
||||
>
|
||||
{{d-icon "bookmark"}}
|
||||
<span>{{i18n "user_action_groups.3"}}</span>
|
||||
{{d-icon "heart"}}
|
||||
<span>{{i18n "user_action_groups.1"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="user-activity-bottom"
|
||||
@connectorTagName="li"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
</HorizontalOverflowNav>
|
||||
</div>
|
||||
{{#if this.user.showBookmarks}}
|
||||
<DNavigationItem
|
||||
@route="userActivity.bookmarks"
|
||||
@ariaCurrentContext="subNav"
|
||||
class="user-nav__activity-bookmarks"
|
||||
>
|
||||
{{d-icon "bookmark"}}
|
||||
<span>{{i18n "user_action_groups.3"}}</span>
|
||||
</DNavigationItem>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="user-activity-bottom"
|
||||
@connectorTagName="li"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
</HorizontalOverflowNav>
|
||||
</div>
|
||||
</PluginOutlet>
|
||||
<section class="user-content" id="user-content">
|
||||
{{outlet}}
|
||||
</section>
|
|
@ -1,6 +1,8 @@
|
|||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { h } from "virtual-dom";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import { applyDecorators, createWidget } from "discourse/widgets/widget";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
@ -148,29 +150,52 @@ export default createWidget("poster-name", {
|
|||
h("span", { className: classNames.join(" ") }, nameContents),
|
||||
];
|
||||
|
||||
if (!this.settings.showNameAndGroup) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
if (
|
||||
name &&
|
||||
this.siteSettings.display_name_on_posts &&
|
||||
sanitizeName(name) !== sanitizeName(username)
|
||||
) {
|
||||
contents.push(
|
||||
h(
|
||||
"span.second." + (nameFirst ? "username" : "full-name"),
|
||||
[this.userLink(attrs, nameFirst ? username : name)].concat(
|
||||
afterNameContents
|
||||
if (this.settings.showNameAndGroup) {
|
||||
if (
|
||||
name &&
|
||||
this.siteSettings.display_name_on_posts &&
|
||||
sanitizeName(name) !== sanitizeName(username)
|
||||
) {
|
||||
contents.push(
|
||||
h(
|
||||
"span.second." + (nameFirst ? "username" : "full-name"),
|
||||
[this.userLink(attrs, nameFirst ? username : name)].concat(
|
||||
afterNameContents
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
this.buildTitleObject(attrs, contents);
|
||||
|
||||
if (this.siteSettings.enable_user_status) {
|
||||
this.addUserStatus(contents, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
this.buildTitleObject(attrs, contents);
|
||||
if (attrs.badgesGranted?.length) {
|
||||
const badges = [];
|
||||
|
||||
if (this.siteSettings.enable_user_status) {
|
||||
this.addUserStatus(contents, attrs);
|
||||
attrs.badgesGranted.forEach((badge) => {
|
||||
// Alter the badge description to show that the badge was granted for this post.
|
||||
badge.description = i18n("post.badge_granted_tooltip", {
|
||||
username: attrs.username,
|
||||
badge_name: badge.name,
|
||||
});
|
||||
|
||||
const badgeIcon = new RenderGlimmer(
|
||||
this,
|
||||
`span.user-badge-button-${badge.slug}`,
|
||||
hbs`<UserBadge @badge={{@data.badge}} @user={{@data.user}} @showName={{false}} />`,
|
||||
{
|
||||
badge,
|
||||
user: attrs.user,
|
||||
}
|
||||
);
|
||||
badges.push(badgeIcon);
|
||||
});
|
||||
|
||||
contents.push(h("span.user-badge-buttons", badges));
|
||||
}
|
||||
|
||||
return contents;
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"test": "ember test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@glimmer/syntax": "^0.92.3",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@glimmer/syntax": "^0.93.1",
|
||||
"@highlightjs/cdn-assets": "^11.10.0",
|
||||
"@json-editor/json-editor": "2.15.2",
|
||||
"@messageformat/core": "^3.4.0",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@swc/core": "^1.9.3",
|
||||
"@swc/core": "^1.10.0",
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/qunit": "^2.19.12",
|
||||
"@types/rsvp": "^4.0.9",
|
||||
|
@ -107,13 +107,13 @@
|
|||
"imports-loader": "^5.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsuites": "^5.7.2",
|
||||
"jsuites": "^5.8.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"make-plural": "^7.4.0",
|
||||
"message-bus-client": "^4.3.8",
|
||||
"pretender": "^3.4.7",
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.3.0",
|
||||
"qunit": "^2.23.0",
|
||||
"qunit-dom": "^3.4.0",
|
||||
"sass": "^1.77.7",
|
||||
"select-kit": "workspace:1.0.0",
|
||||
"sinon": "^19.0.2",
|
||||
|
@ -123,7 +123,7 @@
|
|||
"truth-helpers": "workspace:1.0.0",
|
||||
"util": "^0.12.5",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack": "^5.97.0",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"webpack-stats-plugin": "^1.1.3",
|
||||
"xss": "^1.0.15"
|
||||
|
|
|
@ -6,7 +6,6 @@ import { toggleCheckDraftPopup } from "discourse/services/composer";
|
|||
import userFixtures from "discourse/tests/fixtures/user-fixtures";
|
||||
import {
|
||||
acceptance,
|
||||
query,
|
||||
selectText,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
|
@ -127,7 +126,7 @@ acceptance("Composer Actions", function (needs) {
|
|||
|
||||
assert.strictEqual(categoryChooserReplyArea.header().name(), "faq");
|
||||
assert.dom(".action-title").hasText(i18n("topic.create_long"));
|
||||
assert.true(query(".d-editor-input").value.includes(quote));
|
||||
assert.dom(".d-editor-input").includesValue(quote);
|
||||
});
|
||||
|
||||
test("reply_as_new_topic without a new_topic draft", async function (assert) {
|
||||
|
@ -210,7 +209,7 @@ acceptance("Composer Actions", function (needs) {
|
|||
await composerActions.expand();
|
||||
|
||||
assert.dom(".action-title").hasText(i18n("topic.create_long"));
|
||||
assert.true(query(".d-editor-input").value.includes(quote));
|
||||
assert.dom(".d-editor-input").includesValue(quote);
|
||||
assert.strictEqual(composerActions.rowByIndex(0).value(), "reply_to_post");
|
||||
assert.strictEqual(composerActions.rowByIndex(1).value(), "reply_to_topic");
|
||||
assert.strictEqual(composerActions.rowByIndex(2).value(), "shared_draft");
|
||||
|
|
|
@ -13,17 +13,17 @@ acceptance("Composer - Edit conflict", function (needs) {
|
|||
});
|
||||
});
|
||||
|
||||
test("Should not send originalText when posting a new reply", async function (assert) {
|
||||
test("Should not send 'original_text' when posting a new reply", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:nth-of-type(1) button.reply");
|
||||
await fillIn(
|
||||
".d-editor-input",
|
||||
"hello world hello world hello world hello world hello world"
|
||||
);
|
||||
assert.false(lastBody.includes("originalText"));
|
||||
assert.false(lastBody.includes("original_text"));
|
||||
});
|
||||
|
||||
test("Should send originalText when editing a reply", async function (assert) {
|
||||
test("Should send 'original_text' when editing a reply", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:nth-of-type(1) button.show-more-actions");
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
|
@ -31,6 +31,6 @@ acceptance("Composer - Edit conflict", function (needs) {
|
|||
".d-editor-input",
|
||||
"hello world hello world hello world hello world hello world"
|
||||
);
|
||||
assert.true(lastBody.includes("originalText"));
|
||||
assert.true(lastBody.includes("original_text"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
acceptance("Composer - Image Preview", function (needs) {
|
||||
needs.user({});
|
||||
|
@ -339,10 +339,9 @@ acceptance("Composer - Image Preview", function (needs) {
|
|||
.dom(".d-editor-input")
|
||||
.hasValue(uploads.join("\n"), "Image should be removed from the editor");
|
||||
|
||||
assert.false(
|
||||
query(".d-editor-input").value.includes("image_example_0"),
|
||||
"does not have the first image"
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.doesNotIncludeValue("image_example_0", "does not have the first image");
|
||||
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
|
|
|
@ -576,7 +576,7 @@ acceptance("Composer", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("Composer can toggle between edit and reply", async function (assert) {
|
||||
test("Composer can toggle between edit and reply on the OP", async function (assert) {
|
||||
await visit("/t/this-is-a-test-topic/9");
|
||||
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
|
@ -586,8 +586,10 @@ acceptance("Composer", function (needs) {
|
|||
/^This is the first post\./,
|
||||
"populates the input with the post text"
|
||||
);
|
||||
|
||||
await click(".topic-post:nth-of-type(1) button.reply");
|
||||
assert.dom(".d-editor-input").hasNoValue("clears the input");
|
||||
assert.dom(".d-editor-input").hasNoValue("clears the composer input");
|
||||
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
|
@ -597,6 +599,27 @@ acceptance("Composer", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("Composer can toggle between edit and reply on a reply", async function (assert) {
|
||||
await visit("/t/this-is-a-test-topic/9");
|
||||
|
||||
await click(".topic-post:nth-of-type(2) button.edit");
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.hasValue(
|
||||
/^This is the second post\./,
|
||||
"populates the input with the post text"
|
||||
);
|
||||
|
||||
await click(".topic-post:nth-of-type(2) button.reply");
|
||||
assert.dom(".d-editor-input").hasNoValue("clears the composer input");
|
||||
|
||||
await click(".topic-post:nth-of-type(2) button.edit");
|
||||
assert.true(
|
||||
query(".d-editor-input").value.startsWith("This is the second post."),
|
||||
"populates the input with the post text"
|
||||
);
|
||||
});
|
||||
|
||||
test("Composer can toggle whispers when whisperer user", async function (assert) {
|
||||
const menu = selectKit(".toolbar-popup-menu-options");
|
||||
|
||||
|
@ -802,6 +825,7 @@ acceptance("Composer", function (needs) {
|
|||
i18n("post.cancel_composer.keep_editing"),
|
||||
"has keep editing button"
|
||||
);
|
||||
|
||||
await click(".d-modal__footer button.save-draft");
|
||||
assert.dom(".d-editor-input").hasNoValue("clears the composer input");
|
||||
});
|
||||
|
@ -847,10 +871,9 @@ acceptance("Composer", function (needs) {
|
|||
|
||||
assert.dom(".d-modal__body").doesNotExist("abandon popup shouldn't come");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(longText),
|
||||
"entered text should still be there"
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue(longText, "entered text should still be there");
|
||||
|
||||
assert
|
||||
.dom('.action-title a[href="/t/internationalization-localization/280"]')
|
||||
|
@ -1293,32 +1316,6 @@ acceptance("Composer - default category not set", function (needs) {
|
|||
});
|
||||
// END: Default Composer Category tests
|
||||
|
||||
acceptance("Composer - current time", function (needs) {
|
||||
needs.user();
|
||||
|
||||
test("composer insert current time shortcut", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
assert.dom(".d-editor-input").exists("the composer input is visible");
|
||||
await fillIn(".d-editor-input", "and the time now is: ");
|
||||
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
await triggerKeyEvent(".d-editor-input", "keydown", ".", {
|
||||
...metaModifier,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
assert.true(
|
||||
query("#reply-control .d-editor-input")
|
||||
.value.trim()
|
||||
.startsWith(`and the time now is: [date=${date}`),
|
||||
"adds the current date"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("composer buttons API", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
|
|
|
@ -627,12 +627,12 @@ acceptance("Search - Authenticated", function (needs) {
|
|||
.dom(".d-editor-input")
|
||||
.hasValue(/a link/, "still has the original composer content");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
searchFixtures["search/query"].topics[0].slug
|
||||
),
|
||||
"adds link from search to composer"
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue(
|
||||
searchFixtures["search/query"].topics[0].slug,
|
||||
"adds link from search to composer"
|
||||
);
|
||||
});
|
||||
|
||||
// see https://meta.discourse.org/t/keyboard-navigation-messes-up-the-search-menu/285405
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import CategoryFixtures from "discourse/tests/fixtures/category-fixtures";
|
||||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
|
@ -37,12 +37,12 @@ acceptance("Share and Invite modal", function (needs) {
|
|||
.dom("#modal-alert.alert-warning")
|
||||
.doesNotExist("it does not show the alert with restricted groups");
|
||||
|
||||
assert.true(
|
||||
query("input.invite-link").value.includes(
|
||||
"/t/internationalization-localization/280?u=eviltrout"
|
||||
),
|
||||
"it shows the topic sharing url"
|
||||
);
|
||||
assert
|
||||
.dom("input.invite-link")
|
||||
.includesValue(
|
||||
"/t/internationalization-localization/280?u=eviltrout",
|
||||
"shows the topic sharing url"
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".link-share-actions .invite")
|
||||
|
@ -127,10 +127,12 @@ acceptance("Share url with badges disabled - desktop", function (needs) {
|
|||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-button-share-and-invite");
|
||||
|
||||
assert.false(
|
||||
query("input.invite-link").value.includes("?u=eviltrout"),
|
||||
"it doesn't add the username param when badges are disabled"
|
||||
);
|
||||
assert
|
||||
.dom("input.invite-link")
|
||||
.doesNotIncludeValue(
|
||||
"?u=eviltrout",
|
||||
"doesn't add the username param when badges are disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -148,9 +150,11 @@ acceptance("With username in share links disabled - desktop", function (needs) {
|
|||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-button-share-and-invite");
|
||||
|
||||
assert.false(
|
||||
query("input.invite-link").value.includes("?u=eviltrout"),
|
||||
"it doesn't add the username param when username in share links are disabled"
|
||||
);
|
||||
assert
|
||||
.dom("input.invite-link")
|
||||
.doesNotIncludeValue(
|
||||
"?u=eviltrout",
|
||||
"doesn't add the username param when username in share links are disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
acceptance,
|
||||
chromeTest,
|
||||
publishToMessageBus,
|
||||
query,
|
||||
selectText,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
@ -372,11 +371,9 @@ acceptance("Topic featured links", function (needs) {
|
|||
await selectText("#post_5 blockquote");
|
||||
await click(".quote-button .insert-quote");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
'quote="codinghorror said, post:3, topic:280"'
|
||||
)
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue('quote="codinghorror said, post:3, topic:280"');
|
||||
});
|
||||
|
||||
test("Quoting a quote of a different topic keeps the original topic title", async function (assert) {
|
||||
|
@ -384,11 +381,11 @@ acceptance("Topic featured links", function (needs) {
|
|||
await selectText("#post_9 blockquote");
|
||||
await click(".quote-button .insert-quote");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue(
|
||||
'quote="A new topic with a link to another topic, post:3, topic:62"'
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
test("Quoting a quote with the Reply button keeps the original poster name", async function (assert) {
|
||||
|
@ -396,11 +393,9 @@ acceptance("Topic featured links", function (needs) {
|
|||
await selectText("#post_5 blockquote");
|
||||
await click(".reply");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
'quote="codinghorror said, post:3, topic:280"'
|
||||
)
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue('quote="codinghorror said, post:3, topic:280"');
|
||||
});
|
||||
|
||||
// Using J/K on Firefox clean the text selection, so this won't work there
|
||||
|
@ -412,11 +407,9 @@ acceptance("Topic featured links", function (needs) {
|
|||
await triggerKeyEvent(document, "keypress", "J");
|
||||
await triggerKeyEvent(document, "keypress", "T");
|
||||
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
'quote="codinghorror said, post:3, topic:280"'
|
||||
)
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue('quote="codinghorror said, post:3, topic:280"');
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -424,11 +417,9 @@ acceptance("Topic featured links", function (needs) {
|
|||
await visit("/t/internationalization-localization/280");
|
||||
await selectText("#post_5 .cooked");
|
||||
await click(".quote-button .insert-quote");
|
||||
assert.true(
|
||||
query(".d-editor-input").value.includes(
|
||||
'quote="pekka, post:5, topic:280, full:true"'
|
||||
)
|
||||
);
|
||||
assert
|
||||
.dom(".d-editor-input")
|
||||
.includesValue('quote="pekka, post:5, topic:280, full:true"');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ export default {
|
|||
grant_count: 7,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -64,7 +64,7 @@ export default {
|
|||
grant_count: 30,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -334,7 +334,7 @@ export default {
|
|||
grant_count: 7,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -364,7 +364,7 @@ export default {
|
|||
grant_count: 30,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -2462,7 +2462,7 @@ export default {
|
|||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -2758,7 +2758,7 @@ export default {
|
|||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -2837,7 +2837,7 @@ export default {
|
|||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -2907,7 +2907,7 @@ export default {
|
|||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -3195,7 +3195,7 @@ export default {
|
|||
grant_count: 7,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -3225,7 +3225,7 @@ export default {
|
|||
grant_count: 30,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
icon: "user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
|
|
@ -77,4 +77,22 @@ module("Integration | Component | badge-button", function (hooks) {
|
|||
|
||||
assert.dom(".user-badge.foo").exists();
|
||||
});
|
||||
|
||||
test("setting showName to false hides the name", async function (assert) {
|
||||
this.set("badge", { name: "foo" });
|
||||
|
||||
await render(
|
||||
hbs`<BadgeButton @badge={{this.badge}} @showName={{false}} />`
|
||||
);
|
||||
|
||||
assert.dom(".badge-display-name").doesNotExist();
|
||||
});
|
||||
|
||||
test("showName defaults to true", async function (assert) {
|
||||
this.set("badge", { name: "foo" });
|
||||
|
||||
await render(hbs`<BadgeButton @badge={{this.badge}} />`);
|
||||
|
||||
assert.dom(".badge-display-name").exists();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -344,6 +344,26 @@ third line`
|
|||
assert.strictEqual(textarea.selectionEnd, 23);
|
||||
});
|
||||
|
||||
test("code button does not reset undo history", async function (assert) {
|
||||
this.set("value", "existing");
|
||||
|
||||
await render(hbs`<DEditor @value={{this.value}} />`);
|
||||
const textarea = query("textarea.d-editor-input");
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
await click("button.code");
|
||||
assert.strictEqual(this.value, "`existing`");
|
||||
|
||||
await click("button.code");
|
||||
assert.strictEqual(this.value, "existing");
|
||||
|
||||
document.execCommand("undo");
|
||||
assert.strictEqual(this.value, "`existing`");
|
||||
document.execCommand("undo");
|
||||
assert.strictEqual(this.value, "existing");
|
||||
});
|
||||
|
||||
test("code fences", async function (assert) {
|
||||
this.set("value", "");
|
||||
|
||||
|
@ -615,6 +635,22 @@ third line`
|
|||
assert.strictEqual(textarea.selectionEnd, 18);
|
||||
});
|
||||
|
||||
testCase(
|
||||
"list button does not reset undo history",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", "existing");
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
await click("button.list");
|
||||
assert.strictEqual(this.value, "1. existing");
|
||||
|
||||
document.execCommand("undo");
|
||||
|
||||
assert.strictEqual(this.value, "existing");
|
||||
}
|
||||
);
|
||||
|
||||
test("clicking the toggle-direction changes dir from ltr to rtl and back", async function (assert) {
|
||||
this.siteSettings.support_mixed_text_direction = true;
|
||||
this.siteSettings.default_locale = "en";
|
||||
|
|
|
@ -31,7 +31,7 @@ module("Integration | Component | select-kit/tag-drop", function (hooks) {
|
|||
await render(<template>
|
||||
<TagDrop
|
||||
@currentCategory={{category}}
|
||||
@tagId={{"jeff"}}
|
||||
@tagId="jeff"
|
||||
@options={{hash tagId="jeff"}}
|
||||
/>
|
||||
</template>);
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import TopicListItem from "discourse/components/topic-list/item";
|
||||
import HbrTopicListItem from "discourse/components/topic-list-item";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | topic-list-item", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("checkbox is rendered checked if topic is in selected array", async function (assert) {
|
||||
const store = this.owner.lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 24234 });
|
||||
const topic2 = store.createRecord("topic", { id: 24235 });
|
||||
const selected = [topic];
|
||||
|
||||
await render(<template>
|
||||
<HbrTopicListItem
|
||||
@topic={{topic}}
|
||||
@bulkSelectEnabled={{true}}
|
||||
@selected={{selected}}
|
||||
/>
|
||||
<HbrTopicListItem
|
||||
@topic={{topic2}}
|
||||
@bulkSelectEnabled={{true}}
|
||||
@selected={{selected}}
|
||||
/>
|
||||
</template>);
|
||||
|
||||
const checkboxes = [...document.querySelectorAll("input.bulk-select")];
|
||||
assert.dom(checkboxes[0]).isChecked();
|
||||
assert.dom(checkboxes[1]).isNotChecked();
|
||||
});
|
||||
|
||||
test("topic-list-item-class value transformer", async function (assert) {
|
||||
withPluginApi("1.39.0", (api) => {
|
||||
api.registerValueTransformer(
|
||||
"topic-list-item-class",
|
||||
({ value, context }) => {
|
||||
if (context.topic.get("foo")) {
|
||||
value.push("bar");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const store = this.owner.lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 1234, foo: true });
|
||||
const topic2 = store.createRecord("topic", { id: 1235, foo: false });
|
||||
await render(<template>
|
||||
<TopicListItem @topic={{topic}} />
|
||||
<TopicListItem @topic={{topic2}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".topic-list-item[data-topic-id='1234']").hasClass("bar");
|
||||
assert
|
||||
.dom(".topic-list-item[data-topic-id='1235']")
|
||||
.doesNotHaveClass("bar");
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { getOwner } from "@ember/owner";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | topic-list-item", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("checkbox is rendered checked if topic is in selected array", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 24234 });
|
||||
const topic2 = store.createRecord("topic", { id: 24235 });
|
||||
this.setProperties({
|
||||
topic,
|
||||
topic2,
|
||||
selected: [topic],
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TopicListItem
|
||||
@topic={{this.topic}}
|
||||
@bulkSelectEnabled={{true}}
|
||||
@selected={{this.selected}}
|
||||
/>
|
||||
<TopicListItem
|
||||
@topic={{this.topic2}}
|
||||
@bulkSelectEnabled={{true}}
|
||||
@selected={{this.selected}}
|
||||
/>
|
||||
`);
|
||||
|
||||
const checkboxes = [...document.querySelectorAll("input.bulk-select")];
|
||||
assert.dom(checkboxes[0]).isChecked();
|
||||
assert.dom(checkboxes[1]).isNotChecked();
|
||||
});
|
||||
});
|
|
@ -1,6 +1,8 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import Badge from "discourse/models/badge";
|
||||
import User from "discourse/models/user";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | Widget | poster-name", function (hooks) {
|
||||
|
@ -70,4 +72,38 @@ module("Integration | Component | Widget | poster-name", function (hooks) {
|
|||
|
||||
assert.dom(".second").doesNotExist();
|
||||
});
|
||||
|
||||
test("renders badges that are passed in", async function (assert) {
|
||||
this.set("args", {
|
||||
username: "eviltrout",
|
||||
usernameUrl: "/u/eviltrout",
|
||||
user: User.create({
|
||||
username: "eviltrout",
|
||||
}),
|
||||
badgesGranted: [
|
||||
{ id: 1, icon: "heart", slug: "badge1", name: "Badge One" },
|
||||
{ id: 2, icon: "target", slug: "badge2", name: "Badge Two" },
|
||||
].map((badge) => Badge.createFromJson({ badges: [badge] })[0]),
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="poster-name" @args={{this.args}} />`
|
||||
);
|
||||
|
||||
// Check that the custom CSS classes are set
|
||||
assert.dom("span.user-badge-button-badge1").exists();
|
||||
assert.dom("span.user-badge-button-badge2").exists();
|
||||
|
||||
// Check that the custom titles are set
|
||||
assert.dom("span.user-badge[title*='Badge One']").exists();
|
||||
assert.dom("span.user-badge[title*='Badge Two']").exists();
|
||||
|
||||
// Check that the badges link to the correct badge page
|
||||
assert
|
||||
.dom("a.user-card-badge-link[href='/badges/1/badge1?username=eviltrout']")
|
||||
.exists();
|
||||
assert
|
||||
.dom("a.user-card-badge-link[href='/badges/2/badge2?username=eviltrout']")
|
||||
.exists();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { and } from "truth-helpers";
|
||||
|
@ -31,6 +34,28 @@ export default class DMenu extends Component {
|
|||
};
|
||||
});
|
||||
|
||||
@action
|
||||
registerFloatBody(element) {
|
||||
this.body = element;
|
||||
}
|
||||
|
||||
@action
|
||||
forwardTabToContent(event) {
|
||||
if (!this.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
|
||||
const firstFocusable = this.body.querySelector(
|
||||
'button, a, input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
firstFocusable?.focus() || this.body.focus();
|
||||
}
|
||||
}
|
||||
|
||||
get menuId() {
|
||||
return `d-menu-${this.menuInstance.id}`;
|
||||
}
|
||||
|
@ -73,6 +98,7 @@ export default class DMenu extends Component {
|
|||
@translatedTitle={{@title}}
|
||||
@disabled={{@disabled}}
|
||||
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
||||
{{on "keydown" this.forwardTabToContent}}
|
||||
...attributes
|
||||
>
|
||||
{{#if (has-block "trigger")}}
|
||||
|
@ -122,6 +148,7 @@ export default class DMenu extends Component {
|
|||
@innerClass="fk-d-menu__inner-content"
|
||||
@role="dialog"
|
||||
@inline={{this.options.inline}}
|
||||
{{didInsert this.registerFloatBody}}
|
||||
>
|
||||
{{#if (has-block)}}
|
||||
{{yield this.componentArgs}}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.96.1"
|
||||
"webpack": "^5.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -1104,6 +1104,7 @@ a.inline-editable-field {
|
|||
@import "common/admin/site-settings";
|
||||
@import "common/admin/admin_config_area";
|
||||
@import "common/admin/admin_table";
|
||||
@import "common/admin/admin_filter";
|
||||
@import "common/admin/admin_reports";
|
||||
@import "common/admin/admin_report";
|
||||
@import "common/admin/admin_report_counters";
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.d-admin-filter {
|
||||
background-color: var(--primary-very-low);
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.admin-users-list__search {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
.admin-filter__input {
|
||||
width: 100%;
|
||||
}
|
|
@ -8,6 +8,13 @@ em > code {
|
|||
color: var(--primary);
|
||||
}
|
||||
|
||||
a > code {
|
||||
padding: 2px 4px;
|
||||
background: var(--inline-code-bg);
|
||||
white-space: pre-wrap;
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--primary-very-high);
|
||||
background: var(--hljs-bg);
|
||||
|
|
|
@ -257,6 +257,24 @@
|
|||
|
||||
.names {
|
||||
margin-right: auto;
|
||||
|
||||
.first {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-badge-buttons {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
a {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.post-infos {
|
||||
|
@ -265,8 +283,9 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.user-status-message {
|
||||
.user-status-message-wrap {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
img.emoji {
|
||||
width: 1em;
|
||||
|
|
|
@ -249,9 +249,6 @@
|
|||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
margin: 0 0.5em 0 0;
|
||||
}
|
||||
.topic-statuses {
|
||||
line-height: 1.2;
|
||||
.d-icon {
|
||||
|
@ -268,7 +265,9 @@
|
|||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
@media screen and (min-width: 925px) {
|
||||
width: 90%; // topic title isn't full-width on wide screens
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
|
@ -282,36 +281,66 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
box-sizing: border-box;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
max-width: calc(
|
||||
var(--topic-body-width) + (var(--topic-body-width-padding) * 2) +
|
||||
var(--topic-avatar-width)
|
||||
);
|
||||
#edit-title {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.category-chooser,
|
||||
.mini-tag-chooser {
|
||||
flex: 1 1 35%;
|
||||
margin: 0 0 9px 0;
|
||||
@media all and (max-width: 500px) {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
.mini-tag-chooser {
|
||||
flex: 1 1 54%;
|
||||
margin: 0 0 9px 0;
|
||||
margin-left: 1%; // category at 40%, tag chooser at 58%
|
||||
@media all and (max-width: 500px) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.edit-controls {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.select-kit .category-row {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-title__wrapper {
|
||||
flex: 1 1 100%;
|
||||
#edit-title {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-category__wrapper {
|
||||
flex: 1 1 5%;
|
||||
@include breakpoint(tablet) {
|
||||
min-width: 0; // allows category name to shrink to fit narrow screens
|
||||
}
|
||||
.select-kit.combo-box.category-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-tags__wrapper {
|
||||
flex: 1 1 33%;
|
||||
@include breakpoint(tablet) {
|
||||
flex: 1 1 100%; // force full row on narrow screens
|
||||
}
|
||||
|
||||
.mini-tag-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
.select-kit-header--filter {
|
||||
flex-wrap: nowrap; // forces the whole input to wrap if needed, rather than individual tags
|
||||
min-width: 0;
|
||||
@include breakpoint(tablet) {
|
||||
flex-wrap: wrap; // individual tags will need to wrap on narrow screens
|
||||
}
|
||||
button {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-select-filter {
|
||||
flex-shrink: 0;
|
||||
min-width: 2em; // always provide a minimal space for input
|
||||
}
|
||||
}
|
||||
|
||||
.edit-controls {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.private-message-glyph {
|
||||
|
|
|
@ -43,6 +43,7 @@ body:not(.archetype-private_message) {
|
|||
var(--topic-avatar-width) + var(--topic-body-width) +
|
||||
(var(--topic-body-width-padding) * 2)
|
||||
);
|
||||
padding-block: 0.5em;
|
||||
|
||||
@include breakpoint(mobile-large) {
|
||||
font-size: var(--font-down-1);
|
||||
|
@ -91,7 +92,6 @@ body:not(.archetype-private_message) {
|
|||
}
|
||||
|
||||
&__contents {
|
||||
padding-block: 0.5em;
|
||||
flex-grow: 1;
|
||||
|
||||
.number {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
html.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.d-modal {
|
||||
--modal-max-width: 600px;
|
||||
--modal-width: 30em; // set in ems to scale with user font-size
|
||||
|
|
|
@ -18,11 +18,6 @@
|
|||
z-index: z("base");
|
||||
margin-bottom: 1em;
|
||||
|
||||
#edit-title,
|
||||
.category-chooser,
|
||||
.edit-controls {
|
||||
width: 500px;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--font-up-4);
|
||||
line-height: var(--line-height-medium);
|
||||
|
|
|
@ -1,8 +1,24 @@
|
|||
// Shared styles
|
||||
|
||||
.login-page,
|
||||
.signup-page,
|
||||
.invite-page {
|
||||
#main-outlet,
|
||||
#main-outlet-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-fullpage,
|
||||
.signup-fullpage,
|
||||
.invites-show {
|
||||
justify-content: flex-start;
|
||||
|
||||
html.keyboard-visible:not(.ios-device) & {
|
||||
height: calc((var(--composer-vh, 1vh) * 100) - var(--header-offset));
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.signup-body,
|
||||
.login-body {
|
||||
flex-direction: column;
|
||||
|
|
|
@ -49,6 +49,10 @@ class Admin::SiteSettingsController < Admin::AdminController
|
|||
on_failed_policy(:setting_is_visible) do
|
||||
raise Discourse::InvalidParameters, I18n.t("errors.site_settings.site_setting_is_hidden")
|
||||
end
|
||||
on_failed_policy(:setting_is_shadowed_globally) do
|
||||
raise Discourse::InvalidParameters,
|
||||
I18n.t("errors.site_settings.site_setting_is_shadowed_globally")
|
||||
end
|
||||
on_failed_policy(:setting_is_configurable) do
|
||||
raise Discourse::InvalidParameters,
|
||||
I18n.t("errors.site_settings.site_setting_is_unconfigurable")
|
||||
|
|
|
@ -96,16 +96,26 @@ class DraftsController < ApplicationController
|
|||
|
||||
json = success_json.merge(draft_sequence: sequence)
|
||||
|
||||
if data.present?
|
||||
# this is a bit of a kludge we need to remove (all the parsing) too many special cases here
|
||||
# we need to catch action edit and action editSharedDraft
|
||||
if data["postId"].present? && data["originalText"].present? &&
|
||||
data["action"].to_s.start_with?("edit")
|
||||
post = Post.find_by(id: data["postId"])
|
||||
if post && post.raw != data["originalText"]
|
||||
conflict_user = BasicUserSerializer.new(post.last_editor, root: false)
|
||||
render json: json.merge(conflict_user: conflict_user)
|
||||
return
|
||||
# check for conflicts when editing a post
|
||||
if data.present? && data["postId"].present? && data["action"].to_s.start_with?("edit")
|
||||
original_text = data["original_text"] || data["originalText"]
|
||||
original_title = data["original_title"]
|
||||
original_tags = data["original_tags"]
|
||||
|
||||
if original_text.present?
|
||||
if post = Post.find_by(id: data["postId"])
|
||||
conflict = original_text != post.raw
|
||||
|
||||
if post.post_number == 1
|
||||
conflict ||= original_title.present? && original_title != post.topic.title
|
||||
conflict ||=
|
||||
original_tags.present? && original_tags.sort != post.topic.tags.pluck(:name).sort
|
||||
end
|
||||
|
||||
if conflict
|
||||
conflict_user = BasicUserSerializer.new(post.last_editor, root: false)
|
||||
json.merge!(conflict_user:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -240,8 +240,9 @@ class PostsController < ApplicationController
|
|||
|
||||
Post.plugin_permitted_update_params.keys.each { |param| changes[param] = params[:post][param] }
|
||||
|
||||
raw_old = params[:post][:raw_old]
|
||||
if raw_old.present? && raw_old != post.raw
|
||||
# keep `raw_old` for backwards compatibility
|
||||
original_text = params[:post][:original_text] || params[:post][:raw_old]
|
||||
if original_text.present? && original_text != post.raw
|
||||
return render_json_error(I18n.t("edit_conflict"), status: 409)
|
||||
end
|
||||
|
||||
|
|
|
@ -359,8 +359,19 @@ class TopicsController < ApplicationController
|
|||
|
||||
def update
|
||||
topic = Topic.find_by(id: params[:topic_id])
|
||||
|
||||
guardian.ensure_can_edit!(topic)
|
||||
|
||||
original_title = params[:original_title]
|
||||
if original_title.present? && original_title != topic.title
|
||||
return render_json_error(I18n.t("edit_conflict"), status: 409)
|
||||
end
|
||||
|
||||
original_tags = params[:original_tags]
|
||||
if original_tags.present? && original_tags.sort != topic.tags.pluck(:name).sort
|
||||
return render_json_error(I18n.t("edit_conflict"), status: 409)
|
||||
end
|
||||
|
||||
if params[:category_id] && (params[:category_id].to_i != topic.category_id.to_i)
|
||||
if topic.shared_draft
|
||||
topic.shared_draft.update(category_id: params[:category_id])
|
||||
|
|
|
@ -344,27 +344,28 @@ end
|
|||
#
|
||||
# Table name: badges
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# description :text
|
||||
# badge_type_id :integer not null
|
||||
# grant_count :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# allow_title :boolean default(FALSE), not null
|
||||
# multiple_grant :boolean default(FALSE), not null
|
||||
# icon :string default("fa-certificate")
|
||||
# listable :boolean default(TRUE)
|
||||
# target_posts :boolean default(FALSE)
|
||||
# query :text
|
||||
# enabled :boolean default(TRUE), not null
|
||||
# auto_revoke :boolean default(TRUE), not null
|
||||
# badge_grouping_id :integer default(5), not null
|
||||
# trigger :integer
|
||||
# show_posts :boolean default(FALSE), not null
|
||||
# system :boolean default(FALSE), not null
|
||||
# long_description :text
|
||||
# image_upload_id :integer
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# description :text
|
||||
# badge_type_id :integer not null
|
||||
# grant_count :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# allow_title :boolean default(FALSE), not null
|
||||
# multiple_grant :boolean default(FALSE), not null
|
||||
# icon :string default("fa-certificate")
|
||||
# listable :boolean default(TRUE)
|
||||
# target_posts :boolean default(FALSE)
|
||||
# query :text
|
||||
# enabled :boolean default(TRUE), not null
|
||||
# auto_revoke :boolean default(TRUE), not null
|
||||
# badge_grouping_id :integer default(5), not null
|
||||
# trigger :integer
|
||||
# show_posts :boolean default(FALSE), not null
|
||||
# system :boolean default(FALSE), not null
|
||||
# show_in_post_header :boolean default(FALSE), not null
|
||||
# long_description :text
|
||||
# image_upload_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -327,6 +327,6 @@ class Emoji
|
|||
end
|
||||
|
||||
def self.sanitize_emoji_name(name)
|
||||
name.gsub(/[^a-z0-9]+/i, "_").gsub(/_{2,}/, "_").downcase
|
||||
name.gsub(/[^a-z0-9\+\-]+/i, "_").gsub(/_{2,}/, "_").downcase
|
||||
end
|
||||
end
|
||||
|
|
|
@ -658,6 +658,7 @@ class Post < ActiveRecord::Base
|
|||
"flag_reasons.#{post_action_type_view.types[post_action_type_id]}",
|
||||
locale: SiteSetting.default_locale,
|
||||
base_path: Discourse.base_path,
|
||||
default: PostActionType.names[post_action_type_id],
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -113,6 +113,11 @@ class PostMover
|
|||
close_topic_and_schedule_deletion if moving_all_posts
|
||||
|
||||
destination_topic.reload
|
||||
DiscourseEvent.trigger(
|
||||
:posts_moved,
|
||||
destination_topic_id: destination_topic.id,
|
||||
original_topic_id: original_topic.id,
|
||||
)
|
||||
destination_topic
|
||||
end
|
||||
|
||||
|
@ -299,6 +304,7 @@ class PostMover
|
|||
end
|
||||
|
||||
moved_post.attributes = update
|
||||
moved_post.disable_rate_limits! if @options[:freeze_original]
|
||||
moved_post.save(validate: false)
|
||||
|
||||
DiscourseEvent.trigger(:post_moved, moved_post, original_topic.id)
|
||||
|
|
|
@ -35,6 +35,19 @@ class UserBadge < ActiveRecord::Base
|
|||
scope :for_enabled_badges,
|
||||
-> { where("user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)") }
|
||||
|
||||
scope :by_post_and_user,
|
||||
->(posts) do
|
||||
posts.reduce(UserBadge.none) do |scope, post|
|
||||
scope.or(UserBadge.where(user_id: post.user_id, post_id: post.id))
|
||||
end
|
||||
end
|
||||
scope :for_post_header_badges,
|
||||
->(posts) do
|
||||
by_post_and_user(posts).where(
|
||||
"user_badges.badge_id IN (SELECT id FROM badges WHERE show_posts AND enabled AND listable AND show_in_post_header)",
|
||||
)
|
||||
end
|
||||
|
||||
validates :badge_id, presence: true, uniqueness: { scope: :user_id }, if: :single_grant_badge?
|
||||
|
||||
validates :user_id, presence: true
|
||||
|
|
|
@ -16,7 +16,8 @@ class BadgeSerializer < ApplicationSerializer
|
|||
:long_description,
|
||||
:slug,
|
||||
:has_badge,
|
||||
:manually_grantable?
|
||||
:manually_grantable?,
|
||||
:show_in_post_header
|
||||
|
||||
has_one :badge_type
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ class PostSerializer < BasicPostSerializer
|
|||
:flair_bg_color,
|
||||
:flair_color,
|
||||
:flair_group_id,
|
||||
:badges_granted,
|
||||
:version,
|
||||
:can_edit,
|
||||
:can_delete,
|
||||
|
@ -223,6 +224,18 @@ class PostSerializer < BasicPostSerializer
|
|||
object.user&.flair_group_id
|
||||
end
|
||||
|
||||
def badges_granted
|
||||
return [] unless SiteSetting.enable_badges && SiteSetting.show_badges_in_post_header
|
||||
|
||||
if @topic_view
|
||||
user_badges = @topic_view.post_user_badges[object.id] || []
|
||||
else
|
||||
user_badges = UserBadge.for_post_header_badges([object])
|
||||
end
|
||||
|
||||
user_badges.map { |user_badge| BasicUserBadgeSerializer.new(user_badge, scope: scope).as_json }
|
||||
end
|
||||
|
||||
def link_counts
|
||||
return @single_post_link_counts if @single_post_link_counts.present?
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ class WebHookPostSerializer < PostSerializer
|
|||
flair_color
|
||||
notice
|
||||
mentioned_users
|
||||
badges_granted
|
||||
].each { |attr| define_method("include_#{attr}?") { false } }
|
||||
|
||||
def topic_posts
|
||||
|
|
|
@ -4,12 +4,15 @@ class AdminNotices::Dismiss
|
|||
include Service::Base
|
||||
|
||||
policy :invalid_access
|
||||
|
||||
params do
|
||||
attribute :id, :integer
|
||||
|
||||
validates :id, presence: true
|
||||
end
|
||||
|
||||
model :admin_notice, optional: true
|
||||
|
||||
transaction do
|
||||
step :destroy
|
||||
step :reset_problem_check
|
||||
|
|
|
@ -4,10 +4,12 @@ class Experiments::Toggle
|
|||
include Service::Base
|
||||
|
||||
policy :current_user_is_admin
|
||||
|
||||
params do
|
||||
attribute :setting_name, :string
|
||||
validates :setting_name, presence: true
|
||||
end
|
||||
|
||||
policy :setting_is_available
|
||||
transaction { step :toggle }
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ class Flags::CreateFlag
|
|||
include Service::Base
|
||||
|
||||
policy :invalid_access
|
||||
|
||||
params do
|
||||
attribute :name, :string
|
||||
attribute :description, :string
|
||||
|
@ -18,8 +19,10 @@ class Flags::CreateFlag
|
|||
validates :description, length: { maximum: Flag::MAX_DESCRIPTION_LENGTH }
|
||||
validates :applies_to, inclusion: { in: -> { Flag.valid_applies_to_types } }, allow_nil: false
|
||||
end
|
||||
|
||||
policy :unique_name
|
||||
model :flag, :instantiate_flag
|
||||
|
||||
transaction do
|
||||
step :create
|
||||
step :log
|
||||
|
|
|
@ -8,10 +8,12 @@ class Flags::DestroyFlag
|
|||
|
||||
validates :id, presence: true
|
||||
end
|
||||
|
||||
model :flag
|
||||
policy :not_system
|
||||
policy :not_used
|
||||
policy :invalid_access
|
||||
|
||||
transaction do
|
||||
step :destroy
|
||||
step :log
|
||||
|
|
|
@ -10,10 +10,12 @@ class Flags::ReorderFlag
|
|||
validates :flag_id, presence: true
|
||||
validates :direction, inclusion: { in: %w[up down] }
|
||||
end
|
||||
|
||||
model :flag
|
||||
policy :invalid_access
|
||||
model :all_flags
|
||||
policy :invalid_move
|
||||
|
||||
transaction do
|
||||
step :move
|
||||
step :log
|
||||
|
|
|
@ -4,12 +4,15 @@ class Flags::ToggleFlag
|
|||
include Service::Base
|
||||
|
||||
policy :invalid_access
|
||||
|
||||
params do
|
||||
attribute :flag_id, :integer
|
||||
|
||||
validates :flag_id, presence: true
|
||||
end
|
||||
|
||||
model :flag
|
||||
|
||||
transaction do
|
||||
step :toggle
|
||||
step :log
|
||||
|
|
|
@ -19,11 +19,13 @@ class Flags::UpdateFlag
|
|||
validates :description, length: { maximum: Flag::MAX_DESCRIPTION_LENGTH }
|
||||
validates :applies_to, inclusion: { in: -> { Flag.valid_applies_to_types } }, allow_nil: false
|
||||
end
|
||||
|
||||
model :flag
|
||||
policy :not_system
|
||||
policy :not_used
|
||||
policy :invalid_access
|
||||
policy :unique_name
|
||||
|
||||
transaction do
|
||||
step :update
|
||||
step :log
|
||||
|
|
|
@ -6,6 +6,7 @@ class SiteSetting::Update
|
|||
options { attribute :allow_changing_hidden, :boolean, default: false }
|
||||
|
||||
policy :current_user_is_admin
|
||||
|
||||
params do
|
||||
attribute :setting_name
|
||||
attribute :new_value
|
||||
|
@ -34,6 +35,8 @@ class SiteSetting::Update
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
policy :setting_is_shadowed_globally
|
||||
policy :setting_is_visible
|
||||
policy :setting_is_configurable
|
||||
step :save
|
||||
|
@ -44,6 +47,10 @@ class SiteSetting::Update
|
|||
guardian.is_admin?
|
||||
end
|
||||
|
||||
def setting_is_shadowed_globally(params:)
|
||||
!SiteSetting.shadowed_settings.include?(params.setting_name)
|
||||
end
|
||||
|
||||
def setting_is_visible(params:, options:)
|
||||
options.allow_changing_hidden || !SiteSetting.hidden_settings.include?(params.setting_name)
|
||||
end
|
||||
|
|
|
@ -19,6 +19,7 @@ class User::Silence
|
|||
validates :other_user_ids, length: { maximum: User::MAX_SIMILAR_USERS }
|
||||
validates :post_action, inclusion: { in: %w[delete delete_replies edit] }, allow_blank: true
|
||||
end
|
||||
|
||||
model :user
|
||||
policy :not_silenced_already, class_name: User::Policy::NotAlreadySilenced
|
||||
model :users
|
||||
|
|
|
@ -19,6 +19,7 @@ class User::Suspend
|
|||
validates :other_user_ids, length: { maximum: User::MAX_SIMILAR_USERS }
|
||||
validates :post_action, inclusion: { in: %w[delete delete_replies edit] }, allow_blank: true
|
||||
end
|
||||
|
||||
model :user
|
||||
policy :not_suspended_already, class_name: User::Policy::NotAlreadySuspended
|
||||
model :users
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<%- if can_sign_up? %>
|
||||
<a href="<%= path "/signup"%>" class='btn btn-primary btn-small sign-up-button'><%= I18n.t('sign_up') %></a>
|
||||
<%- end %>
|
||||
<a href="<%= path "/login"%>" class='btn btn-primary btn-small login-button btn-icon-text'><%= SvgSprite.raw_svg('fa-user') %><%= I18n.t('log_in') %></a>
|
||||
<a href="<%= path "/login"%>" class='btn btn-primary btn-small login-button btn-icon-text'><%= SvgSprite.raw_svg('user') %><%= I18n.t('log_in') %></a>
|
||||
</span>
|
||||
<%- end %>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<h1 class="title"><%= @title %></h1>
|
||||
|
||||
<%- if !@current_user %>
|
||||
<a href="<%= path "/login" %>" class='btn btn-primary'><%= SvgSprite.raw_svg('fa-user') %><%= I18n.t('log_in') %></a>
|
||||
<a href="<%= path "/login" %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user') %><%= I18n.t('log_in') %></a>
|
||||
<%- end %>
|
||||
|
||||
<%- if @group&.dig(:allow_membership_requests) %>
|
||||
|
|
|
@ -4920,7 +4920,6 @@ ar:
|
|||
with_topics: "موضوعات %{filter}"
|
||||
with_category: "موضوعات %{filter} في %{category}"
|
||||
filter:
|
||||
title: "المرشح"
|
||||
button:
|
||||
label: "المرشح"
|
||||
latest:
|
||||
|
@ -5770,6 +5769,7 @@ ar:
|
|||
all: "الكل"
|
||||
new_features:
|
||||
title: "ما الجديد"
|
||||
check_for_updates: "تحقق من وجود تحديثات"
|
||||
dashboard:
|
||||
title: "لوحة المعلومات"
|
||||
last_updated: "تم تحديث لوحة المعلومات:"
|
||||
|
@ -6409,6 +6409,7 @@ ar:
|
|||
new_theme: "سمة جديدة"
|
||||
user_selectable: "قابلة للاختيار من قِبل المستخدم"
|
||||
user_fields:
|
||||
field: "الحقل"
|
||||
type: "النوع"
|
||||
more_options:
|
||||
title: "المزيد من الخيارات"
|
||||
|
@ -7633,6 +7634,7 @@ ar:
|
|||
confirm_delete: "هل تريد بالتأكيد حذف سجل DiscourseConnect هذا؟"
|
||||
user_fields:
|
||||
title: "حقول المستخدم"
|
||||
add: "أضف حقل المستخدم"
|
||||
untitled: "بلا لقب"
|
||||
name: "اسم الحقل"
|
||||
type: "نوع الحقل"
|
||||
|
@ -7836,6 +7838,7 @@ ar:
|
|||
none_selected: "تحديد شارة للبدء"
|
||||
allow_title: السماح للمستخدمين باستخدام الشارة كعنوان
|
||||
multiple_grant: يمكن منحها عدة مرات
|
||||
visibility_heading: الرؤية
|
||||
listable: عرض الشارة في صفحة الشارات العامة
|
||||
enabled: مفعَّلة
|
||||
disabled: متوقف
|
||||
|
|
|
@ -1279,7 +1279,6 @@ be:
|
|||
categories_list: "Спіс катэгорый"
|
||||
filters:
|
||||
filter:
|
||||
title: "Filter"
|
||||
button:
|
||||
label: "Filter"
|
||||
latest:
|
||||
|
|
|
@ -3104,7 +3104,6 @@ bg:
|
|||
with_topics: "%{filter} теми"
|
||||
with_category: "%{filter} %{category} теми"
|
||||
filter:
|
||||
title: "Филтър"
|
||||
button:
|
||||
label: "Филтър"
|
||||
latest:
|
||||
|
@ -4350,6 +4349,7 @@ bg:
|
|||
none_selected: "Изберете значка за да започнете."
|
||||
allow_title: Позволете значките да бъдат ползвани като титли.
|
||||
multiple_grant: Може да бъде присъдена няколко пъти
|
||||
visibility_heading: Видимост
|
||||
listable: Показвай значката на страницата със значки
|
||||
enabled: да е включен
|
||||
disabled: деактивирани
|
||||
|
|
|
@ -2710,7 +2710,6 @@ bs_BA:
|
|||
with_topics: "%{filter} teme"
|
||||
with_category: "%{filter} %{category} teme"
|
||||
filter:
|
||||
title: "Filter"
|
||||
button:
|
||||
label: "Filter"
|
||||
latest:
|
||||
|
@ -4378,6 +4377,7 @@ bs_BA:
|
|||
none_selected: "Izaberite značku za početak"
|
||||
allow_title: Allow badge to be used as a title
|
||||
multiple_grant: Can be granted multiple times
|
||||
visibility_heading: Vidljivost
|
||||
listable: Show badge on the public badges page
|
||||
enabled: omogućen
|
||||
disabled: deaktiviran
|
||||
|
|
|
@ -2619,7 +2619,6 @@ ca:
|
|||
with_topics: "%{filter} temes"
|
||||
with_category: "%{filter} %{category} temes"
|
||||
filter:
|
||||
title: "Filtre"
|
||||
button:
|
||||
label: "Filtre"
|
||||
latest:
|
||||
|
@ -4236,6 +4235,7 @@ ca:
|
|||
none_selected: "Tria una insígnia per a començar"
|
||||
allow_title: Permet que es faci servir la insígnia com a títol
|
||||
multiple_grant: Pot concedir-se moltes vegades
|
||||
visibility_heading: Visibilitat
|
||||
listable: Mostra la insígnia en la pàgina pública d'insígnies
|
||||
enabled: activat
|
||||
disabled: desactivat
|
||||
|
|
|
@ -4310,7 +4310,6 @@ cs:
|
|||
with_topics: "%{filter} témata"
|
||||
with_category: "%{filter} %{category} témata"
|
||||
filter:
|
||||
title: "Filtr"
|
||||
button:
|
||||
label: "Filtr"
|
||||
latest:
|
||||
|
@ -6540,6 +6539,7 @@ cs:
|
|||
none_selected: "Vyberte odznak, abyste mohli začít"
|
||||
allow_title: Povolit užití odzanku jako titul
|
||||
multiple_grant: Může být přiděleno několikrát
|
||||
visibility_heading: Viditelnost
|
||||
listable: Zobrazit odznak na veřejné stránce s odzanky
|
||||
enabled: zapnuto
|
||||
disabled: vypnuto
|
||||
|
|
|
@ -3310,7 +3310,6 @@ da:
|
|||
with_topics: "%{filter} emner"
|
||||
with_category: "%{filter} %{category} emner"
|
||||
filter:
|
||||
title: "Filter"
|
||||
button:
|
||||
label: "Filter"
|
||||
latest:
|
||||
|
@ -5355,6 +5354,7 @@ da:
|
|||
none_selected: "Vælg et emblem for at komme igang"
|
||||
allow_title: Tillad at bruge dette emblem som titel
|
||||
multiple_grant: Kan tildeles flere gange
|
||||
visibility_heading: Synlighed
|
||||
listable: Vis emblemer på den offentlige emblem side
|
||||
enabled: aktiveret
|
||||
disabled: deaktiveret
|
||||
|
|
|
@ -3546,6 +3546,7 @@ de:
|
|||
other: "Zeige %{count} Antworten an"
|
||||
in_reply_to: "Übergeordneten Beitrag laden"
|
||||
view_all_posts: "Alle Beiträge anzeigen"
|
||||
badge_granted_tooltip: "%{username} hat für diesen Beitrag das Abzeichen „%{badge_name}“ erhalten!"
|
||||
errors:
|
||||
create: "Entschuldige, es gab einen Fehler beim Anlegen des Beitrags. Bitte versuche es noch einmal."
|
||||
edit: "Entschuldige, es gab einen Fehler beim Bearbeiten des Beitrags. Bitte versuche es noch einmal."
|
||||
|
@ -4049,7 +4050,7 @@ de:
|
|||
with_topics: "%{filter}"
|
||||
with_category: "%{filter} in %{category}"
|
||||
filter:
|
||||
title: "Filter"
|
||||
title: "Gefilterte Ergebnisse für %{filter}"
|
||||
button:
|
||||
label: "Filter"
|
||||
latest:
|
||||
|
@ -4771,6 +4772,7 @@ de:
|
|||
all: "Gesamt"
|
||||
new_features:
|
||||
title: "Was ist neu?"
|
||||
check_for_updates: "Nach Aktualisierungen suchen"
|
||||
dashboard:
|
||||
title: "Dashboard"
|
||||
last_updated: "Dashboard aktualisiert:"
|
||||
|
@ -6265,6 +6267,7 @@ de:
|
|||
invalid: "Entschuldige, du darfst nicht in die Rolle dieses Benutzers schlüpfen."
|
||||
users:
|
||||
title: "Benutzer"
|
||||
description: "Benutzer anzeigen und verwalten."
|
||||
create: "Administrator hinzufügen"
|
||||
last_emailed: "Letzte E-Mail"
|
||||
not_found: "Entschuldige, dieser Benutzername ist im System nicht vorhanden."
|
||||
|
@ -6785,9 +6788,13 @@ de:
|
|||
no_user_badges: "%{name} wurden keine Abzeichen verliehen."
|
||||
no_badges: Es gibt keine Abzeichen, die verliehen werden können.
|
||||
none_selected: "Wähle ein Abzeichen aus, um loszulegen"
|
||||
usage_heading: Verwendung
|
||||
allow_title: Abzeichen darf als Titel verwendet werden
|
||||
multiple_grant: Kann mehrfach verliehen werden
|
||||
visibility_heading: Sichtbarkeit
|
||||
listable: Zeige Abzeichen auf der öffentlichen Abzeichenseite an
|
||||
show_in_post_header: Abzeichen auf dem Beitrag anzeigen, für den es gewährt wurde
|
||||
show_in_post_header_disabled: Erfordert, dass sowohl "Abzeichen auf der Seite mit den öffentlichen Abzeichen anzeigen" als auch "Abzeichen für Beiträge auf der Seite mit den Abzeichen anzeigen" aktiviert sind.
|
||||
enabled: aktiviert
|
||||
disabled: deaktiviert
|
||||
icon: Symbol
|
||||
|
|
|
@ -3319,7 +3319,6 @@ el:
|
|||
with_topics: "%{filter} θέματα"
|
||||
with_category: "%{filter} %{category} θέματα"
|
||||
filter:
|
||||
title: "Φίλτρο"
|
||||
button:
|
||||
label: "Φίλτρο"
|
||||
latest:
|
||||
|
@ -5228,6 +5227,7 @@ el:
|
|||
none_selected: "Διάλεξε ένα παράσημο για να ξεκινήσεις"
|
||||
allow_title: Το παράσημο μπορεί να χρησιμοποιηθεί σαν τίτλος
|
||||
multiple_grant: Μπορεί να απονεμηθεί πολλές φορές
|
||||
visibility_heading: Ορατότητα
|
||||
listable: Εμφάνιση του παράσημου στη δημόσια σελίδα παρασήμων
|
||||
enabled: ενεργοποιημένα
|
||||
disabled: απενεργοποιημένα
|
||||
|
|
|
@ -3816,6 +3816,8 @@ en:
|
|||
in_reply_to: "Load parent post"
|
||||
view_all_posts: "View all posts"
|
||||
|
||||
badge_granted_tooltip: "%{username} earned the '%{badge_name}' badge for this post!"
|
||||
|
||||
errors:
|
||||
create: "Sorry, there was an error creating your post. Please try again."
|
||||
edit: "Sorry, there was an error editing your post. Please try again."
|
||||
|
@ -7195,9 +7197,13 @@ en:
|
|||
no_user_badges: "%{name} has not been granted any badges."
|
||||
no_badges: There are no badges that can be granted.
|
||||
none_selected: "Select a badge to get started"
|
||||
usage_heading: Usage
|
||||
allow_title: Allow badge to be used as a title
|
||||
multiple_grant: Can be granted multiple times
|
||||
visibility_heading: Visibility
|
||||
listable: Show badge on the public badges page
|
||||
show_in_post_header: Show badge on the post it was granted for
|
||||
show_in_post_header_disabled: Requires both 'Show badge on the public badges page' and 'Show post granting badge on badge page' to be enabled.
|
||||
enabled: enabled
|
||||
disabled: disabled
|
||||
icon: Icon
|
||||
|
|
|
@ -1492,12 +1492,12 @@ es:
|
|||
download_backup_codes: "Descargar códigos de recuperación"
|
||||
remaining_codes:
|
||||
one: "Te queda <strong>%{count}</strong> código de copia de seguridad sin usar todavía."
|
||||
other: "Te quedan <strong>%{count}</strong> códigos de respaldo sin usar todavía."
|
||||
use: "Usar un código de copia de seguridad"
|
||||
enable_prerequisites: "Debes activar un método principal de dos factores antes de generar códigos de respaldo."
|
||||
other: "Te quedan <strong>%{count}</strong> códigos de copia de seguridad sin usar todavía."
|
||||
use: "Usar un código de recuperación"
|
||||
enable_prerequisites: "Debes activar un método principal de dos factores antes de generar código de copia de seguridad."
|
||||
codes:
|
||||
title: "Códigos de respaldo generados"
|
||||
description: "Cada uno de estos códigos de respaldo se puede usar una única vez. Mantenlos en un lugar seguro pero accesible."
|
||||
title: "Códigos de copia de seguridad generados"
|
||||
description: "Cada uno de estos códigos de copia de seguridad se puede usar una única vez. Mantenlos en un lugar seguro pero accesible."
|
||||
second_factor:
|
||||
title: "Autenticación de dos factores"
|
||||
enable: "Gestionar la autenticación de dos factores"
|
||||
|
@ -1514,7 +1514,7 @@ es:
|
|||
extended_description: |
|
||||
La autenticación de dos factores añade una capa extra de seguridad a tu cuenta al pedirte un código de un solo uso además de tu contraseña. Los códigos se pueden generar en dispositivos <a href="https://www.google.com/search?q=authenticator+apps+for+android" target='_blank'>Android</a> e <a href="https://www.google.com/search?q=authenticator+apps+for+ios">iOS</a>.
|
||||
oauth_enabled_warning: "Ten en cuenta que los inicios de sesión con redes sociales y páginas de terceros se desactivarán para tu cuenta una vez que actives la autenticación de dos factores."
|
||||
use: "Utilizar la aplicación de autenticación"
|
||||
use: "Usar la aplicación de autenticación"
|
||||
enforced_with_oauth_notice: "Debes activar la autenticación de dos factores. Solo se te pedirá que la utilices cuando inicies sesión con una contraseña, no con métodos de autenticación externa o de inicio de sesión con red social."
|
||||
enforced_notice: "Es obligatorio que actives la autenticación de dos factores antes de acceder al sitio web."
|
||||
disable: "Desactivar"
|
||||
|
@ -1655,8 +1655,8 @@ es:
|
|||
confirm_modal_title: "Conectar cuenta de %{provider}"
|
||||
confirm_description:
|
||||
disconnect: "Tu cuenta de %{provider} «%{account_description}» será desenlazada"
|
||||
account_specific: "Tu cuenta de %{provider} «%{account_description}» se utilizará para la autenticación."
|
||||
generic: "Tu cuenta de %{provider} se utilizará para la autenticación."
|
||||
account_specific: "Tu cuenta de %{provider} «%{account_description}» se usará para la autenticación."
|
||||
generic: "Tu cuenta de %{provider} se usará para la autenticación."
|
||||
activate_account:
|
||||
action: "Haz clic aquí para activar tu cuenta"
|
||||
already_done: "Lo sentimos, este enlace de confirmación de cuenta ya no es válido. ¿Quizás tu cuenta ya está activa?"
|
||||
|
@ -1698,7 +1698,7 @@ es:
|
|||
title: "Código de invitación"
|
||||
instructions: "El registro de la cuenta requiere un código de invitación"
|
||||
auth_tokens:
|
||||
title: "Dispositivos utilizados recientemente"
|
||||
title: "Dispositivos usados recientemente"
|
||||
short_description: "Esta es una lista de los dispositivos que se han conectado recientemente a tu cuenta."
|
||||
details: "Detalles"
|
||||
log_out_all: "Cerrar sesión en todos los dispositivos"
|
||||
|
@ -2235,7 +2235,7 @@ es:
|
|||
second_factor_description: "Introduce el código de autenticación desde tu aplicación:"
|
||||
second_factor_backup: "Iniciar sesión utilizando un código de copia de seguridad"
|
||||
second_factor_backup_title: "Código de respaldo de la autenticación de dos factores"
|
||||
second_factor_backup_description: "Introduce uno de los códigos de respaldo:"
|
||||
second_factor_backup_description: "Introduce uno de los códigos de copia de seguridad:"
|
||||
second_factor: "Iniciar sesión utilizando la aplicación de autenticación"
|
||||
security_key_description: "Cuando tengas tu clave de seguridad física o tu dispositivo móvil compatible preparado, presiona el botón de autenticar con clave de seguridad que se encuentra debajo."
|
||||
security_key_alternative: "Probar de otra manera"
|
||||
|
@ -2794,13 +2794,13 @@ es:
|
|||
aria_label: Filtrar por etiqueta
|
||||
filters:
|
||||
label: Solo temas/publicaciones que…
|
||||
title: coincide el título únicamente
|
||||
title: Coincide solo en el título
|
||||
likes: me han gustado
|
||||
posted: he publicado en ellos
|
||||
created: Creado por mi
|
||||
watching: estoy vigilando
|
||||
tracking: estoy siguiendo
|
||||
private: en mis mensajes
|
||||
private: En mis mensajes
|
||||
bookmarks: he guardado
|
||||
first: son la primera publicación
|
||||
pinned: están anclados
|
||||
|
@ -4047,7 +4047,6 @@ es:
|
|||
with_topics: "%{filter} temas"
|
||||
with_category: "%{filter} Foro de %{category}"
|
||||
filter:
|
||||
title: "Filtrar"
|
||||
button:
|
||||
label: "Filtrar"
|
||||
latest:
|
||||
|
@ -4326,8 +4325,8 @@ es:
|
|||
one: 'Esta etiqueta pertence al grupo: «%{tag_groups}»'
|
||||
other: "Esta etiqueta pertence a estos grupos: %{tag_groups}."
|
||||
category_restrictions:
|
||||
one: "Solo se puede utilizar en esta categoría:"
|
||||
other: "Solo se puede utilizar en estas categorías:"
|
||||
one: "Solo se puede usar en esta categoría:"
|
||||
other: "Solo se puede usar en estas categorías:"
|
||||
edit_synonyms: "Editar sinónimos"
|
||||
add_synonyms_label: "Añadir sinónimos:"
|
||||
add_synonyms: "Añadir"
|
||||
|
@ -5151,7 +5150,7 @@ es:
|
|||
reviewable_created: "El elemento revisable está listo"
|
||||
reviewable_updated: "Se ha actualizado el elemento revisable"
|
||||
user_badge_event:
|
||||
group_name: "Eventos de Medalla"
|
||||
group_name: "Eventos de medalla"
|
||||
user_badge_granted: "Se ha concedido la medalla de usuario"
|
||||
user_badge_revoked: "Se ha revocado la medalla de usuario"
|
||||
like_event:
|
||||
|
@ -6417,7 +6416,7 @@ es:
|
|||
|
||||
<p><b>¡Esto no se puede deshacer!</b></p>
|
||||
|
||||
<p>Para continuar escribe: <code>%{text}</code></p>
|
||||
<p>Para continuar, escribe: <code>%{text}</code></p>
|
||||
text: "eliminar publicaciones de @%{username}"
|
||||
delete: "Eliminar publicaciones de @%{username}"
|
||||
cancel: "Cancelar"
|
||||
|
@ -6742,10 +6741,11 @@ es:
|
|||
granted_badges: Medallas concedidas
|
||||
grant: Conceder
|
||||
no_user_badges: "%{name} no ha recibido ninguna medalla."
|
||||
no_badges: No hay medallas que puedan ser concedidas.
|
||||
no_badges: No hay medallas que se puedan conceder.
|
||||
none_selected: "Selecciona una medalla para empezar"
|
||||
allow_title: Permitir que se use la medalla como título
|
||||
multiple_grant: Se puede conceder varias veces
|
||||
visibility_heading: Visibilidad
|
||||
listable: Mostrar medalla en la página pública de medallas
|
||||
enabled: activado
|
||||
disabled: desactivado
|
||||
|
@ -6788,8 +6788,8 @@ es:
|
|||
with_post_time: <span class="username">%{username}</span> por la publicación en %{link} a las <span class="time">%{time}</span>
|
||||
with_time: <span class="username">%{username}</span> a las <span class="time">%{time}</span>
|
||||
badge_intro:
|
||||
title: "Elige una insignia o crea una nueva"
|
||||
description: "Empieza seleccionando una insignia existente para personalizarla, o crea una insignia totalmente nueva"
|
||||
title: "Elige una medalla o crea una nueva"
|
||||
description: "Empieza seleccionando una medalla existente para personalizarla, o crea una medalla totalmente nueva"
|
||||
mass_award:
|
||||
title: Conceder en masa
|
||||
description: Concede la misma medalla a muchos usuarios a la vez.
|
||||
|
|
|
@ -2362,7 +2362,6 @@ et:
|
|||
with_topics: "%{filter} teemat"
|
||||
with_category: "%{filter} %{category} teemat"
|
||||
filter:
|
||||
title: "Filter"
|
||||
button:
|
||||
label: "Filter"
|
||||
latest:
|
||||
|
@ -3685,6 +3684,7 @@ et:
|
|||
none_selected: "Vali märgis, et alustada"
|
||||
allow_title: Luba märgise kasutamist tiitlina
|
||||
multiple_grant: Võib olla määratud mitmeid kordi
|
||||
visibility_heading: Nähtavus
|
||||
listable: Näita märgist avalike märgiste lehel
|
||||
enabled: sisse lülitatud
|
||||
disabled: välja lülitatud
|
||||
|
|
|
@ -753,12 +753,17 @@ fa_IR:
|
|||
prefill:
|
||||
title: "از قبل پر کردن با تنظیمات برای:"
|
||||
gmail: "جیمیل"
|
||||
outlook: "Outlook.com"
|
||||
office365: "مایکروسافت ۳۶۵"
|
||||
ssl_modes:
|
||||
none: "هیچ کدام"
|
||||
ssl_tls: "SSL/TLS"
|
||||
starttls: "STARTTLS"
|
||||
credentials:
|
||||
title: "اطلاعات ورود"
|
||||
smtp_server: "سرور SMTP"
|
||||
smtp_port: "درگاه SMTP"
|
||||
smtp_ssl_mode: "حالت SSL"
|
||||
imap_server: "سرور IMAP"
|
||||
imap_port: "درگاه IMAP"
|
||||
imap_ssl: "از SSL برای IMAP استفاده کنید"
|
||||
|
@ -3134,7 +3139,6 @@ fa_IR:
|
|||
with_topics: "%{filter} موضوعات"
|
||||
with_category: "%{filter} %{category} موضوعات"
|
||||
filter:
|
||||
title: "فیلتر"
|
||||
button:
|
||||
label: "فیلتر"
|
||||
latest:
|
||||
|
@ -4748,6 +4752,7 @@ fa_IR:
|
|||
none_selected: "برای شروع یک نشان رو انتخاب کنید"
|
||||
allow_title: اجازهی استفاده نشان برای عنوان
|
||||
multiple_grant: نمی توان چندین بار اعطا کرد
|
||||
visibility_heading: قابل مشاهده
|
||||
listable: نشان دادن نشان در صفحه نشانهای عمومی
|
||||
enabled: فعال شده
|
||||
disabled: غیرفعال شده
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue