Merge branch 'main' into feature/wizard-look-and-feel-improvements
This commit is contained in:
commit
19666eaa0a
|
@ -540,10 +540,10 @@ GEM
|
|||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
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)
|
||||
sqlite3 (2.4.1-aarch64-linux-gnu)
|
||||
sqlite3 (2.4.1-arm64-darwin)
|
||||
sqlite3 (2.4.1-x86_64-darwin)
|
||||
sqlite3 (2.4.1-x86_64-linux-gnu)
|
||||
sshkey (3.0.0)
|
||||
stackprof (0.2.26)
|
||||
stringio (3.1.2)
|
||||
|
|
|
@ -205,7 +205,7 @@ export default class AdminFlagsForm extends Component {
|
|||
</checkboxGroup.Field>
|
||||
</form.CheckboxGroup>
|
||||
|
||||
<form.Alert @icon="info-circle">
|
||||
<form.Alert @icon="circle-info">
|
||||
{{i18n "admin.config_areas.flags.form.alert"}}
|
||||
</form.Alert>
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ export default class ThemeCard extends Component {
|
|||
}
|
||||
|
||||
get footerActionIcon() {
|
||||
return this.args.theme.isPendingUpdates ? "sync" : "ellipsis-h";
|
||||
return this.args.theme.isPendingUpdates ? "arrows-rotate" : "ellipsis";
|
||||
}
|
||||
|
||||
// NOTE: inspired by -> https://github.com/discourse/discourse/blob/24caa36eef826bcdaed88aebfa7df154413fb349/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js#L366
|
||||
|
@ -116,7 +116,7 @@ export default class ThemeCard extends Component {
|
|||
<span
|
||||
title={{i18n "admin.customize.theme.updates_available_tooltip"}}
|
||||
class="theme-card__update-available"
|
||||
>{{icon "info-circle"}}</span>
|
||||
>{{icon "circle-info"}}</span>
|
||||
{{/if}}
|
||||
<div class="theme-card__image-wrapper">
|
||||
{{#if @theme.screenshot_url}}
|
||||
|
@ -179,7 +179,7 @@ export default class ThemeCard extends Component {
|
|||
@preventFocus={{true}}
|
||||
@icon={{if
|
||||
@theme.default
|
||||
"far-check-square"
|
||||
"far-square-check"
|
||||
"far-square"
|
||||
}}
|
||||
class="theme-card__button"
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
}}"
|
||||
>
|
||||
{{#if this.versionCheck.behindByOneVersion}}
|
||||
{{d-icon "far-meh"}}
|
||||
{{d-icon "far-face-meh"}}
|
||||
{{else}}
|
||||
{{d-icon "far-face-frown"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
<th>{{i18n "admin.api.user"}}</th>
|
||||
<th>{{i18n "admin.api.created"}}</th>
|
||||
<th>{{i18n "admin.api.last_used"}}</th>
|
||||
<th>{{i18n "admin.site_settings.table_column_heading.status"}}</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |k|}}
|
||||
<tr class="d-admin-row__content {{if k.revoked_at 'revoked'}}">
|
||||
<td class="d-admin-row__overview key">
|
||||
{{#if k.revoked_at}}{{d-icon "circle-xmark"}}{{/if}}
|
||||
{{k.truncatedKey}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail key-description">
|
||||
|
@ -57,6 +57,20 @@
|
|||
{{i18n "admin.api.never_used"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<div class="d-admin-row__mobile-label">{{i18n
|
||||
"admin.site_settings.table_column_heading.status"
|
||||
}}</div>
|
||||
{{#if k.revoked_at}}
|
||||
<div role="status" class="status-label">
|
||||
<div class="status-label-indicator">
|
||||
</div>
|
||||
<div class="status-label-text">
|
||||
{{i18n "admin.api.revoked"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls key-controls">
|
||||
<DButton
|
||||
@action={{route-action "show" k}}
|
||||
|
|
|
@ -166,14 +166,9 @@
|
|||
this.model.attributes.discourse_updated_at
|
||||
leaveAgo="true"
|
||||
}}</p>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={{this.model.attributes.release_notes_link}}
|
||||
class="btn btn-default"
|
||||
>
|
||||
<LinkTo @route="admin.whatsNew" class="btn btn-default">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{{#if this.model.can_view_action_logs}}
|
||||
<DButton
|
||||
@action={{fn this.viewActionLogs this.model.username}}
|
||||
@icon="far-list-alt"
|
||||
@icon="far-rectangle-list"
|
||||
@label="admin.user.action_logs"
|
||||
class="btn-default"
|
||||
/>
|
||||
|
@ -850,7 +850,7 @@
|
|||
{{else}}
|
||||
<DButton
|
||||
@action={{fn this.checkSsoPayload this.model}}
|
||||
@icon="far-list-alt"
|
||||
@icon="far-rectangle-list"
|
||||
@label="admin.users.check_sso.text"
|
||||
@title="admin.users.check_sso.title"
|
||||
class="btn-default"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/users/list"
|
||||
@label={{i18n "admin.permalink.title"}}
|
||||
@label={{i18n "admin.users.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"clean-base-url": "^1.0.0",
|
||||
"express": "^4.21.1",
|
||||
"express": "^4.21.2",
|
||||
"glob": "^10.4.3",
|
||||
"html-entities": "^2.5.2",
|
||||
"html-rewriter-wasm": "^0.4.1",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"@types/jquery": "^3.5.32",
|
||||
"@types/qunit": "^2.19.12",
|
||||
"@types/rsvp": "^4.0.9",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"ember-cli": "~6.0.1",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import Component from "@glimmer/component";
|
||||
import {
|
||||
NO_REMINDER_ICON,
|
||||
WITH_REMINDER_ICON,
|
||||
} from "discourse/models/bookmark";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class BookmarkIcon extends Component {
|
||||
get icon() {
|
||||
if (this.args.bookmark?.get("reminder_at")) {
|
||||
return WITH_REMINDER_ICON;
|
||||
}
|
||||
|
||||
return NO_REMINDER_ICON;
|
||||
}
|
||||
|
||||
get cssClasses() {
|
||||
return this.args.bookmark
|
||||
? "bookmark-icon bookmark-icon__bookmarked"
|
||||
: "bookmark-icon";
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (!this.args.bookmark) {
|
||||
return i18n("bookmarks.create");
|
||||
}
|
||||
|
||||
return this.args.bookmark.get("reminderTitle");
|
||||
}
|
||||
|
||||
<template>
|
||||
{{icon this.icon translatedTitle=this.title class=this.cssClasses}}
|
||||
</template>
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{{d-icon this.icon translatedTitle=this.title class=this.cssClasses}}
|
|
@ -1,42 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { computed } from "@ember/object";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import {
|
||||
NO_REMINDER_ICON,
|
||||
WITH_REMINDER_ICON,
|
||||
} from "discourse/models/bookmark";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class BookmarkIcon extends Component {
|
||||
tagName = "";
|
||||
bookmark = null;
|
||||
|
||||
@computed("bookmark.reminder_at")
|
||||
get icon() {
|
||||
if (!this.bookmark) {
|
||||
return NO_REMINDER_ICON;
|
||||
}
|
||||
|
||||
if (!isEmpty(this.bookmark.reminder_at)) {
|
||||
return WITH_REMINDER_ICON;
|
||||
}
|
||||
|
||||
return NO_REMINDER_ICON;
|
||||
}
|
||||
|
||||
@computed("bookmark")
|
||||
get cssClasses() {
|
||||
return this.bookmark
|
||||
? "bookmark-icon bookmark-icon__bookmarked"
|
||||
: "bookmark-icon";
|
||||
}
|
||||
|
||||
@computed("bookmark.title")
|
||||
get title() {
|
||||
if (!this.bookmark) {
|
||||
return i18n("bookmarks.create");
|
||||
}
|
||||
|
||||
return this.bookmark.reminderTitle;
|
||||
}
|
||||
}
|
|
@ -200,6 +200,7 @@ export default class ComposerEditor extends Component {
|
|||
@bind
|
||||
setupEditor(textManipulation) {
|
||||
this.textManipulation = textManipulation;
|
||||
this.uppyComposerUpload.textManipulation = textManipulation;
|
||||
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
|
||||
|
|
|
@ -306,8 +306,7 @@ export default class DEditor extends Component {
|
|||
this.site.hashtag_configurations["topic-composer"],
|
||||
this.siteSettings,
|
||||
{
|
||||
afterComplete: (value) => {
|
||||
this.set("value", value);
|
||||
afterComplete: () => {
|
||||
schedule(
|
||||
"afterRender",
|
||||
this.textManipulation,
|
||||
|
@ -327,8 +326,7 @@ export default class DEditor extends Component {
|
|||
this.textManipulation.autocomplete({
|
||||
template: findRawTemplate("emoji-selector-autocomplete"),
|
||||
key: ":",
|
||||
afterComplete: (text) => {
|
||||
this.set("value", text);
|
||||
afterComplete: () => {
|
||||
schedule(
|
||||
"afterRender",
|
||||
this.textManipulation,
|
||||
|
@ -466,9 +464,7 @@ export default class DEditor extends Component {
|
|||
onRender: (options) => renderUserStatusHtml(options),
|
||||
key: "@",
|
||||
transformComplete: (v) => v.username || v.name,
|
||||
afterComplete: (value) => {
|
||||
this.set("value", value);
|
||||
|
||||
afterComplete: () => {
|
||||
schedule(
|
||||
"afterRender",
|
||||
this.textManipulation,
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class GroupCardContents extends CardContentsBase.extend(
|
|||
this.setProperties({ group });
|
||||
|
||||
if (!group.flair_url && !group.flair_bg_color) {
|
||||
group.set("flair_url", "fa-users");
|
||||
group.set("flair_url", "users");
|
||||
}
|
||||
|
||||
if (group.can_see_members && group.members.length < maxMembersToDisplay) {
|
||||
|
|
|
@ -131,6 +131,8 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="move-to-topic-after-radio-buttons" />
|
||||
|
||||
{{#if this.existingTopic}}
|
||||
<p>
|
||||
{{html-safe
|
||||
|
|
|
@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
|||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { mergeTopic, movePosts } from "discourse/models/topic";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
@ -145,6 +146,15 @@ export default class MoveToTopic extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
mergeOptions = applyValueTransformer(
|
||||
"move-to-topic-merge-options",
|
||||
mergeOptions
|
||||
);
|
||||
moveOptions = applyValueTransformer(
|
||||
"move-to-topic-move-options",
|
||||
moveOptions
|
||||
);
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (this.args.model.selectedAllPosts) {
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class PostMenuReadButton extends Component {
|
|||
<DButton
|
||||
...attributes
|
||||
@action={{@buttonActions.toggleWhoRead}}
|
||||
@icon="book-reader"
|
||||
@icon="book-open-reader"
|
||||
@title="post.controls.read_indicator"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class PostMenuShowMoreButton extends Component {
|
|||
class="post-action-menu__show-more show-more-actions"
|
||||
...attributes
|
||||
@action={{@buttonActions.showMoreActions}}
|
||||
@icon="ellipsis-h"
|
||||
@icon="ellipsis"
|
||||
@title="show_more"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -185,6 +185,14 @@ export default class Item extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
get useMobileLayout() {
|
||||
return applyValueTransformer(
|
||||
"topic-list-item-mobile-layout",
|
||||
this.site.mobileView,
|
||||
{ topic: this.args.topic }
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
|
@ -210,7 +218,7 @@ export default class Item extends Component {
|
|||
(if @topic.closed "closed")
|
||||
this.tagClassNames
|
||||
(applyValueTransformer
|
||||
"topic-list-item-class" (array) (hash topic=@topic)
|
||||
"topic-list-item-class" (array) (hash topic=@topic index=@index)
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
@ -218,20 +226,7 @@ export default class Item extends Component {
|
|||
@name="above-topic-list-item"
|
||||
@outletArgs={{hash topic=@topic}}
|
||||
/>
|
||||
{{#if this.site.desktopView}}
|
||||
{{#each @columns as |entry|}}
|
||||
<entry.value.item
|
||||
@topic={{@topic}}
|
||||
@bulkSelectEnabled={{@bulkSelectEnabled}}
|
||||
@onBulkSelectToggle={{this.onBulkSelectToggle}}
|
||||
@isSelected={{this.isSelected}}
|
||||
@showTopicPostBadges={{@showTopicPostBadges}}
|
||||
@hideCategory={{@hideCategory}}
|
||||
@tagsForUser={{@tagsForUser}}
|
||||
@expandPinned={{this.expandPinned}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#if this.useMobileLayout}}
|
||||
<td class="topic-list-data">
|
||||
<div class="pull-left">
|
||||
{{#if @bulkSelectEnabled}}
|
||||
|
@ -337,6 +332,19 @@ export default class Item extends Component {
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{{else}}
|
||||
{{#each @columns as |entry|}}
|
||||
<entry.value.item
|
||||
@topic={{@topic}}
|
||||
@bulkSelectEnabled={{@bulkSelectEnabled}}
|
||||
@onBulkSelectToggle={{this.onBulkSelectToggle}}
|
||||
@isSelected={{this.isSelected}}
|
||||
@showTopicPostBadges={{@showTopicPostBadges}}
|
||||
@hideCategory={{@hideCategory}}
|
||||
@tagsForUser={{@tagsForUser}}
|
||||
@expandPinned={{this.expandPinned}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { array, hash } from "@ember/helper";
|
||||
import { service } from "@ember/service";
|
||||
import { eq, or } from "truth-helpers";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
|
@ -172,6 +172,7 @@ export default class TopicList extends Component {
|
|||
class={{concatClass
|
||||
"topic-list"
|
||||
(if this.bulkSelectEnabled "sticky-header")
|
||||
(applyValueTransformer "topic-list-class" (array) (hash topics=@topics))
|
||||
}}
|
||||
>
|
||||
<caption class="sr-only">{{i18n "sr_topic_list_caption"}}</caption>
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
<div class="user-profile-avatar">
|
||||
{{bound-avatar @user "huge"}}
|
||||
<UserAvatarFlair @user={{@user}} />
|
||||
<div>
|
||||
<PluginOutlet
|
||||
@name="user-profile-avatar-wrapper"
|
||||
@outletArgs={{hash user=@user}}
|
||||
>
|
||||
<div class="user-profile-avatar">
|
||||
<PluginOutlet
|
||||
@name="user-profile-avatar-flair"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=@user}}
|
||||
/>
|
||||
@name="user-profile-avatar-img-wrapper"
|
||||
@outletArgs={{hash user=@user}}
|
||||
>
|
||||
{{bound-avatar @user "huge"}}
|
||||
</PluginOutlet>
|
||||
|
||||
<UserAvatarFlair @user={{@user}} />
|
||||
<div>
|
||||
<PluginOutlet
|
||||
@name="user-profile-avatar-flair"
|
||||
@outletArgs={{hash model=@user}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PluginOutlet>
|
|
@ -3,6 +3,7 @@ import { hash } from "@ember/helper";
|
|||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { NO_VALUE_OPTION } from "discourse/form-kit/lib/constants";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import FKControlSelectOption from "./select/option";
|
||||
|
||||
export default class FKControlSelect extends Component {
|
||||
|
@ -17,6 +18,10 @@ export default class FKControlSelect extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
get hasSelectedValue() {
|
||||
return this.args.field.value && this.args.field.value !== NO_VALUE_OPTION;
|
||||
}
|
||||
|
||||
<template>
|
||||
<select
|
||||
value={{@field.value}}
|
||||
|
@ -25,6 +30,14 @@ export default class FKControlSelect extends Component {
|
|||
class="form-kit__control-select"
|
||||
{{on "input" this.handleInput}}
|
||||
>
|
||||
<FKControlSelectOption @value={{NO_VALUE_OPTION}}>
|
||||
{{#if this.hasSelectedValue}}
|
||||
{{i18n "form_kit.select.none_placeholder"}}
|
||||
{{else}}
|
||||
{{i18n "form_kit.select.select_placeholder"}}
|
||||
{{/if}}
|
||||
</FKControlSelectOption>
|
||||
|
||||
{{yield
|
||||
(hash Option=(component FKControlSelectOption selected=@field.value))
|
||||
}}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { cancel } from "@ember/runloop";
|
|||
import { createPopper } from "@popperjs/core";
|
||||
import $ from "jquery";
|
||||
import { isDocumentRTL } from "discourse/lib/text-direction";
|
||||
import { caretPosition, setCaretPosition } from "discourse/lib/utilities";
|
||||
import { TextareaAutocompleteHandler } from "discourse/lib/textarea-text-manipulation";
|
||||
import Site from "discourse/models/site";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
|
@ -114,6 +114,8 @@ export default function (options) {
|
|||
const isInput = me[0].tagName === "INPUT" && !options.treatAsTextarea;
|
||||
let inputSelectedItems = [];
|
||||
|
||||
options.textHandler ??= new TextareaAutocompleteHandler(me[0]);
|
||||
|
||||
function handlePaste() {
|
||||
discourseLater(() => me.trigger("keydown"), 50);
|
||||
}
|
||||
|
@ -216,8 +218,6 @@ export default function (options) {
|
|||
}
|
||||
|
||||
let completeTerm = async function (term, event) {
|
||||
let completeEnd = null;
|
||||
|
||||
if (term) {
|
||||
if (isInput) {
|
||||
me.val("");
|
||||
|
@ -231,15 +231,13 @@ export default function (options) {
|
|||
}
|
||||
|
||||
if (term) {
|
||||
let text = me.val();
|
||||
|
||||
// After completion is done our position for completeStart may have
|
||||
// drifted. This can happen if the TEXTAREA changed out-of-band between
|
||||
// the time autocomplete was first displayed and the time of completion
|
||||
// Specifically this may happen due to uploads which inject a placeholder
|
||||
// which is later replaced with a different length string.
|
||||
let pos = await guessCompletePosition({ completeTerm: true });
|
||||
|
||||
let completeEnd = null;
|
||||
if (
|
||||
pos.completeStart !== undefined &&
|
||||
pos.completeEnd !== undefined
|
||||
|
@ -247,31 +245,18 @@ export default function (options) {
|
|||
completeStart = pos.completeStart;
|
||||
completeEnd = pos.completeEnd;
|
||||
} else {
|
||||
completeStart = completeEnd = caretPosition(me[0]);
|
||||
completeStart = completeEnd =
|
||||
options.textHandler.getCaretPosition();
|
||||
}
|
||||
|
||||
let space =
|
||||
text.substring(completeEnd + 1, completeEnd + 2) === " " ? "" : " ";
|
||||
|
||||
text =
|
||||
text.substring(0, completeStart) +
|
||||
(options.preserveKey ? options.key || "" : "") +
|
||||
term +
|
||||
space +
|
||||
text.substring(completeEnd + 1, text.length);
|
||||
|
||||
me.val(text);
|
||||
|
||||
let newCaretPos = completeStart + 1 + term.length;
|
||||
|
||||
if (options.key) {
|
||||
newCaretPos++;
|
||||
}
|
||||
|
||||
setCaretPosition(me[0], newCaretPos);
|
||||
options.textHandler.replaceTerm({
|
||||
start: completeStart,
|
||||
end: completeEnd,
|
||||
term: (options.preserveKey ? options.key || "" : "") + term,
|
||||
});
|
||||
|
||||
if (options && options.afterComplete) {
|
||||
options.afterComplete(text, event);
|
||||
options.afterComplete(options.textHandler.value, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -429,9 +414,7 @@ export default function (options) {
|
|||
}
|
||||
|
||||
let vOffset = 0;
|
||||
let pos = me.caretPosition({
|
||||
pos: completeStart + 1,
|
||||
});
|
||||
let pos = options.textHandler.getCaretCoords(completeStart);
|
||||
|
||||
if (options.treatAsTextarea) {
|
||||
vOffset = -32;
|
||||
|
@ -539,7 +522,11 @@ export default function (options) {
|
|||
closeAutocomplete();
|
||||
});
|
||||
|
||||
async function checkTriggerRule(opts) {
|
||||
async function checkTriggerRule(_opts) {
|
||||
const opts = {
|
||||
..._opts,
|
||||
inCodeBlock: () => options.textHandler.inCodeBlock(),
|
||||
};
|
||||
const shouldTrigger = await options.triggerRule?.(me[0], opts);
|
||||
return shouldTrigger ?? true;
|
||||
}
|
||||
|
@ -557,12 +544,12 @@ export default function (options) {
|
|||
return true;
|
||||
}
|
||||
|
||||
let cp = caretPosition(me[0]);
|
||||
const key = me[0].value[cp - 1];
|
||||
let cp = options.textHandler.getCaretPosition();
|
||||
const key = options.textHandler.value[cp - 1];
|
||||
|
||||
if (options.key) {
|
||||
if (options.onKeyUp && key !== options.key) {
|
||||
let match = options.onKeyUp(me.val(), cp);
|
||||
let match = options.onKeyUp(options.textHandler.value, cp);
|
||||
|
||||
if (match) {
|
||||
completeStart = cp - match[0].length;
|
||||
|
@ -574,10 +561,9 @@ export default function (options) {
|
|||
|
||||
if (completeStart === null && cp > 0) {
|
||||
if (key === options.key) {
|
||||
let prevChar = me.val().charAt(cp - 2);
|
||||
const shouldTrigger = await checkTriggerRule();
|
||||
let prevChar = options.textHandler.value.charAt(cp - 2);
|
||||
if (
|
||||
shouldTrigger &&
|
||||
(await checkTriggerRule()) &&
|
||||
(!prevChar || ALLOWED_LETTERS_REGEXP.test(prevChar))
|
||||
) {
|
||||
completeStart = cp - 1;
|
||||
|
@ -585,7 +571,10 @@ export default function (options) {
|
|||
}
|
||||
}
|
||||
} else if (completeStart !== null) {
|
||||
let term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
let term = options.textHandler.value.substring(
|
||||
completeStart + (options.key ? 1 : 0),
|
||||
cp
|
||||
);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
}
|
||||
}
|
||||
|
@ -593,10 +582,9 @@ export default function (options) {
|
|||
async function guessCompletePosition(opts) {
|
||||
let prev, stopFound, term;
|
||||
let prevIsGood = true;
|
||||
let element = me[0];
|
||||
let backSpace = opts?.backSpace;
|
||||
let completeTermOption = opts?.completeTerm;
|
||||
let caretPos = caretPosition(element);
|
||||
let caretPos = options.textHandler.getCaretPosition();
|
||||
|
||||
if (backSpace) {
|
||||
caretPos -= 1;
|
||||
|
@ -609,12 +597,12 @@ export default function (options) {
|
|||
|
||||
while (prevIsGood && caretPos >= 0) {
|
||||
caretPos -= 1;
|
||||
prev = element.value[caretPos];
|
||||
prev = options.textHandler.value[caretPos];
|
||||
|
||||
stopFound = prev === options.key;
|
||||
|
||||
if (stopFound) {
|
||||
prev = element.value[caretPos - 1];
|
||||
prev = options.textHandler.value[caretPos - 1];
|
||||
const shouldTrigger = await checkTriggerRule({ backSpace });
|
||||
|
||||
if (
|
||||
|
@ -622,7 +610,10 @@ export default function (options) {
|
|||
(prev === undefined || ALLOWED_LETTERS_REGEXP.test(prev))
|
||||
) {
|
||||
start = caretPos;
|
||||
term = element.value.substring(caretPos + 1, initialCaretPos);
|
||||
term = options.textHandler.value.substring(
|
||||
caretPos + 1,
|
||||
initialCaretPos
|
||||
);
|
||||
end = caretPos + term.length;
|
||||
break;
|
||||
}
|
||||
|
@ -653,9 +644,10 @@ export default function (options) {
|
|||
inputSelectedItems.push("");
|
||||
}
|
||||
|
||||
if (typeof inputSelectedItems[0] === "string" && me.val().length > 0) {
|
||||
const value = options.textHandler.value;
|
||||
if (typeof inputSelectedItems[0] === "string" && value.length > 0) {
|
||||
inputSelectedItems.pop();
|
||||
inputSelectedItems.push(me.val());
|
||||
inputSelectedItems.push(value);
|
||||
if (options.onChangeItems) {
|
||||
options.onChangeItems(inputSelectedItems);
|
||||
}
|
||||
|
@ -693,10 +685,13 @@ export default function (options) {
|
|||
}
|
||||
|
||||
if (completeStart !== null) {
|
||||
cp = caretPosition(me[0]);
|
||||
cp = options.textHandler.getCaretPosition();
|
||||
|
||||
// allow people to right arrow out of completion
|
||||
if (e.which === keys.rightArrow && me[0].value[cp] === " ") {
|
||||
if (
|
||||
e.which === keys.rightArrow &&
|
||||
options.textHandler.value[cp] === " "
|
||||
) {
|
||||
closeAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
@ -771,7 +766,10 @@ export default function (options) {
|
|||
return true;
|
||||
}
|
||||
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
term = options.textHandler.value.substring(
|
||||
completeStart + (options.key ? 1 : 0),
|
||||
cp
|
||||
);
|
||||
|
||||
if (completeStart === cp && term === options.key) {
|
||||
closeAutocomplete();
|
||||
|
|
|
@ -4,11 +4,7 @@ import { ajax } from "discourse/lib/ajax";
|
|||
import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
|
||||
import { getHashtagTypeClasses as getHashtagTypeClassesNew } from "discourse/lib/hashtag-type-registry";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import {
|
||||
caretPosition,
|
||||
escapeExpression,
|
||||
inCodeBlock,
|
||||
} from "discourse/lib/utilities";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { INPUT_DELAY, isTesting } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
|
@ -50,8 +46,8 @@ export function setupHashtagAutocomplete(
|
|||
);
|
||||
}
|
||||
|
||||
export async function hashtagTriggerRule(textarea) {
|
||||
return !(await inCodeBlock(textarea.value, caretPosition(textarea)));
|
||||
export async function hashtagTriggerRule(textarea, { inCodeBlock }) {
|
||||
return !(await inCodeBlock());
|
||||
}
|
||||
|
||||
export function hashtagAutocompleteOptions(
|
||||
|
@ -62,8 +58,6 @@ export function hashtagAutocompleteOptions(
|
|||
return {
|
||||
template: findRawTemplate("hashtag-autocomplete"),
|
||||
key: "#",
|
||||
afterComplete: autocompleteOptions.afterComplete,
|
||||
treatAsTextarea: autocompleteOptions.treatAsTextarea,
|
||||
scrollElementSelector: ".hashtag-autocomplete__fadeout",
|
||||
autoSelectFirstSuggestion: true,
|
||||
transformComplete: (obj) => obj.ref,
|
||||
|
@ -75,6 +69,7 @@ export function hashtagAutocompleteOptions(
|
|||
},
|
||||
triggerRule: async (textarea, opts) =>
|
||||
await hashtagTriggerRule(textarea, opts),
|
||||
...autocompleteOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,14 +11,6 @@ export const ADMIN_NAV_MAP = [
|
|||
label: "admin.account.sidebar_link.backups",
|
||||
icon: "box-archive",
|
||||
},
|
||||
{
|
||||
name: "admin_whats_new",
|
||||
route: "admin.whatsNew",
|
||||
label: "admin.account.sidebar_link.whats_new.title",
|
||||
icon: "gift",
|
||||
keywords: "admin.account.sidebar_link.whats_new.keywords",
|
||||
moderator: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -208,6 +208,14 @@ export function useAdminNavConfig(navMap) {
|
|||
label: "admin.advanced.sidebar_link.all_site_settings",
|
||||
icon: "gear",
|
||||
},
|
||||
{
|
||||
name: "admin_whats_new",
|
||||
route: "admin.whatsNew",
|
||||
label: "admin.account.sidebar_link.whats_new.title",
|
||||
icon: "gift",
|
||||
keywords: "admin.account.sidebar_link.whats_new.keywords",
|
||||
moderator: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -12,9 +12,11 @@ import {
|
|||
clipboardHelpers,
|
||||
determinePostReplaceSelection,
|
||||
inCodeBlock,
|
||||
setCaretPosition,
|
||||
} from "discourse/lib/utilities";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const INDENT_DIRECTION_LEFT = "left";
|
||||
|
@ -51,13 +53,19 @@ export default class TextareaTextManipulation {
|
|||
textarea;
|
||||
$textarea;
|
||||
|
||||
autocompleteHandler;
|
||||
placeholder;
|
||||
|
||||
constructor(owner, { markdownOptions, textarea, eventPrefix = "composer" }) {
|
||||
setOwner(this, owner);
|
||||
this.placeholder = new TextareaPlaceholderHandler(owner, this);
|
||||
|
||||
this.eventPrefix = eventPrefix;
|
||||
this.textarea = textarea;
|
||||
this.$textarea = $(textarea);
|
||||
|
||||
this.autocompleteHandler = new TextareaAutocompleteHandler(textarea);
|
||||
|
||||
generateLinkifyFunction(markdownOptions || {}).then((linkify) => {
|
||||
// When pasting links, we should use the same rules to match links as we do when creating links for a cooked post.
|
||||
this._cachedLinkify = linkify;
|
||||
|
@ -345,13 +353,7 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
|
||||
_insertAt(start, end, text) {
|
||||
this.textarea.setSelectionRange(start, end);
|
||||
this.textarea.focus();
|
||||
if (start !== end && text === "") {
|
||||
document.execCommand("delete", false);
|
||||
} else {
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
insertAtTextarea(this.textarea, start, end, text);
|
||||
}
|
||||
|
||||
extractTable(text) {
|
||||
|
@ -738,7 +740,6 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
async inCodeBlock() {
|
||||
return inCodeBlock(
|
||||
this.$textarea.value ?? this.$textarea.val(),
|
||||
|
@ -825,7 +826,187 @@ export default class TextareaTextManipulation {
|
|||
putCursorAtEnd(this.textarea);
|
||||
}
|
||||
|
||||
autocomplete() {
|
||||
return this.$textarea.autocomplete(...arguments);
|
||||
autocomplete(options) {
|
||||
return this.$textarea.autocomplete(
|
||||
options instanceof Object
|
||||
? { textHandler: this.autocompleteHandler, ...options }
|
||||
: options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function insertAtTextarea(textarea, start, end, text) {
|
||||
textarea.setSelectionRange(start, end);
|
||||
textarea.focus();
|
||||
if (start !== end && text === "") {
|
||||
document.execCommand("delete", false);
|
||||
} else {
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
}
|
||||
|
||||
export class TextareaAutocompleteHandler {
|
||||
textarea;
|
||||
$textarea;
|
||||
|
||||
constructor(textarea) {
|
||||
this.textarea = textarea;
|
||||
this.$textarea = $(textarea);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.textarea.value;
|
||||
}
|
||||
|
||||
replaceTerm({ start, end, term }) {
|
||||
const space = this.value.substring(end + 1, end + 2) === " " ? "" : " ";
|
||||
insertAtTextarea(this.textarea, start, end + 1, term + space);
|
||||
setCaretPosition(this.textarea, start + 1 + term.trim().length);
|
||||
}
|
||||
|
||||
getCaretPosition() {
|
||||
return caretPosition(this.textarea);
|
||||
}
|
||||
|
||||
getCaretCoords(start) {
|
||||
return this.$textarea.caretPosition({ pos: start + 1 });
|
||||
}
|
||||
|
||||
async inCodeBlock() {
|
||||
return inCodeBlock(
|
||||
this.$textarea.value ?? this.$textarea.val(),
|
||||
caretPosition(this.$textarea)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextareaPlaceholderHandler {
|
||||
@service composer;
|
||||
|
||||
textManipulation;
|
||||
|
||||
#placeholders = {};
|
||||
|
||||
constructor(owner, textManipulation) {
|
||||
setOwner(this, owner);
|
||||
|
||||
this.textManipulation = textManipulation;
|
||||
}
|
||||
|
||||
#uploadPlaceholder(file, currentMarkdown) {
|
||||
const clipboard = i18n("clipboard");
|
||||
const uploadFilenamePlaceholder = this.#uploadFilenamePlaceholder(
|
||||
file,
|
||||
currentMarkdown
|
||||
);
|
||||
const filename = uploadFilenamePlaceholder
|
||||
? uploadFilenamePlaceholder
|
||||
: clipboard;
|
||||
|
||||
let placeholder = `[${i18n("uploading_filename", { filename })}]()\n`;
|
||||
if (!this.#cursorIsOnEmptyLine()) {
|
||||
placeholder = `\n${placeholder}`;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
#cursorIsOnEmptyLine() {
|
||||
const selectionStart = this.textManipulation.textarea.selectionStart;
|
||||
return (
|
||||
selectionStart === 0 ||
|
||||
this.textManipulation.value.charAt(selectionStart - 1) === "\n"
|
||||
);
|
||||
}
|
||||
|
||||
#uploadFilenamePlaceholder(file, currentMarkdown) {
|
||||
const filename = this.#filenamePlaceholder(file);
|
||||
|
||||
// when adding two separate files with the same filename search for matching
|
||||
// placeholder already existing in the editor ie [Uploading: test.png…]
|
||||
// and add order nr to the next one: [Uploading: test.png(1)…]
|
||||
const escapedFilename = escapeRegExp(filename);
|
||||
const regexString = `\\[${i18n("uploading_filename", {
|
||||
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
||||
})}\\]\\(\\)`;
|
||||
const globalRegex = new RegExp(regexString, "g");
|
||||
const matchingPlaceholder = currentMarkdown.match(globalRegex);
|
||||
if (matchingPlaceholder) {
|
||||
// get last matching placeholder and its consecutive nr in regex
|
||||
// capturing group and apply +1 to the placeholder
|
||||
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
|
||||
const regex = new RegExp(regexString);
|
||||
const orderNr = regex.exec(lastMatch)[1]
|
||||
? parseInt(regex.exec(lastMatch)[1], 10) + 1
|
||||
: 1;
|
||||
return `${filename}(${orderNr})`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
#filenamePlaceholder(data) {
|
||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
|
||||
insert(file) {
|
||||
const placeholder = this.#uploadPlaceholder(
|
||||
file,
|
||||
this.composer.model.reply
|
||||
);
|
||||
|
||||
this.textManipulation.insertText(placeholder);
|
||||
|
||||
this.#placeholders[file.id] = { uploadPlaceholder: placeholder };
|
||||
}
|
||||
|
||||
progress(file) {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
placeholderData.processingPlaceholder = `[${i18n("processing_filename", {
|
||||
filename: file.name,
|
||||
})}]()\n`;
|
||||
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.uploadPlaceholder,
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
|
||||
// Safari applies user-defined replacements to text inserted programmatically.
|
||||
// One of the most common replacements is ... -> …, so we take care of the case
|
||||
// where that transformation has been applied to the original placeholder
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.uploadPlaceholder.replace("...", "…"),
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
}
|
||||
|
||||
progressComplete(file) {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
Object.values(this.#placeholders).forEach((data) => {
|
||||
this.textManipulation.replaceText(data.uploadPlaceholder, "");
|
||||
});
|
||||
}
|
||||
|
||||
cancel(file) {
|
||||
if (this.#placeholders[file.id]) {
|
||||
this.textManipulation.replaceText(
|
||||
this.#placeholders[file.id].uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
success(file, markdown) {
|
||||
this.textManipulation.replaceText(
|
||||
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,15 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
|||
"invite-simple-mode-topic",
|
||||
"mentions-class",
|
||||
"more-topics-tabs",
|
||||
"move-to-topic-merge-options",
|
||||
"move-to-topic-move-options",
|
||||
"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-class",
|
||||
"topic-list-item-class",
|
||||
"topic-list-item-mobile-layout",
|
||||
]);
|
||||
|
|
|
@ -23,7 +23,6 @@ import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
|||
import { clipboardHelpers } from "discourse/lib/utilities";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class UppyComposerUpload {
|
||||
|
@ -53,12 +52,12 @@ export default class UppyComposerUpload {
|
|||
uploadPreProcessors;
|
||||
uploadHandlers;
|
||||
|
||||
textManipulation;
|
||||
|
||||
#inProgressUploads = [];
|
||||
#bufferedUploadErrors = [];
|
||||
#placeholders = {};
|
||||
#consecutiveImages = [];
|
||||
|
||||
#useUploadPlaceholders = true;
|
||||
#uploadTargetBound = false;
|
||||
#userCancelled = false;
|
||||
|
||||
|
@ -288,7 +287,7 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
file.meta.cancelled = true;
|
||||
this.#removeInProgressUpload(file.id);
|
||||
this.#resetUpload(file, { removePlaceholder: true });
|
||||
this.#resetUpload(file);
|
||||
if (this.#inProgressUploads.length === 0) {
|
||||
this.#userCancelled = true;
|
||||
this.uppyWrapper.uppyInstance.cancelAll();
|
||||
|
@ -335,17 +334,7 @@ export default class UppyComposerUpload {
|
|||
})
|
||||
);
|
||||
|
||||
const placeholder = this.#uploadPlaceholder(file);
|
||||
this.#placeholders[file.id] = {
|
||||
uploadPlaceholder: placeholder,
|
||||
};
|
||||
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:insert-text`,
|
||||
placeholder
|
||||
);
|
||||
}
|
||||
this.textManipulation.placeholder.insert(file);
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:upload-started`,
|
||||
|
@ -366,28 +355,22 @@ export default class UppyComposerUpload {
|
|||
return;
|
||||
}
|
||||
let upload = response.body;
|
||||
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
// Only remove in progress after async resolvers finish:
|
||||
this.#removeInProgressUpload(file.id);
|
||||
cacheShortUploadUrl(upload.short_url, upload);
|
||||
|
||||
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
new ComposerVideoThumbnailUppy(getOwner(this)).generateVideoThumbnail(
|
||||
file,
|
||||
upload.url,
|
||||
() => {
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
}
|
||||
this.textManipulation.placeholder.success(file, markdown);
|
||||
|
||||
this.#resetUpload(file, { removePlaceholder: false });
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:upload-success`,
|
||||
file.name,
|
||||
|
@ -412,18 +395,7 @@ export default class UppyComposerUpload {
|
|||
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
||||
// Do the manual cancelling work only if the user clicked cancel
|
||||
if (this.#userCancelled) {
|
||||
Object.values(this.#placeholders).forEach((data) => {
|
||||
run(() => {
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
data.uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.textManipulation.placeholder.cancelAll();
|
||||
this.#userCancelled = false;
|
||||
this.#reset();
|
||||
|
||||
|
@ -442,7 +414,7 @@ export default class UppyComposerUpload {
|
|||
@bind
|
||||
_handleUploadError(file, error, response) {
|
||||
this.#removeInProgressUpload(file.id);
|
||||
this.#resetUpload(file, { removePlaceholder: true });
|
||||
this.#resetUpload(file);
|
||||
|
||||
file.meta.error = error;
|
||||
|
||||
|
@ -508,36 +480,13 @@ export default class UppyComposerUpload {
|
|||
});
|
||||
|
||||
this.uppyWrapper.onPreProcessProgress((file) => {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
placeholderData.processingPlaceholder = `[${i18n("processing_filename", {
|
||||
filename: file.name,
|
||||
})}]()\n`;
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.uploadPlaceholder,
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
|
||||
// Safari applies user-defined replacements to text inserted programmatically.
|
||||
// One of the most common replacements is ... -> …, so we take care of the case
|
||||
// where that transformation has been applied to the original placeholder
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.uploadPlaceholder.replace("...", "…"),
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
this.textManipulation.placeholder.progress(file);
|
||||
});
|
||||
|
||||
this.uppyWrapper.onPreProcessComplete(
|
||||
(file) => {
|
||||
run(() => {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
this.textManipulation.placeholder.progressComplete(file);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
|
@ -554,47 +503,6 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
}
|
||||
|
||||
#uploadFilenamePlaceholder(file) {
|
||||
const filename = this.#filenamePlaceholder(file);
|
||||
|
||||
// when adding two separate files with the same filename search for matching
|
||||
// placeholder already existing in the editor ie [Uploading: test.png…]
|
||||
// and add order nr to the next one: [Uploading: test.png(1)…]
|
||||
const escapedFilename = escapeRegExp(filename);
|
||||
const regexString = `\\[${i18n("uploading_filename", {
|
||||
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
||||
})}\\]\\(\\)`;
|
||||
const globalRegex = new RegExp(regexString, "g");
|
||||
const matchingPlaceholder = this.composerModel.reply.match(globalRegex);
|
||||
if (matchingPlaceholder) {
|
||||
// get last matching placeholder and its consecutive nr in regex
|
||||
// capturing group and apply +1 to the placeholder
|
||||
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
|
||||
const regex = new RegExp(regexString);
|
||||
const orderNr = regex.exec(lastMatch)[1]
|
||||
? parseInt(regex.exec(lastMatch)[1], 10) + 1
|
||||
: 1;
|
||||
return `${filename}(${orderNr})`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
#uploadPlaceholder(file) {
|
||||
const clipboard = i18n("clipboard");
|
||||
const uploadFilenamePlaceholder = this.#uploadFilenamePlaceholder(file);
|
||||
const filename = uploadFilenamePlaceholder
|
||||
? uploadFilenamePlaceholder
|
||||
: clipboard;
|
||||
|
||||
let placeholder = `[${i18n("uploading_filename", { filename })}]()\n`;
|
||||
if (!this.#cursorIsOnEmptyLine()) {
|
||||
placeholder = `\n${placeholder}`;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
#useXHRUploads() {
|
||||
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
||||
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
||||
|
@ -620,14 +528,8 @@ export default class UppyComposerUpload {
|
|||
this.#fileInputEl.value = "";
|
||||
}
|
||||
|
||||
#resetUpload(file, opts) {
|
||||
if (opts.removePlaceholder && this.#placeholders[file.id]) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
this.#placeholders[file.id].uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
#resetUpload(file) {
|
||||
this.textManipulation.placeholder.cancel(file);
|
||||
}
|
||||
|
||||
@bind
|
||||
|
@ -704,10 +606,6 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
}
|
||||
|
||||
#filenamePlaceholder(data) {
|
||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
|
||||
#findMatchingUploadHandler(fileName) {
|
||||
return this.uploadHandlers.find((handler) => {
|
||||
const ext = handler.extensions.join("|");
|
||||
|
@ -716,14 +614,6 @@ export default class UppyComposerUpload {
|
|||
});
|
||||
}
|
||||
|
||||
#cursorIsOnEmptyLine() {
|
||||
const textArea = this.#editorEl.querySelector(this.editorInputClass);
|
||||
const selectionStart = textArea.selectionStart;
|
||||
return (
|
||||
selectionStart === 0 || textArea.value.charAt(selectionStart - 1) === "\n"
|
||||
);
|
||||
}
|
||||
|
||||
#autoGridImages() {
|
||||
const reply = this.composerModel.get("reply");
|
||||
const imagesToWrapGrid = new Set(this.#consecutiveImages);
|
||||
|
|
|
@ -23,6 +23,7 @@ import discourseComputed from "discourse-common/utils/decorators";
|
|||
import { i18n } from "discourse-i18n";
|
||||
|
||||
let _customizations = [];
|
||||
|
||||
export function registerCustomizationCallback(cb) {
|
||||
_customizations.push(cb);
|
||||
}
|
||||
|
@ -804,29 +805,29 @@ export default class Composer extends RestModel {
|
|||
}
|
||||
|
||||
/**
|
||||
Open a composer
|
||||
Open a composer
|
||||
|
||||
@method open
|
||||
@param {Object} opts
|
||||
@param {String} opts.action The action we're performing: edit, reply, createTopic, createSharedDraft, privateMessage
|
||||
@param {String} opts.draftKey
|
||||
@param {String} opts.draftSequence
|
||||
@param {Post} [opts.post] The post we're replying to, if present
|
||||
@param {Topic} [opts.topic] The topic we're replying to, if present
|
||||
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
|
||||
@param {String} [opts.reply]
|
||||
@param {String} [opts.recipients]
|
||||
@param {Number} [opts.composerTime]
|
||||
@param {Number} [opts.typingTime]
|
||||
@param {Boolean} [opts.whisper]
|
||||
@param {Boolean} [opts.noBump]
|
||||
@param {String} [opts.archetypeId] One of `site.archetypes` e.g. `regular` or `private_message`
|
||||
@param {Object} [opts.metaData]
|
||||
@param {Number} [opts.categoryId]
|
||||
@param {Number} [opts.postId]
|
||||
@param {Number} [opts.destinationCategoryId]
|
||||
@param {String} [opts.title]
|
||||
**/
|
||||
@method open
|
||||
@param {Object} opts
|
||||
@param {String} opts.action The action we're performing: edit, reply, createTopic, createSharedDraft, privateMessage
|
||||
@param {String} opts.draftKey
|
||||
@param {String} opts.draftSequence
|
||||
@param {Post} [opts.post] The post we're replying to, if present
|
||||
@param {Topic} [opts.topic] The topic we're replying to, if present
|
||||
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
|
||||
@param {String} [opts.reply]
|
||||
@param {String} [opts.recipients]
|
||||
@param {Number} [opts.composerTime]
|
||||
@param {Number} [opts.typingTime]
|
||||
@param {Boolean} [opts.whisper]
|
||||
@param {Boolean} [opts.noBump]
|
||||
@param {String} [opts.archetypeId] One of `site.archetypes` e.g. `regular` or `private_message`
|
||||
@param {Object} [opts.metaData]
|
||||
@param {Number} [opts.categoryId]
|
||||
@param {Number} [opts.postId]
|
||||
@param {Number} [opts.destinationCategoryId]
|
||||
@param {String} [opts.title]
|
||||
**/
|
||||
open(opts) {
|
||||
let promise = Promise.resolve();
|
||||
|
||||
|
@ -886,7 +887,18 @@ export default class Composer extends RestModel {
|
|||
});
|
||||
|
||||
if (!this.topic) {
|
||||
this.set("topic", opts.post.topic);
|
||||
if (opts.post.topic) {
|
||||
this.set("topic", opts.post.topic);
|
||||
} else {
|
||||
// handles the edge cases where the topic model is not loaded in the post model and the store does not have a
|
||||
// topic for the post, e.g., make a post then edit right away, edit a post outside the post stream, etc.
|
||||
promise = promise.then(async () => {
|
||||
const data = await Topic.find(opts.post.topic_id, {});
|
||||
const topic = this.store.createRecord("topic", data);
|
||||
this.post.set("topic", topic);
|
||||
this.set("topic", topic);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (opts.postId) {
|
||||
promise = promise.then(() =>
|
||||
|
@ -935,37 +947,22 @@ export default class Composer extends RestModel {
|
|||
}
|
||||
this.setProperties(topicProps);
|
||||
|
||||
promise = promise.then(() => {
|
||||
let rawPromise = this.store.find("post", opts.post.id).then((post) => {
|
||||
this.setProperties({
|
||||
post,
|
||||
reply: post.raw,
|
||||
originalText: post.raw,
|
||||
});
|
||||
|
||||
if (post.post_number === 1 && this.canEditTitle) {
|
||||
this.setProperties({
|
||||
originalTitle: post.topic.title,
|
||||
originalTags: post.topic.tags,
|
||||
});
|
||||
}
|
||||
promise = promise.then(async () => {
|
||||
const post = await this.store.find("post", opts.post.id);
|
||||
this.setProperties({
|
||||
post,
|
||||
reply: post.raw,
|
||||
originalText: post.raw,
|
||||
});
|
||||
|
||||
// edge case ... make a post then edit right away
|
||||
// store does not have topic for the post
|
||||
if (this.topic && this.topic.id === this.post.topic_id) {
|
||||
// nothing to do ... we have the right topic
|
||||
} else {
|
||||
rawPromise = this.store
|
||||
.find("topic", this.post.topic_id)
|
||||
.then((topic) => {
|
||||
this.set("topic", topic);
|
||||
});
|
||||
if (post.post_number === 1 && this.canEditTitle) {
|
||||
this.setProperties({
|
||||
originalTitle: this.topic.title,
|
||||
originalTags: this.topic.tags,
|
||||
});
|
||||
}
|
||||
|
||||
return rawPromise.then(() => {
|
||||
this.appEvents.trigger("composer:reply-reloaded", this);
|
||||
});
|
||||
this.appEvents.trigger("composer:reply-reloaded", this);
|
||||
});
|
||||
} else if (opts.action === REPLY && opts.quote) {
|
||||
this.set("reply", opts.quote);
|
||||
|
|
|
@ -171,14 +171,13 @@ export default class Post extends RestModel {
|
|||
@alias("can_edit") canEdit; // for compatibility with existing code
|
||||
@equal("trust_level", 0) new_user;
|
||||
@equal("post_number", 1) firstPost;
|
||||
@or("deleted_at", "deletedViaTopic") deleted;
|
||||
@and("firstPost", "topic.deleted_at") deletedViaTopic; // mark fist post as deleted if topic was deleted
|
||||
@or("deleted_at", "deletedViaTopic") deleted; // post is either highlighted as deleted or hidden/removed from the post stream
|
||||
@not("deleted") notDeleted;
|
||||
@or("deleted_at", "user_deleted") recoverable; // post or content still can be recovered
|
||||
@propertyEqual("topic.details.created_by.id", "user_id") topicOwner;
|
||||
@alias("topic.details.created_by.id") topicCreatedById;
|
||||
|
||||
// Posts can show up as deleted if the topic is deleted
|
||||
@and("firstPost", "topic.deleted_at") deletedViaTopic;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
|
@ -314,14 +313,6 @@ export default class Post extends RestModel {
|
|||
return this.firstPost && !!this.topic.details.can_publish_page;
|
||||
}
|
||||
|
||||
get canRecover() {
|
||||
return this.deleted && this.can_recover;
|
||||
}
|
||||
|
||||
get isRecovering() {
|
||||
return !this.deleted && this.can_recover;
|
||||
}
|
||||
|
||||
get canRecoverTopic() {
|
||||
return this.firstPost && this.deleted && this.topic.details.can_recover;
|
||||
}
|
||||
|
@ -330,6 +321,14 @@ export default class Post extends RestModel {
|
|||
return this.firstPost && !this.deleted && this.topic.details.can_recover;
|
||||
}
|
||||
|
||||
get canRecover() {
|
||||
return !this.canRecoverTopic && this.recoverable && this.can_recover;
|
||||
}
|
||||
|
||||
get isRecovering() {
|
||||
return !this.isRecoveringTopic && !this.recoverable && this.can_recover;
|
||||
}
|
||||
|
||||
get canToggleLike() {
|
||||
return !!this.likeAction?.get("canToggle");
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import ActionSummary from "discourse/models/action-summary";
|
|||
import Bookmark from "discourse/models/bookmark";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import Site from "discourse/models/site";
|
||||
import TopicDetails from "discourse/models/topic-details";
|
||||
import { flushMap } from "discourse/services/store";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
|
@ -461,7 +462,13 @@ export default class Topic extends RestModel {
|
|||
}
|
||||
|
||||
set details(value) {
|
||||
this._details = value;
|
||||
if (value instanceof TopicDetails) {
|
||||
this._details = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// we need to ensure that details is an instance of TopicDetails
|
||||
this._details = this.store.createRecord("topicDetails", value);
|
||||
}
|
||||
|
||||
@discourseComputed("visible")
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.model.automatic}}
|
||||
{{#if (and this.currentUser.admin this.model.automatic)}}
|
||||
<DTooltip class="admin-group-automatic-tooltip">
|
||||
<:trigger>
|
||||
{{d-icon "gear"}}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/standalone": "^7.26.2",
|
||||
"@babel/standalone": "^7.26.4",
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@discourse/backburner.js": "^2.7.1-0",
|
||||
"@discourse/itsatrap": "^2.0.10",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"@ember/render-modifiers": "^2.1.0",
|
||||
"@ember/string": "^4.0.0",
|
||||
"@ember/test-helpers": "^4.0.4",
|
||||
"@ember/test-waiters": "^3.1.0",
|
||||
"@ember/test-waiters": "^4.0.0",
|
||||
"@embroider/compat": "^3.7.0",
|
||||
"@embroider/core": "^3.4.19",
|
||||
"@embroider/macros": "^1.16.9",
|
||||
|
@ -112,18 +112,18 @@
|
|||
"make-plural": "^7.4.0",
|
||||
"message-bus-client": "^4.3.8",
|
||||
"pretender": "^3.4.7",
|
||||
"qunit": "^2.23.0",
|
||||
"qunit": "^2.23.1",
|
||||
"qunit-dom": "^3.4.0",
|
||||
"sass": "^1.77.7",
|
||||
"select-kit": "workspace:1.0.0",
|
||||
"sinon": "^19.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"terser": "^5.36.0",
|
||||
"terser": "^5.37.0",
|
||||
"testem": "^3.15.2",
|
||||
"truth-helpers": "workspace:1.0.0",
|
||||
"util": "^0.12.5",
|
||||
"virtual-dom": "^2.1.1",
|
||||
"webpack": "^5.97.0",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"webpack-stats-plugin": "^1.1.3",
|
||||
"xss": "^1.0.15"
|
||||
|
|
|
@ -215,7 +215,7 @@ acceptance(
|
|||
visible: true,
|
||||
public_admission: true,
|
||||
public_exit: false,
|
||||
flair_url: "fa-circle-half-stroke",
|
||||
flair_url: "circle-half-stroke",
|
||||
is_group_owner: true,
|
||||
mentionable: true,
|
||||
messageable: true,
|
||||
|
|
|
@ -24,6 +24,24 @@ acceptance("Managing Group Membership", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("As an admin on an automatic group", async function (assert) {
|
||||
await visit("/g/moderators");
|
||||
|
||||
assert
|
||||
.dom(".admin-group-automatic-tooltip")
|
||||
.exists("displays automatic tooltip");
|
||||
});
|
||||
|
||||
test("As a non-admin user on an automatic group", async function (assert) {
|
||||
updateCurrentUser({ admin: false });
|
||||
|
||||
await visit("/g/moderators");
|
||||
|
||||
assert
|
||||
.dom(".admin-group-automatic-tooltip")
|
||||
.doesNotExist("does not display automatic tooltip");
|
||||
});
|
||||
|
||||
test("As an admin", async function (assert) {
|
||||
updateCurrentUser({ can_create_group: true });
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import pretender, {
|
||||
parsePostData,
|
||||
response,
|
||||
} from "discourse/tests/helpers/create-pretender";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
acceptance("Modal - move-to-topic", function (needs) {
|
||||
needs.user({ admin: true });
|
||||
|
||||
test("Transformer can modify merge/move options sent in request", async function (assert) {
|
||||
withPluginApi("1.24.0", (api) => {
|
||||
["move-to-topic-merge-options", "move-to-topic-move-options"].forEach(
|
||||
(transformerName) => {
|
||||
api.registerValueTransformer(transformerName, (transformer) => {
|
||||
transformer.value.sillyVal = true;
|
||||
return transformer.value;
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
// Open admin menu, select a post, and open move to topic modal
|
||||
await click(".topic-admin-menu-trigger");
|
||||
await click(".topic-admin-menu-content .topic-admin-multi-select button");
|
||||
await click(".select-posts .select-post");
|
||||
await click(".selected-posts .move-to-topic");
|
||||
|
||||
// Choose existing topic, and pick the first topic.
|
||||
await click("input#move-to-existing-topic");
|
||||
await fillIn("input#choose-topic-title", 1);
|
||||
await click(".choose-topic-list .existing-topic input");
|
||||
|
||||
pretender.post("/t/280/move-posts", (request) => {
|
||||
assert.step("request");
|
||||
const data = parsePostData(request.requestBody);
|
||||
assert.strictEqual(data.sillyVal, "true");
|
||||
return response({ success: true });
|
||||
});
|
||||
|
||||
// Submit!
|
||||
await click(".d-modal__footer .btn-primary");
|
||||
assert.verifySteps(["request"]);
|
||||
});
|
||||
});
|
|
@ -70,7 +70,7 @@ acceptance("Search - Full Page", function (needs) {
|
|||
grant_count: 0,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
|
|
@ -60,7 +60,7 @@ export default {
|
|||
grant_count: 11,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 8,
|
||||
|
@ -75,7 +75,7 @@ export default {
|
|||
grant_count: 10,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-gear",
|
||||
icon: "gear",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 8,
|
||||
|
@ -89,7 +89,7 @@ export default {
|
|||
grant_count: 29,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
|
@ -103,7 +103,7 @@ export default {
|
|||
grant_count: 200,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 8,
|
||||
|
@ -118,7 +118,7 @@ export default {
|
|||
grant_count: 9,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-bug",
|
||||
icon: "bug",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 7,
|
||||
|
@ -132,7 +132,7 @@ export default {
|
|||
grant_count: 0,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 8,
|
||||
|
@ -146,7 +146,7 @@ export default {
|
|||
grant_count: 467,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
|
@ -161,7 +161,7 @@ export default {
|
|||
grant_count: 183,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-bug",
|
||||
icon: "bug",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 7,
|
||||
|
@ -175,7 +175,7 @@ export default {
|
|||
grant_count: 4,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
|
@ -189,7 +189,7 @@ export default {
|
|||
grant_count: 278,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -203,7 +203,7 @@ export default {
|
|||
grant_count: 5834,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
|
@ -217,7 +217,7 @@ export default {
|
|||
grant_count: 60,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -231,7 +231,7 @@ export default {
|
|||
grant_count: 22,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 3,
|
||||
|
@ -245,7 +245,7 @@ export default {
|
|||
grant_count: 2,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 3,
|
||||
|
@ -259,7 +259,7 @@ export default {
|
|||
grant_count: 2387,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -273,7 +273,7 @@ export default {
|
|||
grant_count: 285,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -287,7 +287,7 @@ export default {
|
|||
grant_count: 42,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 2,
|
||||
|
@ -301,7 +301,7 @@ export default {
|
|||
grant_count: 1718,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 2,
|
||||
|
@ -315,7 +315,7 @@ export default {
|
|||
grant_count: 270,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -330,7 +330,7 @@ export default {
|
|||
grant_count: 545,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -344,7 +344,7 @@ export default {
|
|||
grant_count: 397,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -358,7 +358,7 @@ export default {
|
|||
grant_count: 259,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 3,
|
||||
|
@ -372,7 +372,7 @@ export default {
|
|||
grant_count: 933,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 2,
|
||||
|
@ -396,7 +396,7 @@ export default {
|
|||
grant_count: 545,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -419,7 +419,7 @@ export default {
|
|||
grant_count: 10,
|
||||
allow_title: true,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -437,7 +437,7 @@ export default {
|
|||
grant_count: 545,
|
||||
allow_title: false,
|
||||
multiple_grant: false,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 1,
|
||||
|
@ -1714,7 +1714,7 @@ export default {
|
|||
{
|
||||
id: 1,
|
||||
name: "Only icon",
|
||||
icon: "fa-rocket",
|
||||
icon: "rocket",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
@ -1724,7 +1724,7 @@ export default {
|
|||
{
|
||||
id: 3,
|
||||
name: "Both image and icon",
|
||||
icon: "fa-rocket",
|
||||
icon: "rocket",
|
||||
image_url: "/images/avatar.png?3",
|
||||
multiple_grant: true,
|
||||
query: "SELECT 1",
|
||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
|||
visible: true,
|
||||
public_admission: true,
|
||||
public_exit: false,
|
||||
flair_url: "fa-circle-half-stroke",
|
||||
flair_url: "circle-half-stroke",
|
||||
is_group_owner: true,
|
||||
mentionable: true,
|
||||
messageable: true,
|
||||
|
@ -66,7 +66,7 @@ export default {
|
|||
visible: true,
|
||||
public_admission: true,
|
||||
public_exit: false,
|
||||
flair_url: "fa-circle-half-stroke",
|
||||
flair_url: "circle-half-stroke",
|
||||
is_group_owner: true,
|
||||
mentionable: true,
|
||||
messageable: true,
|
||||
|
@ -260,7 +260,7 @@ export default {
|
|||
},
|
||||
},
|
||||
"/groups/discourse/posts.json": {
|
||||
"posts": [
|
||||
posts: [
|
||||
{
|
||||
id: 94607,
|
||||
cooked:
|
||||
|
@ -1319,8 +1319,8 @@ export default {
|
|||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
"/groups/alternative-group.json": {
|
||||
group: {
|
||||
|
@ -1334,7 +1334,7 @@ export default {
|
|||
has_messages: true,
|
||||
public_admission: true,
|
||||
public_exit: false,
|
||||
flair_url: "fa-circle-half-stroke",
|
||||
flair_url: "circle-half-stroke",
|
||||
is_group_owner: true,
|
||||
mentionable: true,
|
||||
messageable: true,
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
grant_count: 14,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -349,7 +349,7 @@ export default {
|
|||
grant_count: 14,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
@ -3210,7 +3210,7 @@ export default {
|
|||
grant_count: 14,
|
||||
allow_title: false,
|
||||
multiple_grant: true,
|
||||
icon: "fa-certificate",
|
||||
icon: "certificate",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import BookmarkIcon from "discourse/components/bookmark-icon";
|
||||
import { formattedReminderTime } from "discourse/lib/bookmark";
|
||||
import { tomorrow } from "discourse/lib/time-utils";
|
||||
import Bookmark from "discourse/models/bookmark";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
|
@ -11,15 +10,14 @@ module("Integration | Component | bookmark-icon", function (hooks) {
|
|||
setupRenderingTest(hooks);
|
||||
|
||||
test("with reminder", async function (assert) {
|
||||
this.setProperties({
|
||||
bookmark: Bookmark.create({
|
||||
reminder_at: tomorrow(this.currentUser.user_option.timezone),
|
||||
name: "some name",
|
||||
currentUser: this.currentUser,
|
||||
}),
|
||||
const store = this.owner.lookup("service:store");
|
||||
const bookmark = store.createRecord("bookmark", {
|
||||
reminder_at: tomorrow(this.currentUser.user_option.timezone),
|
||||
name: "some name",
|
||||
currentUser: this.currentUser,
|
||||
});
|
||||
|
||||
await render(hbs`<BookmarkIcon @bookmark={{this.bookmark}} />`);
|
||||
await render(<template><BookmarkIcon @bookmark={{bookmark}} /></template>);
|
||||
|
||||
assert
|
||||
.dom(".d-icon-discourse-bookmark-clock.bookmark-icon__bookmarked")
|
||||
|
@ -28,7 +26,7 @@ module("Integration | Component | bookmark-icon", function (hooks) {
|
|||
"title",
|
||||
i18n("bookmarks.created_with_reminder_generic", {
|
||||
date: formattedReminderTime(
|
||||
this.bookmark.reminder_at,
|
||||
bookmark.reminder_at,
|
||||
this.currentUser.user_option.timezone
|
||||
),
|
||||
name: "some name",
|
||||
|
@ -37,15 +35,13 @@ module("Integration | Component | bookmark-icon", function (hooks) {
|
|||
});
|
||||
|
||||
test("no reminder", async function (assert) {
|
||||
this.set(
|
||||
"bookmark",
|
||||
Bookmark.create({
|
||||
name: "some name",
|
||||
currentUser: this.currentUser,
|
||||
})
|
||||
);
|
||||
const store = this.owner.lookup("service:store");
|
||||
const bookmark = store.createRecord("bookmark", {
|
||||
name: "some name",
|
||||
currentUser: this.currentUser,
|
||||
});
|
||||
|
||||
await render(hbs`<BookmarkIcon @bookmark={{this.bookmark}} />`);
|
||||
await render(<template><BookmarkIcon @bookmark={{bookmark}} /></template>);
|
||||
|
||||
assert.dom(".d-icon-bookmark.bookmark-icon__bookmarked").exists();
|
||||
assert.dom(".svg-icon-title").hasAttribute(
|
||||
|
@ -56,12 +52,8 @@ module("Integration | Component | bookmark-icon", function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test("null bookmark", async function (assert) {
|
||||
this.setProperties({
|
||||
bookmark: null,
|
||||
});
|
||||
|
||||
await render(hbs`<BookmarkIcon @bookmark={{this.bookmark}} />`);
|
||||
test("no bookmark", async function (assert) {
|
||||
await render(<template><BookmarkIcon /></template>);
|
||||
|
||||
assert.dom(".d-icon-bookmark.bookmark-icon").exists();
|
||||
assert
|
|
@ -166,7 +166,7 @@ module("Integration | Component | CreateInvite", function (hooks) {
|
|||
|
||||
assert.deepEqual(
|
||||
formKit().field("expiresAfterDays").options(),
|
||||
["1", "3", "7", "30", "90", "999999"],
|
||||
["__NONE__", "1", "3", "7", "30", "90", "999999"],
|
||||
"the value of invite_expiry_days is added to the dropdown"
|
||||
);
|
||||
|
||||
|
@ -179,7 +179,7 @@ module("Integration | Component | CreateInvite", function (hooks) {
|
|||
|
||||
assert.deepEqual(
|
||||
formKit().field("expiresAfterDays").options(),
|
||||
["1", "7", "30", "90", "999999"],
|
||||
["__NONE__", "1", "7", "30", "90", "999999"],
|
||||
"the value of invite_expiry_days is not added to the dropdown if it's already one of the options"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { module, test } from "qunit";
|
|||
import Form from "discourse/components/form";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
module(
|
||||
"Integration | Component | FormKit | Controls | Select",
|
||||
|
@ -50,5 +51,33 @@ module(
|
|||
|
||||
assert.dom(".form-kit__control-select").hasAttribute("disabled");
|
||||
});
|
||||
|
||||
test("no selection", async function (assert) {
|
||||
await render(<template>
|
||||
<Form as |form|>
|
||||
<form.Field @name="foo" @title="Foo" as |field|>
|
||||
<field.Select as |select|>
|
||||
<select.Option @value="option-1">Option 1</select.Option>
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert
|
||||
.dom(".form-kit__control-select option:nth-child(1)")
|
||||
.hasText(
|
||||
i18n("form_kit.select.select_placeholder"),
|
||||
"it shows a placeholder for selection"
|
||||
);
|
||||
|
||||
await formKit().field("foo").select("option-1");
|
||||
|
||||
assert
|
||||
.dom(".form-kit__control-select option:nth-child(1)")
|
||||
.hasText(
|
||||
i18n("form_kit.select.none_placeholder"),
|
||||
"it shows a placeholder for unselection"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -9,28 +9,28 @@ function setupSiteGroups(that) {
|
|||
{
|
||||
id: 1,
|
||||
name: "admins",
|
||||
flair_url: "fa-bars",
|
||||
flair_url: "bars",
|
||||
flair_bg_color: "CC000A",
|
||||
flair_color: "FFFFFA",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "staff",
|
||||
flair_url: "fa-bars",
|
||||
flair_url: "bars",
|
||||
flair_bg_color: "CC0005",
|
||||
flair_color: "FFFFF5",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "trust_level_1",
|
||||
flair_url: "fa-dice-one",
|
||||
flair_url: "dice-one",
|
||||
flair_bg_color: "CC0001",
|
||||
flair_color: "FFFFF1",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "trust_level_2",
|
||||
flair_url: "fa-dice-two",
|
||||
flair_url: "dice-two",
|
||||
flair_bg_color: "CC0002",
|
||||
flair_color: "FFFFF2",
|
||||
},
|
||||
|
@ -159,7 +159,7 @@ module("Integration | Component | user-avatar-flair", function (hooks) {
|
|||
moderator: false,
|
||||
trust_level: 3,
|
||||
flair_name: "Band Geeks",
|
||||
flair_url: "fa-xmark",
|
||||
flair_url: "xmark",
|
||||
flair_bg_color: "123456",
|
||||
flair_color: "B0B0B0",
|
||||
flair_group_id: 41,
|
||||
|
|
|
@ -8,7 +8,7 @@ module("Integration | Component | Widget | avatar-flair", function (hooks) {
|
|||
|
||||
test("avatar flair with an icon", async function (assert) {
|
||||
this.set("args", {
|
||||
flair_url: "fa-bars",
|
||||
flair_url: "bars",
|
||||
flair_bg_color: "CC0000",
|
||||
flair_color: "FFFFFF",
|
||||
});
|
||||
|
|
|
@ -151,4 +151,22 @@ module("Unit | Utility | autocomplete", function (hooks) {
|
|||
assert.dom("#ac-testing li a.selected").exists({ count: 1 });
|
||||
assert.dom("#ac-testing li a.selected").hasText("test1");
|
||||
});
|
||||
|
||||
test("Autocomplete doesn't reset undo history", async function (assert) {
|
||||
const element = textArea();
|
||||
|
||||
$(element).autocomplete({
|
||||
key: "@",
|
||||
template,
|
||||
dataSource: () => ["test1", "test2"],
|
||||
});
|
||||
|
||||
await simulateKeys(element, "@t\r");
|
||||
|
||||
assert.strictEqual(element.value, "@test1 ");
|
||||
|
||||
document.execCommand("undo");
|
||||
|
||||
assert.strictEqual(element.value, "@t");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { IMAGE_VERSION as v } from "pretty-text/emoji/version";
|
|||
import { module, test } from "qunit";
|
||||
import Category from "discourse/models/category";
|
||||
import Topic from "discourse/models/topic";
|
||||
import TopicDetails from "discourse/models/topic-details";
|
||||
|
||||
module("Unit | Model | topic", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
@ -108,6 +109,10 @@ module("Unit | Model | topic", function (hooks) {
|
|||
const topic = this.store.createRecord("topic", { id: 1234 });
|
||||
const topicDetails = topic.details;
|
||||
|
||||
assert.true(
|
||||
topicDetails instanceof TopicDetails,
|
||||
"topicDetails is an instance of TopicDetails"
|
||||
);
|
||||
assert.present(topicDetails, "a topic has topicDetails after we create it");
|
||||
assert.strictEqual(
|
||||
topicDetails.topic,
|
||||
|
@ -165,6 +170,10 @@ module("Unit | Model | topic", function (hooks) {
|
|||
});
|
||||
|
||||
assert.blank(topic.post_stream, "it does not update post_stream");
|
||||
assert.true(
|
||||
topic.details instanceof TopicDetails,
|
||||
"topicDetails is an instance of TopicDetails"
|
||||
);
|
||||
assert.strictEqual(topic.details.hello, "world", "it updates the details");
|
||||
assert.strictEqual(topic.cool, "property", "it updates other properties");
|
||||
assert.strictEqual(topic.category, category);
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"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.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
{{#if this.collection.content.length}}
|
||||
<ul class="select-kit-collection" aria-live="polite" role="menu">
|
||||
<ul
|
||||
class="select-kit-collection"
|
||||
aria-live="polite"
|
||||
role="menu"
|
||||
{{this.bodyScrollLock}}
|
||||
>
|
||||
{{#each this.collection.content as |item index|}}
|
||||
{{component
|
||||
(component-for-row this.collection.identifier item this.selectKit)
|
||||
|
|
|
@ -1,5 +1,35 @@
|
|||
import Component from "@ember/component";
|
||||
import { service } from "@ember/service";
|
||||
import { tagName } from "@ember-decorators/component";
|
||||
import { modifier } from "ember-modifier";
|
||||
import {
|
||||
disableBodyScroll,
|
||||
enableBodyScroll,
|
||||
} from "discourse/lib/body-scroll-lock";
|
||||
|
||||
@tagName("")
|
||||
export default class SelectKitCollection extends Component {}
|
||||
export default class SelectKitCollection extends Component {
|
||||
@service site;
|
||||
|
||||
bodyScrollLock = modifier((element) => {
|
||||
if (!this.site.mobileView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when opened a modal will disable all scroll but itself
|
||||
// this code is whitelisting the collection to ensure it can be scrolled in this case
|
||||
// however we only want to do this if the modal is open to avoid breaking the scroll on the page
|
||||
// eg: opening a combobox under a topic shouldn't prevent you to scroll the topic page
|
||||
const isModalOpen =
|
||||
document.documentElement.classList.contains("modal-open");
|
||||
if (!isModalOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
disableBodyScroll(element);
|
||||
|
||||
return () => {
|
||||
enableBodyScroll(element);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"ember-source": "~5.5.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"webpack": "^5.97.0"
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"license": "GPL-2.0-only",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@babel/standalone": "^7.26.2",
|
||||
"@babel/standalone": "^7.26.4",
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"babel-plugin-ember-template-compilation": "^2.3.0",
|
||||
"content-tag": "^3.0.0",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"handlebars": "^4.7.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"polyfill-crypto.getrandomvalues": "^1.0.0",
|
||||
"terser": "^5.36.0"
|
||||
"terser": "^5.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
|
|
|
@ -66,6 +66,33 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
--d-border-radius: var(--space-4);
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: fit-content;
|
||||
background-color: var(--primary-low);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--d-border-radius);
|
||||
|
||||
.status-label-indicator {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-high);
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-1);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.status-label-text {
|
||||
color: var(--primary-high);
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-admin-row__overview {
|
||||
|
|
|
@ -45,15 +45,15 @@ table.web-hooks.grid {
|
|||
|
||||
// Api keys
|
||||
|
||||
table.api-keys {
|
||||
.d-admin-table.api-keys {
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
tr.revoked {
|
||||
color: var(--primary-medium);
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.d-admin-row__overview.key {
|
||||
width: 30%;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@
|
|||
padding-left: calc(
|
||||
48px - var(--pm-padding)
|
||||
); // 48px is the width of the avatar
|
||||
display: grid;
|
||||
grid-template-areas: "contents additional" "pm-map pm-map";
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
section {
|
||||
border: none;
|
||||
background: var(--primary-very-low);
|
||||
|
@ -73,8 +77,22 @@
|
|||
padding-top: var(--pm-padding);
|
||||
}
|
||||
|
||||
&__contents,
|
||||
&__additional-contents {
|
||||
padding-top: var(--pm-padding);
|
||||
}
|
||||
|
||||
&__contents {
|
||||
grid-area: contents;
|
||||
}
|
||||
|
||||
&__additional-contents {
|
||||
grid-area: additional;
|
||||
}
|
||||
|
||||
&__private-message-map {
|
||||
padding-bottom: var(--pm-padding);
|
||||
grid-area: pm-map;
|
||||
padding: 0.5em var(--pm-padding) var(--pm-padding);
|
||||
}
|
||||
|
||||
.participants {
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.--updating .d-icon-sync {
|
||||
&.--updating .d-icon-arrows-rotate {
|
||||
animation: rotate 3s linear infinite;
|
||||
margin-right: 0.45em;
|
||||
@keyframes rotate {
|
||||
|
|
|
@ -857,6 +857,7 @@ class TopicsController < ApplicationController
|
|||
params.permit(:participants)
|
||||
params.permit(:chronological_order)
|
||||
params.permit(:archetype)
|
||||
params.permit(:freeze_original)
|
||||
|
||||
raise Discourse::InvalidAccess if params[:archetype] == "private_message" && !guardian.is_staff?
|
||||
|
||||
|
@ -869,6 +870,7 @@ class TopicsController < ApplicationController
|
|||
args = {}
|
||||
args[:destination_topic_id] = destination_topic_id.to_i
|
||||
args[:chronological_order] = params[:chronological_order] == "true"
|
||||
args[:freeze_original] = params[:freeze_original] == "true"
|
||||
|
||||
if params[:archetype].present?
|
||||
args[:archetype] = params[:archetype]
|
||||
|
@ -891,6 +893,7 @@ class TopicsController < ApplicationController
|
|||
params.permit(:participants)
|
||||
params.permit(:chronological_order)
|
||||
params.permit(:archetype)
|
||||
params.permit(:freeze_original)
|
||||
|
||||
topic = Topic.with_deleted.find_by(id: topic_id)
|
||||
guardian.ensure_can_move_posts!(topic)
|
||||
|
@ -1399,6 +1402,7 @@ class TopicsController < ApplicationController
|
|||
].present?
|
||||
args[:tags] = params[:tags] if params[:tags].present?
|
||||
args[:chronological_order] = params[:chronological_order] == "true"
|
||||
args[:freeze_original] = true if params[:freeze_original] == "true"
|
||||
|
||||
if params[:archetype].present?
|
||||
args[:archetype] = params[:archetype]
|
||||
|
|
|
@ -252,7 +252,7 @@ class Badge < ActiveRecord::Base
|
|||
def default_icon=(val)
|
||||
if self.image_upload_id.blank?
|
||||
self.icon ||= val
|
||||
self.icon = val if self.icon == "fa-certificate"
|
||||
self.icon = val if self.icon == "certificate"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -353,7 +353,7 @@ end
|
|||
# updated_at :datetime not null
|
||||
# allow_title :boolean default(FALSE), not null
|
||||
# multiple_grant :boolean default(FALSE), not null
|
||||
# icon :string default("fa-certificate")
|
||||
# icon :string default("certificate")
|
||||
# listable :boolean default(TRUE)
|
||||
# target_posts :boolean default(FALSE)
|
||||
# query :text
|
||||
|
@ -363,9 +363,9 @@ end
|
|||
# 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
|
||||
# show_in_post_header :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -6,6 +6,12 @@ class MovedPost < ActiveRecord::Base
|
|||
|
||||
belongs_to :new_topic, class_name: "Topic", foreign_key: :new_topic_id
|
||||
belongs_to :new_post, class_name: "Post", foreign_key: :new_post_id
|
||||
|
||||
# The author of the moved post
|
||||
belongs_to :posting_user, class_name: "User", foreign_key: :post_user_id
|
||||
|
||||
# The user who moved the post
|
||||
belongs_to :user
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
@ -23,12 +29,16 @@ end
|
|||
# created_new_topic :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# old_topic_title :string
|
||||
# post_user_id :integer
|
||||
# user_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_moved_posts_on_new_post_id (new_post_id)
|
||||
# index_moved_posts_on_new_topic_id (new_topic_id)
|
||||
# index_moved_posts_on_old_post_id (old_post_id)
|
||||
# index_moved_posts_on_old_post_number (old_post_number)
|
||||
# index_moved_posts_on_old_topic_id (old_topic_id)
|
||||
# index_moved_posts_on_new_post_id (new_post_id)
|
||||
# index_moved_posts_on_new_topic_id (new_topic_id)
|
||||
# index_moved_posts_on_new_topic_id_and_post_user_id (new_topic_id,post_user_id)
|
||||
# index_moved_posts_on_old_post_id (old_post_id)
|
||||
# index_moved_posts_on_old_post_number (old_post_number)
|
||||
# index_moved_posts_on_old_topic_id (old_topic_id)
|
||||
#
|
||||
|
|
|
@ -11,8 +11,12 @@ class PostMover
|
|||
# freeze_original: :boolean - if true, the original topic will be frozen but not deleted and posts will be "copied" to topic
|
||||
def initialize(original_topic, user, post_ids, move_to_pm: false, options: {})
|
||||
@original_topic = original_topic
|
||||
@original_topic_title = original_topic.title
|
||||
@user = user
|
||||
@post_ids = post_ids
|
||||
# For now we store a copy of post_ids. If `freeze_original` is present, we will have new post_ids.
|
||||
# When we create the new posts, we will pluck out post_ids out of this and replace with updated ids.
|
||||
@post_ids_after_move = post_ids
|
||||
@move_to_pm = move_to_pm
|
||||
@options = options
|
||||
end
|
||||
|
@ -274,6 +278,13 @@ class PostMover
|
|||
new_post.custom_fields = post.custom_fields
|
||||
new_post.save_custom_fields
|
||||
|
||||
# When freezing original, ensure the notification generated points
|
||||
# to the newly created post, not the old OP
|
||||
if @options[:freeze_original]
|
||||
@post_ids_after_move =
|
||||
@post_ids_after_move.map { |post_id| post_id == post.id ? new_post.id : post_id }
|
||||
end
|
||||
|
||||
DiscourseEvent.trigger(:first_post_moved, new_post, post)
|
||||
DiscourseEvent.trigger(:post_moved, new_post, original_topic.id)
|
||||
|
||||
|
@ -307,6 +318,11 @@ class PostMover
|
|||
moved_post.disable_rate_limits! if @options[:freeze_original]
|
||||
moved_post.save(validate: false)
|
||||
|
||||
if moved_post.id != post.id
|
||||
@post_ids_after_move =
|
||||
@post_ids_after_move.map { |post_id| post_id == post.id ? moved_post.id : post_id }
|
||||
end
|
||||
|
||||
DiscourseEvent.trigger(:post_moved, moved_post, original_topic.id)
|
||||
|
||||
# Move any links from the post to the new topic
|
||||
|
@ -337,6 +353,7 @@ class PostMover
|
|||
old_topic_id: post.topic_id,
|
||||
old_post_id: post.id,
|
||||
old_post_number: post.post_number,
|
||||
post_user_id: post.user_id,
|
||||
new_topic_id: destination_topic.id,
|
||||
new_post_number: new_post_number,
|
||||
new_topic_title: destination_topic.title,
|
||||
|
@ -347,10 +364,12 @@ class PostMover
|
|||
metadata[:new_post_id] = new_post.id
|
||||
metadata[:now] = Time.zone.now
|
||||
metadata[:created_new_topic] = @creating_new_topic
|
||||
metadata[:old_topic_title] = @original_topic_title
|
||||
metadata[:user_id] = @user.id
|
||||
|
||||
DB.exec(<<~SQL, metadata)
|
||||
INSERT INTO moved_posts(old_topic_id, old_post_id, old_post_number, new_topic_id, new_topic_title, new_post_id, new_post_number, created_new_topic, created_at, updated_at)
|
||||
VALUES (:old_topic_id, :old_post_id, :old_post_number, :new_topic_id, :new_topic_title, :new_post_id, :new_post_number, :created_new_topic, :now, :now)
|
||||
INSERT INTO moved_posts(old_topic_id, old_topic_title, old_post_id, old_post_number, post_user_id, user_id, new_topic_id, new_topic_title, new_post_id, new_post_number, created_new_topic, created_at, updated_at)
|
||||
VALUES (:old_topic_id, :old_topic_title, :old_post_id, :old_post_number, :post_user_id, :user_id, :new_topic_id, :new_topic_title, :new_post_id, :new_post_number, :created_new_topic, :now, :now)
|
||||
SQL
|
||||
end
|
||||
|
||||
|
@ -703,7 +722,7 @@ class PostMover
|
|||
def enqueue_jobs(topic)
|
||||
@post_creator.enqueue_jobs if @post_creator
|
||||
|
||||
Jobs.enqueue(:notify_moved_posts, post_ids: post_ids, moved_by_id: user.id)
|
||||
Jobs.enqueue(:notify_moved_posts, post_ids: @post_ids_after_move, moved_by_id: user.id)
|
||||
|
||||
Jobs.enqueue(:delete_inaccessible_notifications, topic_id: topic.id)
|
||||
end
|
||||
|
|
|
@ -270,8 +270,15 @@ class Reviewable < ActiveRecord::Base
|
|||
|
||||
def actions_for(guardian, args = nil)
|
||||
args ||= {}
|
||||
built_actions =
|
||||
Actions.new(self, guardian).tap { |actions| build_actions(actions, guardian, args) }
|
||||
|
||||
Actions.new(self, guardian).tap { |actions| build_actions(actions, guardian, args) }
|
||||
# Empty bundles can cause big issues on the client side, so we remove them
|
||||
# here. It's not valid anyway to have a bundle with no actions, but you can
|
||||
# add a bundle via actions.add_bundle and then not add any actions to it.
|
||||
built_actions.bundles.reject!(&:empty?)
|
||||
|
||||
built_actions
|
||||
end
|
||||
|
||||
def editable_for(guardian, args = nil)
|
||||
|
|
|
@ -1298,6 +1298,9 @@ class Topic < ActiveRecord::Base
|
|||
moved_by,
|
||||
post_ids,
|
||||
move_to_pm: opts[:archetype].present? && opts[:archetype] == "private_message",
|
||||
options: {
|
||||
freeze_original: opts[:freeze_original],
|
||||
},
|
||||
)
|
||||
|
||||
if opts[:destination_topic_id]
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
<%= t 'admin_login.safe_mode' %>
|
||||
</label>
|
||||
<br/>
|
||||
<%= submit_tag t('admin_login.submit_button'), class: "btn btn-primary" %>
|
||||
<%= button_tag t('admin_login.submit_button'), class: "btn btn-primary", type: "submit" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<form method="post">
|
||||
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
||||
<%= button_tag(type: "submit", class: "btn btn-primary") do %>
|
||||
<%= SvgSprite.raw_svg('fa-plug') %><%= t 'login.omniauth_confirm_button' %>
|
||||
<%= SvgSprite.raw_svg('plug') %><%= t 'login.omniauth_confirm_button' %>
|
||||
<% end %>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -2479,6 +2479,9 @@ ar:
|
|||
optional: اختياري
|
||||
errors_summary_title: "يحتوي هذا النموذج على أخطاء:"
|
||||
dirty_form: "لم ترسل التغييرات! هل تريد بالتأكيد المغادرة؟"
|
||||
select:
|
||||
select_placeholder: "تحديد…"
|
||||
none_placeholder: "لا يوجد"
|
||||
errors:
|
||||
required: "مطلوب"
|
||||
invalid_url: "يجب أن يكون عنوان URL صالحًا"
|
||||
|
@ -7731,6 +7734,8 @@ ar:
|
|||
title: "تحميل الصور"
|
||||
selectable_avatars:
|
||||
title: "قائمة الصور الرمزية التي يمكن للمستخدمين الاختيار منها"
|
||||
table_column_heading:
|
||||
status: "الحالة"
|
||||
categories:
|
||||
all_results: "الكل"
|
||||
required: "مطلوب"
|
||||
|
|
|
@ -2161,6 +2161,8 @@ be:
|
|||
label: "Загрузіць"
|
||||
selectable_avatars:
|
||||
title: "Спіс аватараў карыстальнікі могуць выбраць"
|
||||
table_column_heading:
|
||||
status: "Статус"
|
||||
categories:
|
||||
all_results: "усе"
|
||||
required: "абавязковыя"
|
||||
|
|
|
@ -1767,6 +1767,8 @@ bg:
|
|||
form_kit:
|
||||
reset: Нулиране
|
||||
optional: по избор
|
||||
select:
|
||||
none_placeholder: "Без"
|
||||
errors:
|
||||
required: "Задъжителни"
|
||||
close: "Затвори"
|
||||
|
@ -4285,6 +4287,8 @@ bg:
|
|||
uploaded_image_list:
|
||||
upload:
|
||||
label: "Качване"
|
||||
table_column_heading:
|
||||
status: "Статус"
|
||||
categories:
|
||||
all_results: "Всички"
|
||||
required: "Задъжителни"
|
||||
|
|
|
@ -1443,6 +1443,8 @@ bs_BA:
|
|||
form_kit:
|
||||
reset: Resetovati
|
||||
optional: opciono
|
||||
select:
|
||||
none_placeholder: "Ništa"
|
||||
errors:
|
||||
required: "Required"
|
||||
close: "Zatvori"
|
||||
|
@ -4310,6 +4312,8 @@ bs_BA:
|
|||
label: "Učitavanje"
|
||||
selectable_avatars:
|
||||
title: "Popis avatara koje korisnici mogu izabrati"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "All"
|
||||
required: "Required"
|
||||
|
|
|
@ -1389,6 +1389,8 @@ ca:
|
|||
form_kit:
|
||||
reset: Restableix
|
||||
optional: opcional
|
||||
select:
|
||||
none_placeholder: "Cap"
|
||||
errors:
|
||||
required: "Necessari"
|
||||
close: "Tanca"
|
||||
|
@ -4166,6 +4168,8 @@ ca:
|
|||
label: "Carrega"
|
||||
selectable_avatars:
|
||||
title: "Llista d'avatars que els usuaris poden triar"
|
||||
table_column_heading:
|
||||
status: "Estat"
|
||||
categories:
|
||||
all_results: "Tot"
|
||||
required: "Necessari"
|
||||
|
|
|
@ -2164,6 +2164,9 @@ cs:
|
|||
form_kit:
|
||||
reset: obnovit výchozí
|
||||
optional: volitelné
|
||||
select:
|
||||
select_placeholder: "Vybrat…"
|
||||
none_placeholder: "Žádná"
|
||||
errors:
|
||||
required: "Nezbytnosti"
|
||||
too_low: "Musí být alespoň %{count}"
|
||||
|
@ -6453,6 +6456,8 @@ cs:
|
|||
uploaded_image_list:
|
||||
upload:
|
||||
label: "Nahrát"
|
||||
table_column_heading:
|
||||
status: "Stav"
|
||||
categories:
|
||||
all_results: "Všechny"
|
||||
required: "Nezbytnosti"
|
||||
|
|
|
@ -1694,6 +1694,9 @@ da:
|
|||
form_kit:
|
||||
reset: Nulstil
|
||||
optional: valgfri
|
||||
select:
|
||||
select_placeholder: "Vælg…"
|
||||
none_placeholder: "Ingen"
|
||||
errors:
|
||||
required: "Obligatoriske"
|
||||
invalid_url: "Skal være en gyldig URL"
|
||||
|
@ -5280,6 +5283,8 @@ da:
|
|||
label: "Overfør"
|
||||
selectable_avatars:
|
||||
title: "Liste over avatarer, som brugere kan vælge imellem"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "Alle"
|
||||
required: "Obligatoriske"
|
||||
|
|
|
@ -2051,6 +2051,9 @@ de:
|
|||
optional: optional
|
||||
errors_summary_title: "Dieses Formular enthält Fehler:"
|
||||
dirty_form: "Du hast deine Änderungen nicht gespeichert! Bist du sicher, dass du die Seite verlassen möchtest?"
|
||||
select:
|
||||
select_placeholder: "Auswählen …"
|
||||
none_placeholder: "Keiner"
|
||||
errors:
|
||||
required: "Erforderlich"
|
||||
invalid_url: "Muss eine gültige URL sein"
|
||||
|
@ -6687,6 +6690,8 @@ de:
|
|||
title: "Bilder hochladen"
|
||||
selectable_avatars:
|
||||
title: "Liste von Avataren, aus der Benutzer wählen können."
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "Alle"
|
||||
required: "Erforderlich"
|
||||
|
|
|
@ -1747,6 +1747,9 @@ el:
|
|||
optional: προεραιτικό
|
||||
errors_summary_title: "Αυτή η φόρμα περιέχει σφάλματα:"
|
||||
dirty_form: "Δεν έχετε υποβάλει τις αλλαγές σας! Είστε σίγουροι ότι θέλετε να αποχωρήσετε;"
|
||||
select:
|
||||
select_placeholder: "Επιλογή…"
|
||||
none_placeholder: "Κανένα"
|
||||
errors:
|
||||
required: "Απαιτείται"
|
||||
invalid_url: "Πρέπει να είναι μια έγκυρη διεύθυνση URL"
|
||||
|
@ -5155,6 +5158,8 @@ el:
|
|||
label: "Μεταφόρτωση"
|
||||
selectable_avatars:
|
||||
title: "Λίστα των avatars που μπορούν να επιλέξουν οι χρήστες"
|
||||
table_column_heading:
|
||||
status: "Κατάσταση"
|
||||
categories:
|
||||
all_results: "Όλα"
|
||||
required: "Απαιτείται"
|
||||
|
|
|
@ -2203,6 +2203,9 @@ en:
|
|||
optional: optional
|
||||
errors_summary_title: "This form contains errors:"
|
||||
dirty_form: "You didn't submit your changes! Are you sure you want to leave?"
|
||||
select:
|
||||
select_placeholder: "Select…"
|
||||
none_placeholder: "None"
|
||||
errors:
|
||||
required: "Required"
|
||||
invalid_url: "Must be a valid URL"
|
||||
|
@ -7095,6 +7098,8 @@ en:
|
|||
title: "Upload images"
|
||||
selectable_avatars:
|
||||
title: "List of avatars users can choose from"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "All"
|
||||
required: "Required"
|
||||
|
|
|
@ -2050,6 +2050,9 @@ es:
|
|||
optional: opcional
|
||||
errors_summary_title: "Este formulario contiene errores:"
|
||||
dirty_form: "¡No has enviado tus cambios! ¿Seguro que quieres salir?"
|
||||
select:
|
||||
select_placeholder: "Seleccionar…"
|
||||
none_placeholder: "Ninguno"
|
||||
errors:
|
||||
required: "Obligatorio"
|
||||
invalid_url: "Debe ser una URL válida"
|
||||
|
@ -6642,6 +6645,8 @@ es:
|
|||
title: "Subir imágenes"
|
||||
selectable_avatars:
|
||||
title: "Lista de avatares que los usuarios pueden escoger"
|
||||
table_column_heading:
|
||||
status: "Estado"
|
||||
categories:
|
||||
all_results: "Todo"
|
||||
required: "Obligatorio"
|
||||
|
|
|
@ -1253,6 +1253,9 @@ et:
|
|||
form_kit:
|
||||
reset: Lähtesta
|
||||
optional: valikuline
|
||||
select:
|
||||
select_placeholder: "Vali..."
|
||||
none_placeholder: "Pole"
|
||||
errors:
|
||||
required: "Nõutud"
|
||||
close: "Sulge"
|
||||
|
@ -3620,6 +3623,8 @@ et:
|
|||
uploaded_image_list:
|
||||
upload:
|
||||
label: "Lae üles"
|
||||
table_column_heading:
|
||||
status: "Staatus"
|
||||
categories:
|
||||
all_results: "Kõik"
|
||||
required: "Nõutud"
|
||||
|
|
|
@ -1681,6 +1681,9 @@ fa_IR:
|
|||
form_kit:
|
||||
reset: بازنشانی
|
||||
optional: اختیاری
|
||||
select:
|
||||
select_placeholder: "انتخاب…"
|
||||
none_placeholder: "هیچ کدام"
|
||||
errors:
|
||||
required: "مورد نیاز"
|
||||
close: "بستن"
|
||||
|
@ -4685,6 +4688,8 @@ fa_IR:
|
|||
label: "ویرایش فهرست"
|
||||
upload:
|
||||
label: "بارگذاری"
|
||||
table_column_heading:
|
||||
status: "وضعیت"
|
||||
categories:
|
||||
all_results: "همه"
|
||||
required: "مورد نیاز"
|
||||
|
|
|
@ -2050,6 +2050,9 @@ fi:
|
|||
optional: valinnainen
|
||||
errors_summary_title: "Tämä lomake sisältää virheitä:"
|
||||
dirty_form: "Et lähettänyt muutoksiasi! Oletko varma, että haluat poistua?"
|
||||
select:
|
||||
select_placeholder: "Valitse…"
|
||||
none_placeholder: "Ei valittu"
|
||||
errors:
|
||||
required: "Pakollinen"
|
||||
invalid_url: "Täytyy olla kelvollinen URL"
|
||||
|
@ -6643,6 +6646,8 @@ fi:
|
|||
title: "Lataa kuvia"
|
||||
selectable_avatars:
|
||||
title: "Avatarit, joista käyttäjä voi valita"
|
||||
table_column_heading:
|
||||
status: "Tila"
|
||||
categories:
|
||||
all_results: "Kaikki"
|
||||
required: "Pakolliset"
|
||||
|
|
|
@ -2050,6 +2050,9 @@ fr:
|
|||
optional: facultatif
|
||||
errors_summary_title: "Ce formulaire contient des erreurs :"
|
||||
dirty_form: "Vous n'avez pas soumis vos modifications ! Voulez-vous vraiment partir ?"
|
||||
select:
|
||||
select_placeholder: "Sélectionnez…"
|
||||
none_placeholder: "Jamais"
|
||||
errors:
|
||||
required: "Requis"
|
||||
invalid_url: "Doit être une URL valide"
|
||||
|
@ -6643,6 +6646,8 @@ fr:
|
|||
title: "Téléverser des images"
|
||||
selectable_avatars:
|
||||
title: "Liste d'avatars que les utilisateurs peuvent choisir"
|
||||
table_column_heading:
|
||||
status: "Statut"
|
||||
categories:
|
||||
all_results: "Toutes"
|
||||
required: "Requis"
|
||||
|
|
|
@ -1534,6 +1534,8 @@ gl:
|
|||
form_kit:
|
||||
reset: Restabelecer
|
||||
optional: opcional
|
||||
select:
|
||||
none_placeholder: "Ningunha"
|
||||
errors:
|
||||
required: "Obrigatorio"
|
||||
close: "Pechar"
|
||||
|
@ -4733,6 +4735,8 @@ gl:
|
|||
label: "Cargar"
|
||||
selectable_avatars:
|
||||
title: "Listaxe de avatares que os usuarios poden elixir"
|
||||
table_column_heading:
|
||||
status: "Estado"
|
||||
categories:
|
||||
all_results: "Todo"
|
||||
required: "Obrigatorio"
|
||||
|
|
|
@ -2275,6 +2275,9 @@ he:
|
|||
optional: רשות
|
||||
errors_summary_title: "הטופס הזה מכיל שגיאות:"
|
||||
dirty_form: "לא הגשת את השינויים שלך! לצאת?"
|
||||
select:
|
||||
select_placeholder: "בחירה…"
|
||||
none_placeholder: "ללא"
|
||||
errors:
|
||||
required: "נדרש"
|
||||
invalid_url: "חייבת להיות כתובת תקפה"
|
||||
|
@ -3954,6 +3957,7 @@ he:
|
|||
other: "מוצגות %{count} תגובות אל"
|
||||
in_reply_to: "טעינת הפוסט ההורה"
|
||||
view_all_posts: "הצגת כל הפוסטים"
|
||||
badge_granted_tooltip: "ל־%{username} יש זכאות לעיטור ‚%{badge_name}’ על הפוסט!"
|
||||
errors:
|
||||
create: "אירעה שגיאה ביצירת הפוסט שלך. נא לנסות שוב, עמך הסליחה."
|
||||
edit: "אירעה שגיאה בעריכת הפוסט שלך. נא לנסות שוב, עמך הסליחה."
|
||||
|
@ -7248,6 +7252,8 @@ he:
|
|||
title: "העלאת תמונות"
|
||||
selectable_avatars:
|
||||
title: "רשימת תמונות ייצוגיות מהן המשתמשים יכולים לבחור"
|
||||
table_column_heading:
|
||||
status: "מצב"
|
||||
categories:
|
||||
all_results: "הכול"
|
||||
required: "נדרש"
|
||||
|
@ -7351,10 +7357,12 @@ he:
|
|||
no_user_badges: "למשתמש %{name} לא הוענקו עיטורים."
|
||||
no_badges: אין עיטורים שניתן להעניק.
|
||||
none_selected: "נא לבחור עיטור כדי להתחיל"
|
||||
usage_heading: שימוש
|
||||
allow_title: לאפשר להשתמש בעיטור ככותרת
|
||||
multiple_grant: יכולים להינתן מספר פעמים
|
||||
visibility_heading: חשיפה
|
||||
listable: הצגת עיטורים בעמוד העיטורים הפומבי
|
||||
show_in_post_header: הצגת עיטור על הפוסט שבגינו הוענק
|
||||
enabled: פעיל
|
||||
disabled: מושבת
|
||||
icon: סמליל
|
||||
|
|
|
@ -1923,6 +1923,9 @@ hr:
|
|||
form_kit:
|
||||
reset: Resetirati
|
||||
optional: neobvezno
|
||||
select:
|
||||
select_placeholder: "Odaberite…"
|
||||
none_placeholder: "Ništa"
|
||||
errors:
|
||||
required: "Potrebno"
|
||||
close: "Zatvori"
|
||||
|
@ -5885,6 +5888,8 @@ hr:
|
|||
label: "Učitaj"
|
||||
selectable_avatars:
|
||||
title: "Popis avatara koji korisnici mogu birati"
|
||||
table_column_heading:
|
||||
status: "Statust"
|
||||
categories:
|
||||
all_results: "Sve"
|
||||
required: "Potrebno"
|
||||
|
|
|
@ -2051,6 +2051,9 @@ hu:
|
|||
optional: nem kötelező
|
||||
errors_summary_title: "Ez az űrlap hibákat tartalmaz:"
|
||||
dirty_form: "A változások nem lettek mentve! Biztos hogy el akarja hagyni a területet?"
|
||||
select:
|
||||
select_placeholder: "Kiválaszt..."
|
||||
none_placeholder: "Egyik sem"
|
||||
errors:
|
||||
required: "Kötelező"
|
||||
invalid_url: "Érvényes URL-nek kell lennie"
|
||||
|
@ -6655,6 +6658,8 @@ hu:
|
|||
title: "Kép(ek) feltöltése"
|
||||
selectable_avatars:
|
||||
title: "A felhasználó által választható profilképek listája"
|
||||
table_column_heading:
|
||||
status: "Állapot"
|
||||
categories:
|
||||
all_results: "Mind"
|
||||
required: "Kötelező"
|
||||
|
|
|
@ -1976,6 +1976,9 @@ hy:
|
|||
form_kit:
|
||||
reset: Զրոյացնել
|
||||
optional: ընտրովի
|
||||
select:
|
||||
select_placeholder: "Ընտրել..."
|
||||
none_placeholder: "Ոչ մի"
|
||||
errors:
|
||||
required: "Պարտադիր"
|
||||
too_short:
|
||||
|
@ -2675,6 +2678,7 @@ hy:
|
|||
read_more_in_category: "Ցանկանո՞ւմ եք կարդալ ավելին: Դիտեք այլ թեմաներ՝ %{categoryLink}-ում կամ <a href='%{latestLink}'>դիտեք վերջին թեմաները</a>"
|
||||
read_more: "Ցանկանու՞մ եք կարդալ ավելին: <a href='%{categoryLink}'>Դիտեք բոլոր կատեգորիաները</a> կամ <a href='%{latestLink}'>դիտեք վերջին թեմաները</a>:"
|
||||
unread_indicator: "Ոչ մի անդամ դեռ չի կարդացել այս թեմայի մինչև վերջին գրառումը: "
|
||||
created_at: "Ստեղծվել է՝ %{date}"
|
||||
bumped_at: "Վերջինը՝ %{date}"
|
||||
browse_all_categories_latest: "<a href='%{basePath}/categories'>Թերթեք բոլոր կատեգորիաները</a> կամ <a href='%{basePath}/latest'>դիտեք վերջին թեմաները</a>."
|
||||
browse_all_categories_latest_or_top: "<a href='%{basePath}/categories'>Թերթեք բոլոր կատեգորիաները</a>, <a href='%{basePath}/latest'>դիտեք վերջին թեմաները</a> կամ ամենահայտնիները:"
|
||||
|
@ -2935,7 +2939,7 @@ hy:
|
|||
publishing_settings: "Հրապարակման Կարգավորումներ"
|
||||
change_owner:
|
||||
title: "Փոխել Սեփականատիրոջը"
|
||||
action: "Փոխել Սեփականատիրոջը"
|
||||
action: "փոխել սեփականության իրավունքը"
|
||||
error: "Գրառումների սեփականատիրոջը փոփոխելիս տեղի է ունեցել սխալ:"
|
||||
placeholder: "նոր սեփականատիրոջ օգտանունը"
|
||||
instructions:
|
||||
|
@ -3061,6 +3065,7 @@ hy:
|
|||
rebake: "Վերակառուցել HTML-ը"
|
||||
publish_page: "Էջի Հրատարակում"
|
||||
unhide: "Դարձնել Տեսանելի"
|
||||
change_owner: "Փոխել Սեփականատիրոջը..."
|
||||
lock_post: "Արգելափակել Գրառումը"
|
||||
lock_post_description: "արգելել հրապարակողին խմբագրել այս գրառումը"
|
||||
unlock_post: "Արգելաբացել Գրառումը"
|
||||
|
@ -4025,8 +4030,19 @@ hy:
|
|||
groups_filter_instructions: "Համապատասխան webhook-ները կգործարկվեն միայն, եթե իրադարձությունը կապված է ընտրված խմբերի հետ: Թողեք դատարկ՝ webhook-ները բոլոր խմբերի համար գործարկելու համար:"
|
||||
groups_filter: "Գործարկվող Խմբերը"
|
||||
delete_confirm: "Ջնջե՞լ այս webhook-ը:"
|
||||
topic_event:
|
||||
topic_created: "Թեման ստեղծված է"
|
||||
post_event:
|
||||
post_created: "Գրառումը ստեղծված է"
|
||||
group_event:
|
||||
group_created: "Խումբը ստեղծված է"
|
||||
tag_event:
|
||||
tag_created: "Պիտակը ստեղծված է"
|
||||
category_event:
|
||||
category_created: "Կատեգորիան ստեղծված է"
|
||||
user_event:
|
||||
user_confirmed_email: "Օգտագործողը հաստատել է էլ. փոստը"
|
||||
user_created: "Օգտագործողը ստեղծված է"
|
||||
delivery_status:
|
||||
title: "Ուղարկման Կարգավիճակը"
|
||||
inactive: "Ոչ ակտիվ"
|
||||
|
@ -5042,6 +5058,8 @@ hy:
|
|||
label: "Վերբեռնել"
|
||||
selectable_avatars:
|
||||
title: "Անձնապատկերների ցանկը, որոնցից օգտատերերը կարող են ընտրություն կատարել"
|
||||
table_column_heading:
|
||||
status: "Ստատուս"
|
||||
categories:
|
||||
all_results: "Ամբողջը"
|
||||
required: "Պարտադիր"
|
||||
|
@ -5162,6 +5180,7 @@ hy:
|
|||
uploading: "Վերբեռնվում է…"
|
||||
name: "Անուն"
|
||||
group: "Խումբ"
|
||||
created_by: "Ստեղծել է՝"
|
||||
image: "Նկար"
|
||||
alt: "մասնավոր էմոջիի նախնական դիտում "
|
||||
delete_confirm: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել :%{name}: էմոջին:"
|
||||
|
|
|
@ -1453,6 +1453,8 @@ id:
|
|||
form_kit:
|
||||
reset: Reset
|
||||
optional: opsional
|
||||
select:
|
||||
none_placeholder: "Tidak ada"
|
||||
close: "Tutup"
|
||||
assets_changed_confirm: "Situs ini baru saja menerima pembaruan perangkat lunak. Dapatkan versi terbaru sekarang?"
|
||||
logout: "Anda telah keluar."
|
||||
|
@ -2777,6 +2779,8 @@ id:
|
|||
uploaded_image_list:
|
||||
upload:
|
||||
label: "Unggah"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "Semua"
|
||||
users: "Pengguna"
|
||||
|
|
|
@ -2050,6 +2050,9 @@ it:
|
|||
optional: facoltativo
|
||||
errors_summary_title: "Questo modulo contiene errori:"
|
||||
dirty_form: "Non hai caricato le modifiche! Vuoi davvero uscire?"
|
||||
select:
|
||||
select_placeholder: "Seleziona…"
|
||||
none_placeholder: "Nessuna"
|
||||
errors:
|
||||
required: "Obbligatorio"
|
||||
invalid_url: "L'URL deve essere valido"
|
||||
|
@ -6644,6 +6647,8 @@ it:
|
|||
title: "Carica immagini"
|
||||
selectable_avatars:
|
||||
title: "Elenco degli Avatar tra cui l'utente può scegliere"
|
||||
table_column_heading:
|
||||
status: "Stato"
|
||||
categories:
|
||||
all_results: "Tutte"
|
||||
required: "Obbligatorie"
|
||||
|
|
|
@ -1939,6 +1939,9 @@ ja:
|
|||
optional: オプション
|
||||
errors_summary_title: "このフォームにはエラーがあります:"
|
||||
dirty_form: "変更を送信しませんでした!終了してもよろしいですか?"
|
||||
select:
|
||||
select_placeholder: "選択…"
|
||||
none_placeholder: "なし"
|
||||
errors:
|
||||
required: "必須"
|
||||
invalid_url: "有効な URL である必要があります"
|
||||
|
@ -6366,6 +6369,8 @@ ja:
|
|||
title: "画像のアップロード"
|
||||
selectable_avatars:
|
||||
title: "ユーザーが選択できるアバターのリスト"
|
||||
table_column_heading:
|
||||
status: "ステータス"
|
||||
categories:
|
||||
all_results: "すべて"
|
||||
required: "必須"
|
||||
|
|
|
@ -1613,6 +1613,8 @@ ko:
|
|||
form_kit:
|
||||
reset: 리셋
|
||||
optional: 선택 사항
|
||||
select:
|
||||
none_placeholder: "없음"
|
||||
errors:
|
||||
required: "필수"
|
||||
too_short:
|
||||
|
@ -5009,6 +5011,8 @@ ko:
|
|||
label: "업로드"
|
||||
selectable_avatars:
|
||||
title: "사용자가 선택할 수있는 아바타 목록"
|
||||
table_column_heading:
|
||||
status: "상태"
|
||||
categories:
|
||||
all_results: "전체"
|
||||
required: "필수"
|
||||
|
|
|
@ -1676,6 +1676,8 @@ lt:
|
|||
form_kit:
|
||||
reset: Atstatyti
|
||||
optional: pasirinktinai
|
||||
select:
|
||||
none_placeholder: "Nieko"
|
||||
errors:
|
||||
required: "Privalomi"
|
||||
close: "Uždaryti"
|
||||
|
@ -4926,6 +4928,8 @@ lt:
|
|||
label: "Įkelti"
|
||||
selectable_avatars:
|
||||
title: "Avatarų, kuriuos vartotojai gali pasirinkti, sąrašas"
|
||||
table_column_heading:
|
||||
status: "Statusas"
|
||||
categories:
|
||||
all_results: "Visi"
|
||||
required: "Privalomi"
|
||||
|
|
|
@ -1462,6 +1462,8 @@ lv:
|
|||
form_kit:
|
||||
reset: Atlikt
|
||||
optional: pēc izvēles
|
||||
select:
|
||||
none_placeholder: "Nav"
|
||||
errors:
|
||||
required: "Nepieciešams"
|
||||
close: "Aizvērt"
|
||||
|
@ -3677,6 +3679,8 @@ lv:
|
|||
uploaded_image_list:
|
||||
upload:
|
||||
label: "Augšupielādēt"
|
||||
table_column_heading:
|
||||
status: "Statuss"
|
||||
categories:
|
||||
all_results: "Viss"
|
||||
required: "Nepieciešams"
|
||||
|
|
|
@ -1979,6 +1979,9 @@ nb_NO:
|
|||
optional: valgfritt
|
||||
errors_summary_title: "Dette skjemaet inneholder feil:"
|
||||
dirty_form: "Du har ikke sendt inn endringene dine! Er du sikker på at du vil dra?"
|
||||
select:
|
||||
select_placeholder: "Velg…"
|
||||
none_placeholder: "Ingen"
|
||||
errors:
|
||||
required: "Påkrevd"
|
||||
invalid_url: "Må være en gyldig URL"
|
||||
|
@ -5544,6 +5547,8 @@ nb_NO:
|
|||
label: "Last opp"
|
||||
selectable_avatars:
|
||||
title: "Liste over avatarer brukere kan velge mellom"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "Alle"
|
||||
required: "Påkrevd"
|
||||
|
|
|
@ -2050,6 +2050,9 @@ nl:
|
|||
optional: optioneel
|
||||
errors_summary_title: "Dit formulier bevat fouten:"
|
||||
dirty_form: "Je hebt je wijzigingen niet verzonden! Weet je zeker dat je wilt verlaten?"
|
||||
select:
|
||||
select_placeholder: "Selecteren…"
|
||||
none_placeholder: "Geen"
|
||||
errors:
|
||||
required: "Vereist"
|
||||
invalid_url: "Moet een geldige URL zijn"
|
||||
|
@ -6644,6 +6647,8 @@ nl:
|
|||
title: "Afbeeldingen uploaden"
|
||||
selectable_avatars:
|
||||
title: "Lijst van avatars waaruit gebruikers kunnen kiezen"
|
||||
table_column_heading:
|
||||
status: "Status"
|
||||
categories:
|
||||
all_results: "Alle"
|
||||
required: "Vereist"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue