mirror of
https://github.com/discourse/discourse.git
synced 2025-10-03 17:21:20 +08:00
Merge branch 'main' into feature/bump-wiki-docs-topics-op-edit
This commit is contained in:
commit
862284ca08
76 changed files with 663 additions and 780 deletions
|
@ -312,7 +312,7 @@ GEM
|
|||
mustache (1.1.1)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.10)
|
||||
net-imap (0.5.11)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
|
@ -1041,7 +1041,7 @@ CHECKSUMS
|
|||
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
|
||||
mustache (1.1.1) sha256=90891fdd50b53919ca334c8c1031eada1215e78d226d5795e523d6123a2717d0
|
||||
net-http (0.6.0) sha256=9621b20c137898af9d890556848c93603716cab516dc2c89b01a38b894e259fb
|
||||
net-imap (0.5.10) sha256=f84d206a296bff48a3a10507567fc38b050d2a40c92ea0d448164f64e60d6205
|
||||
net-imap (0.5.11) sha256=761143adaf153208c49e8e9d52e1b18ce17a24dc4f7f4f367480b5b98010f221
|
||||
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||
|
|
|
@ -9,7 +9,7 @@ const AdminSearchFilters = <template>
|
|||
<span class={{concat "admin-search__filter --" type}}>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"btn-small admin-search__filter-item"
|
||||
"btn-default btn-small admin-search__filter-item"
|
||||
(if (get @typeFilters type) "is-active")
|
||||
}}
|
||||
@translatedLabel={{i18n
|
||||
|
|
|
@ -32,16 +32,16 @@ export default class ColorInput extends Component {
|
|||
}
|
||||
|
||||
@computed("hexValueWithFallback")
|
||||
get normalizedValue() {
|
||||
return this.normalize(this.hexValueWithFallback);
|
||||
get valueForPicker() {
|
||||
return this.normalize(this.hexValueWithFallback, { forPicker: true });
|
||||
}
|
||||
|
||||
normalize(color) {
|
||||
normalize(color, { forPicker = false } = {}) {
|
||||
if (this._valid(color)) {
|
||||
if (!color.startsWith("#")) {
|
||||
color = "#" + color;
|
||||
}
|
||||
if (color.length === 4) {
|
||||
if (color.length === 4 && (!this.skipNormalize || forPicker)) {
|
||||
color =
|
||||
"#" +
|
||||
color
|
||||
|
@ -109,8 +109,8 @@ export default class ColorInput extends Component {
|
|||
<input
|
||||
class="picker"
|
||||
type="color"
|
||||
value={{this.normalizedValue}}
|
||||
title={{this.normalizedValue}}
|
||||
value={{this.valueForPicker}}
|
||||
title={{this.valueForPicker}}
|
||||
{{on "input" this.onPickerInput}}
|
||||
aria-labelledby={{this.ariaLabelledby}}
|
||||
/>
|
||||
|
|
|
@ -39,12 +39,12 @@ export default class InlineEditCheckbox extends Component {
|
|||
<DButton
|
||||
@action={{fn @action this.value}}
|
||||
@icon="check"
|
||||
class="btn-primary btn-small submit-edit"
|
||||
class="btn-success btn-small submit-edit"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.reset}}
|
||||
@icon="xmark"
|
||||
class="btn-small cancel-edit"
|
||||
class="btn-danger btn-small cancel-edit"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -347,7 +347,7 @@ export default class InstallThemeModal extends Component {
|
|||
<span>{{i18n "admin.customize.theme.installed"}}</span>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn-small"
|
||||
class="btn-default btn-small"
|
||||
@label="admin.customize.theme.install"
|
||||
@disabled={{this.installDisabled}}
|
||||
@icon="upload"
|
||||
|
|
|
@ -18,6 +18,7 @@ export default class SchemaSettingTypeTags extends SchemaSettingTypeModels {
|
|||
@tags={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
@options={{this.tagChooserOption}}
|
||||
@everyTag={{@spec.every_tag}}
|
||||
class={{if this.validationErrorMessage "--invalid"}}
|
||||
/>
|
||||
|
||||
|
|
|
@ -82,7 +82,6 @@ export default class SimpleList extends Component {
|
|||
<div class="simple-list value-list" ...attributes>
|
||||
{{#if this.collection}}
|
||||
<div class="values">
|
||||
{{this.collection.length}}
|
||||
{{#each this.collection as |value index|}}
|
||||
<div data-index={{index}} class="value">
|
||||
<DButton
|
||||
|
|
|
@ -53,7 +53,7 @@ export default RouteTemplate(
|
|||
<DButton
|
||||
@action={{@controller.cancelEditingName}}
|
||||
@icon="xmark"
|
||||
class="btn-small cancel-edit"
|
||||
class="btn-default btn-small cancel-edit"
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
|
|
|
@ -3,7 +3,6 @@ import RouteTemplate from "ember-route-template";
|
|||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import FlatButton from "discourse/components/flat-button";
|
||||
import TextField from "discourse/components/text-field";
|
||||
import categoryLink from "discourse/helpers/category-link";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
|
@ -51,10 +50,11 @@ export default RouteTemplate(
|
|||
}}
|
||||
>
|
||||
<td class="d-table__cell --overview">
|
||||
<FlatButton
|
||||
<DButton
|
||||
@title="admin.permalink.copy_to_clipboard"
|
||||
@icon="far-clipboard"
|
||||
@action={{fn @controller.copyUrl pl}}
|
||||
class="btn-flat"
|
||||
/>
|
||||
<span
|
||||
id="admin-permalink-{{pl.id}}"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable ember/no-classic-components */
|
||||
import Component from "@ember/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { computed } from "@ember/object";
|
||||
import { compare } from "@ember/utils";
|
||||
import { attributeBindings } from "@ember-decorators/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
@ -14,9 +13,10 @@ export default class AnonymousTopicFooterButtons extends Component {
|
|||
elementId = "topic-footer-buttons";
|
||||
role = "region";
|
||||
|
||||
@getTopicFooterButtons() allButtons;
|
||||
get allButtons() {
|
||||
return getTopicFooterButtons(this);
|
||||
}
|
||||
|
||||
@computed("allButtons.[]")
|
||||
get buttons() {
|
||||
return (
|
||||
this.allButtons
|
||||
|
|
|
@ -11,7 +11,6 @@ import { and } from "truth-helpers";
|
|||
import BookmarkActionsDropdown from "discourse/components/bookmark-actions-dropdown";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import FlatButton from "discourse/components/flat-button";
|
||||
import LoadMore from "discourse/components/load-more";
|
||||
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
|
@ -214,9 +213,9 @@ export default class BookmarkList extends Component {
|
|||
<PluginOutlet @name="bookmark-list-table-header">
|
||||
{{#if this.bulkSelectEnabled}}
|
||||
<th class="bulk-select topic-list-data">
|
||||
<FlatButton
|
||||
<DButton
|
||||
@action={{this.toggleBulkSelect}}
|
||||
@class="bulk-select"
|
||||
class="bulk-select btn-flat"
|
||||
@icon="list-check"
|
||||
@title="bookmarks.bulk.toggle"
|
||||
/>
|
||||
|
@ -252,9 +251,9 @@ export default class BookmarkList extends Component {
|
|||
/>
|
||||
</span>
|
||||
{{else}}
|
||||
<FlatButton
|
||||
<DButton
|
||||
@action={{this.toggleBulkSelect}}
|
||||
@class="bulk-select"
|
||||
class="btn-flat bulk-select"
|
||||
@icon="list-check"
|
||||
@title="bookmarks.bulk.toggle"
|
||||
/>
|
||||
|
|
|
@ -32,8 +32,6 @@ export default class DButton extends Component {
|
|||
get btnType() {
|
||||
if (this.args.icon) {
|
||||
return this.computedLabel ? "btn-icon-text" : "btn-icon";
|
||||
} else if (this.computedLabel) {
|
||||
return "btn-text";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import DButton from "discourse/components/d-button";
|
|||
|
||||
export const DPageActionButton = <template>
|
||||
<DButton
|
||||
class="d-page-action-button btn-small"
|
||||
class="d-page-action-button btn-small btn-default"
|
||||
...attributes
|
||||
@action={{@action}}
|
||||
@route={{@route}}
|
||||
|
|
|
@ -129,6 +129,10 @@ export default class EditCategoryGeneral extends Component {
|
|||
updateColor(field, newColor) {
|
||||
const color = newColor.replace("#", "");
|
||||
|
||||
if (color === field.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.name === "color") {
|
||||
const whiteDiff = this.colorDifference(color, CATEGORY_TEXT_COLORS[0]);
|
||||
const blackDiff = this.colorDifference(color, CATEGORY_TEXT_COLORS[1]);
|
||||
|
@ -160,6 +164,41 @@ export default class EditCategoryGeneral extends Component {
|
|||
return rDiff + gDiff + bDiff;
|
||||
}
|
||||
|
||||
@action
|
||||
validateColor(name, color, { addError }) {
|
||||
color = color.trim();
|
||||
|
||||
let title;
|
||||
if (name === "color") {
|
||||
title = i18n("category.background_color");
|
||||
} else if (name === "text_color") {
|
||||
title = i18n("category.foreground_color");
|
||||
} else {
|
||||
throw new Error(`unknown title for category attribute ${name}`);
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
addError(name, {
|
||||
title,
|
||||
message: i18n("category.color_validations.cant_be_empty"),
|
||||
});
|
||||
}
|
||||
|
||||
if (color.length !== 3 && color.length !== 6) {
|
||||
addError(name, {
|
||||
title,
|
||||
message: i18n("category.color_validations.incorrect_length"),
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^[0-9A-Fa-f]+$/.test(color)) {
|
||||
addError(name, {
|
||||
title,
|
||||
message: i18n("category.color_validations.non_hexdecimal"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get categoryDescription() {
|
||||
if (this.args.category.description) {
|
||||
return htmlSafe(this.args.category.description);
|
||||
|
@ -318,6 +357,8 @@ export default class EditCategoryGeneral extends Component {
|
|||
@name="color"
|
||||
@title={{i18n "category.background_color"}}
|
||||
@format="full"
|
||||
@validate={{this.validateColor}}
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
|
@ -328,6 +369,7 @@ export default class EditCategoryGeneral extends Component {
|
|||
@valid={{@category.colorValid}}
|
||||
@ariaLabelledby="background-color-label"
|
||||
@onChangeColor={{fn this.updateColor field}}
|
||||
@skipNormalize={{true}}
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{this.backgroundColors}}
|
||||
|
@ -345,6 +387,8 @@ export default class EditCategoryGeneral extends Component {
|
|||
@name="text_color"
|
||||
@title={{i18n "category.foreground_color"}}
|
||||
@format="full"
|
||||
@validate={{this.validateColor}}
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
|
@ -353,7 +397,8 @@ export default class EditCategoryGeneral extends Component {
|
|||
<ColorInput
|
||||
@hexValue={{readonly field.value}}
|
||||
@ariaLabelledby="foreground-color-label"
|
||||
@onBlur={{fn this.updateColor field}}
|
||||
@onChangeColor={{fn this.updateColor field}}
|
||||
@skipNormalize={{true}}
|
||||
/>
|
||||
<ColorPicker
|
||||
@colors={{CATEGORY_TEXT_COLORS}}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/* eslint-disable ember/no-classic-components */
|
||||
import Component from "@ember/component";
|
||||
import {
|
||||
attributeBindings,
|
||||
classNames,
|
||||
tagName,
|
||||
} from "@ember-decorators/component";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@tagName("button")
|
||||
@classNames("btn-flat")
|
||||
@attributeBindings("disabled", "resolvedTitle:title")
|
||||
export default class FlatButton extends Component {
|
||||
@discourseComputed("title", "translatedTitle")
|
||||
resolvedTitle(title, translatedTitle) {
|
||||
if (title) {
|
||||
return i18n(title);
|
||||
} else if (translatedTitle) {
|
||||
return translatedTitle;
|
||||
}
|
||||
}
|
||||
|
||||
keyDown(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.action?.();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
this.action?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
<template>{{icon this.icon}}</template>
|
||||
}
|
|
@ -54,14 +54,14 @@ export default class ReviewableClaimedTopic extends Component {
|
|||
@icon="xmark"
|
||||
@action={{this.unclaim}}
|
||||
@title="review.unclaim.help"
|
||||
class="btn-small unclaim"
|
||||
class="btn-default btn-small unclaim"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@icon="user-plus"
|
||||
@title="review.claim.title"
|
||||
@action={{this.claim}}
|
||||
class="btn-small claim"
|
||||
class="btn-default btn-small claim"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -425,7 +425,7 @@ export default class SearchMenu extends Component {
|
|||
@label="search.in_this_topic"
|
||||
@title="search.in_this_topic_tooltip"
|
||||
@action={{this.clearTopicContext}}
|
||||
class="btn-small search-context"
|
||||
class="btn-default btn-small search-context"
|
||||
/>
|
||||
{{else if this.inPMInboxContext}}
|
||||
<DButton
|
||||
|
@ -433,7 +433,7 @@ export default class SearchMenu extends Component {
|
|||
@label="search.in_messages"
|
||||
@title="search.in_messages_tooltip"
|
||||
@action={{this.clearPMInboxContext}}
|
||||
class="btn-small search-context"
|
||||
class="btn-default btn-small search-context"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -36,20 +36,24 @@ export default class TopicFooterButtons extends Component {
|
|||
elementId = "topic-footer-buttons";
|
||||
role = "region";
|
||||
|
||||
@getTopicFooterButtons() inlineButtons;
|
||||
@getTopicFooterDropdowns() inlineDropdowns;
|
||||
|
||||
@alias("currentUser.can_send_private_messages") canSendPms;
|
||||
@alias("topic.details.can_invite_to") canInviteTo;
|
||||
@alias("currentUser.user_option.enable_defer") canDefer;
|
||||
@or("topic.archived", "topic.closed", "topic.deleted") inviteDisabled;
|
||||
|
||||
get inlineButtons() {
|
||||
return getTopicFooterButtons(this);
|
||||
}
|
||||
|
||||
get inlineDropdowns() {
|
||||
return getTopicFooterDropdowns(this);
|
||||
}
|
||||
|
||||
@discourseComputed("canSendPms", "topic.isPrivateMessage")
|
||||
canArchive(canSendPms, isPM) {
|
||||
return canSendPms && isPM;
|
||||
}
|
||||
|
||||
@computed("inlineButtons.[]", "inlineDropdowns.[]")
|
||||
get inlineActionables() {
|
||||
return (
|
||||
this.inlineButtons
|
||||
|
@ -70,15 +74,12 @@ export default class TopicFooterButtons extends Component {
|
|||
return new TopicBookmarkManager(getOwner(this), this.topic);
|
||||
}
|
||||
|
||||
// topic.assigned_to_user is for backward plugin support
|
||||
@discourseComputed("inlineButtons.[]", "topic.assigned_to_user")
|
||||
dropdownButtons(inlineButtons) {
|
||||
return inlineButtons.filter((button) => button.dropdown);
|
||||
get dropdownButtons() {
|
||||
return this.inlineButtons.filter((button) => button.dropdown);
|
||||
}
|
||||
|
||||
@discourseComputed("dropdownButtons.[]")
|
||||
loneDropdownButton(dropdownButtons) {
|
||||
return dropdownButtons.length === 1 ? dropdownButtons[0] : null;
|
||||
get loneDropdownButton() {
|
||||
return this.dropdownButtons.length === 1 ? this.dropdownButtons[0] : null;
|
||||
}
|
||||
|
||||
@discourseComputed("topic.isPrivateMessage")
|
||||
|
|
|
@ -8,7 +8,7 @@ const BulkSelectCell = <template>
|
|||
<button
|
||||
{{on "click" @bulkSelectHelper.toggleBulkSelect}}
|
||||
title={{i18n "topics.bulk.toggle"}}
|
||||
class="btn-flat bulk-select"
|
||||
class="btn-transparent bulk-select no-text"
|
||||
>
|
||||
{{icon "list-check"}}
|
||||
</button>
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class SortableColumn extends Component {
|
|||
<button
|
||||
{{on "click" @bulkSelectHelper.toggleBulkSelect}}
|
||||
title={{i18n "topics.bulk.toggle"}}
|
||||
class="btn-flat bulk-select"
|
||||
class="btn-transparent bulk-select no-text"
|
||||
>
|
||||
{{icon "list-check"}}
|
||||
</button>
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class TopicLocalizedContentToggle extends Component {
|
|||
@title={{this.title}}
|
||||
class={{concatClass
|
||||
"btn btn-default btn-toggle-localized-content no-text"
|
||||
(unless this.showingOriginal "btn-active")
|
||||
(unless this.showingOriginal "--active")
|
||||
}}
|
||||
@action={{this.showOriginal}}
|
||||
/>
|
||||
|
|
|
@ -649,7 +649,7 @@ export default class TopicTimelineScrollArea extends Component {
|
|||
@icon="layer-group"
|
||||
@label="summary.short_label"
|
||||
title={{i18n "summary.short_title"}}
|
||||
class="show-summary btn-small"
|
||||
class="show-summary btn-default btn-small"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -61,14 +61,6 @@ export default class EditCategoryTabsController extends Controller {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!transientData.color) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (transientData.text_color.length < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.saving || this.deleting) {
|
||||
return true;
|
||||
}
|
||||
|
@ -120,13 +112,12 @@ export default class EditCategoryTabsController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
saveCategory(transientData) {
|
||||
saveCategory(data) {
|
||||
if (this.validators.some((validator) => validator())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.setProperties(transientData);
|
||||
|
||||
this.model.setProperties(data);
|
||||
this.set("saving", true);
|
||||
|
||||
this.model
|
||||
|
|
|
@ -118,15 +118,23 @@ export default class SecurityController extends Controller {
|
|||
"model.no_password",
|
||||
"siteSettings",
|
||||
"model.user_passkeys",
|
||||
"model.associated_accounts"
|
||||
"model.associated_accounts",
|
||||
"model.can_remove_password"
|
||||
)
|
||||
canRemovePassword(
|
||||
isAnonymous,
|
||||
noPassword,
|
||||
siteSettings,
|
||||
userPasskeys,
|
||||
associatedAccounts
|
||||
associatedAccounts,
|
||||
canRemove
|
||||
) {
|
||||
// Hint returned from staff-info controller that
|
||||
// works even if staff hasn't revealed e-mails.
|
||||
if (canRemove) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
isAnonymous ||
|
||||
noPassword ||
|
||||
|
|
|
@ -225,7 +225,7 @@ class FKForm extends Component {
|
|||
try {
|
||||
this.isSubmitting = true;
|
||||
|
||||
await this.validate(this.fields.values());
|
||||
await this.validate([...this.fields.values()]);
|
||||
|
||||
if (this.formData.isValid) {
|
||||
this.formData.save();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { computed } from "@ember/object";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
let _topicFooterButtons = {};
|
||||
|
@ -71,80 +70,79 @@ export function registerTopicFooterButton(button) {
|
|||
_topicFooterButtons[normalizedButton.id] = normalizedButton;
|
||||
}
|
||||
|
||||
export function getTopicFooterButtons() {
|
||||
const dependentKeys = [].concat(
|
||||
export function getTopicFooterButtons(context) {
|
||||
const legacyDependentKeys = [].concat(
|
||||
...Object.values(_topicFooterButtons)
|
||||
.map((tfb) => tfb.dependentKeys)
|
||||
.filter((x) => x)
|
||||
);
|
||||
|
||||
return computed(...dependentKeys, {
|
||||
get() {
|
||||
const _isFunction = (descriptor) =>
|
||||
descriptor && typeof descriptor === "function";
|
||||
legacyDependentKeys.forEach((k) => context.get(k));
|
||||
|
||||
const _compute = (button, property) => {
|
||||
const field = button[property];
|
||||
const _isFunction = (descriptor) =>
|
||||
descriptor && typeof descriptor === "function";
|
||||
|
||||
if (_isFunction(field)) {
|
||||
return field.apply(this);
|
||||
}
|
||||
const _compute = (button, property) => {
|
||||
const field = button[property];
|
||||
|
||||
return field;
|
||||
};
|
||||
if (_isFunction(field)) {
|
||||
return field.apply(context);
|
||||
}
|
||||
|
||||
return Object.values(_topicFooterButtons)
|
||||
.filter((button) => _compute(button, "displayed"))
|
||||
.map((button) => {
|
||||
const discourseComputedButton = {};
|
||||
|
||||
discourseComputedButton.id = button.id;
|
||||
discourseComputedButton.type = button.type;
|
||||
return field;
|
||||
};
|
||||
|
||||
return Object.values(_topicFooterButtons)
|
||||
.filter((button) => _compute(button, "displayed"))
|
||||
.map((button) => {
|
||||
return {
|
||||
id: button.id,
|
||||
type: button.type,
|
||||
get label() {
|
||||
const label = _compute(button, "label");
|
||||
discourseComputedButton.label = label
|
||||
? i18n(label)
|
||||
: _compute(button, "translatedLabel");
|
||||
|
||||
return label ? i18n(label) : _compute(button, "translatedLabel");
|
||||
},
|
||||
get ariaLabel() {
|
||||
const ariaLabel = _compute(button, "ariaLabel");
|
||||
if (ariaLabel) {
|
||||
discourseComputedButton.ariaLabel = i18n(ariaLabel);
|
||||
return i18n(ariaLabel);
|
||||
} else {
|
||||
const translatedAriaLabel = _compute(button, "translatedAriaLabel");
|
||||
discourseComputedButton.ariaLabel =
|
||||
translatedAriaLabel || discourseComputedButton.label;
|
||||
return translatedAriaLabel || this.label;
|
||||
}
|
||||
|
||||
},
|
||||
get title() {
|
||||
const title = _compute(button, "title");
|
||||
discourseComputedButton.title = title
|
||||
? i18n(title)
|
||||
: _compute(button, "translatedTitle");
|
||||
|
||||
discourseComputedButton.classNames = (
|
||||
_compute(button, "classNames") || []
|
||||
).join(" ");
|
||||
|
||||
discourseComputedButton.icon = _compute(button, "icon");
|
||||
discourseComputedButton.disabled = _compute(button, "disabled");
|
||||
discourseComputedButton.dropdown = _compute(button, "dropdown");
|
||||
discourseComputedButton.priority = _compute(button, "priority");
|
||||
|
||||
discourseComputedButton.anonymousOnly = _compute(
|
||||
button,
|
||||
"anonymousOnly"
|
||||
);
|
||||
|
||||
return title ? i18n(title) : _compute(button, "translatedTitle");
|
||||
},
|
||||
get classNames() {
|
||||
return (_compute(button, "classNames") || []).join(" ");
|
||||
},
|
||||
get icon() {
|
||||
return _compute(button, "icon");
|
||||
},
|
||||
get disabled() {
|
||||
return _compute(button, "disabled");
|
||||
},
|
||||
get dropdown() {
|
||||
return _compute(button, "dropdown");
|
||||
},
|
||||
get priority() {
|
||||
return _compute(button, "priority");
|
||||
},
|
||||
get anonymousOnly() {
|
||||
return _compute(button, "anonymousOnly");
|
||||
},
|
||||
get action() {
|
||||
if (_isFunction(button.action)) {
|
||||
discourseComputedButton.action = () => button.action.apply(this);
|
||||
return () => button.action.apply(context);
|
||||
} else {
|
||||
const actionName = button.action;
|
||||
discourseComputedButton.action = () => this[actionName]();
|
||||
return () => context[actionName]();
|
||||
}
|
||||
|
||||
return discourseComputedButton;
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function clearTopicFooterButtons() {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { computed } from "@ember/object";
|
||||
|
||||
let _topicFooterDropdowns = {};
|
||||
|
||||
export function registerTopicFooterDropdown(dropdown) {
|
||||
|
@ -55,52 +53,59 @@ export function registerTopicFooterDropdown(dropdown) {
|
|||
_topicFooterDropdowns[normalizedDropdown.id] = normalizedDropdown;
|
||||
}
|
||||
|
||||
export function getTopicFooterDropdowns() {
|
||||
const dependentKeys = [].concat(
|
||||
export function getTopicFooterDropdowns(context) {
|
||||
const legacyDependentKeys = [].concat(
|
||||
...Object.values(_topicFooterDropdowns)
|
||||
.map((item) => item.dependentKeys)
|
||||
.filter(Boolean)
|
||||
);
|
||||
legacyDependentKeys.forEach((key) => context.get(key));
|
||||
|
||||
return computed(...dependentKeys, {
|
||||
get() {
|
||||
const _isFunction = (descriptor) =>
|
||||
descriptor && typeof descriptor === "function";
|
||||
const _isFunction = (descriptor) =>
|
||||
descriptor && typeof descriptor === "function";
|
||||
|
||||
const _compute = (dropdown, property) => {
|
||||
const field = dropdown[property];
|
||||
const _compute = (dropdown, property) => {
|
||||
const field = dropdown[property];
|
||||
|
||||
if (_isFunction(field)) {
|
||||
return field.apply(this);
|
||||
}
|
||||
if (_isFunction(field)) {
|
||||
return field.apply(context);
|
||||
}
|
||||
|
||||
return field;
|
||||
return field;
|
||||
};
|
||||
|
||||
return Object.values(_topicFooterDropdowns)
|
||||
.filter((dropdown) => _compute(dropdown, "displayed"))
|
||||
.map((dropdown) => {
|
||||
return {
|
||||
id: dropdown.id,
|
||||
type: dropdown.type,
|
||||
get classNames() {
|
||||
return (_compute(dropdown, "classNames") || []).join(" ");
|
||||
},
|
||||
get icon() {
|
||||
return _compute(dropdown, "icon");
|
||||
},
|
||||
get disabled() {
|
||||
return _compute(dropdown, "disabled");
|
||||
},
|
||||
get priority() {
|
||||
return _compute(dropdown, "priority");
|
||||
},
|
||||
get content() {
|
||||
return _compute(dropdown, "content");
|
||||
},
|
||||
get value() {
|
||||
return _compute(dropdown, "value");
|
||||
},
|
||||
get action() {
|
||||
return dropdown.action;
|
||||
},
|
||||
get noneItem() {
|
||||
return _compute(dropdown, "noneItem");
|
||||
},
|
||||
};
|
||||
|
||||
return Object.values(_topicFooterDropdowns)
|
||||
.filter((dropdown) => _compute(dropdown, "displayed"))
|
||||
.map((dropdown) => {
|
||||
const discourseComputedDropdown = {};
|
||||
|
||||
discourseComputedDropdown.id = dropdown.id;
|
||||
discourseComputedDropdown.type = dropdown.type;
|
||||
|
||||
discourseComputedDropdown.classNames = (
|
||||
_compute(dropdown, "classNames") || []
|
||||
).join(" ");
|
||||
|
||||
discourseComputedDropdown.icon = _compute(dropdown, "icon");
|
||||
discourseComputedDropdown.disabled = _compute(dropdown, "disabled");
|
||||
discourseComputedDropdown.priority = _compute(dropdown, "priority");
|
||||
discourseComputedDropdown.content = _compute(dropdown, "content");
|
||||
discourseComputedDropdown.value = _compute(dropdown, "value");
|
||||
discourseComputedDropdown.action = dropdown.action;
|
||||
discourseComputedDropdown.noneItem = _compute(dropdown, "noneItem");
|
||||
|
||||
return discourseComputedDropdown;
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function clearTopicFooterDropdowns() {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { fn } from "@ember/helper";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
|
@ -93,6 +92,7 @@ export default RouteTemplate(
|
|||
<Form
|
||||
@data={{@controller.formData}}
|
||||
@onDirtyCheck={{@controller.isLeavingForm}}
|
||||
@onSubmit={{@controller.saveCategory}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Section
|
||||
|
@ -104,7 +104,7 @@ export default RouteTemplate(
|
|||
<Tab
|
||||
@selectedTab={{@controller.selectedTab}}
|
||||
@category={{@controller.model}}
|
||||
@action={{@controller.registerValidator}}
|
||||
@registerValidator={{@controller.registerValidator}}
|
||||
@transientData={{transientData}}
|
||||
@form={{form}}
|
||||
/>
|
||||
|
@ -119,12 +119,10 @@ export default RouteTemplate(
|
|||
{{/if}}
|
||||
|
||||
<form.Actions class="edit-category-footer">
|
||||
<form.Button
|
||||
<form.Submit
|
||||
@disabled={{not (@controller.canSaveForm transientData)}}
|
||||
@action={{fn @controller.saveCategory transientData}}
|
||||
@label={{@controller.saveLabel}}
|
||||
id="save-category"
|
||||
class="btn-primary"
|
||||
/>
|
||||
|
||||
{{#if @controller.model.can_delete}}
|
||||
|
|
|
@ -11,7 +11,6 @@ import UserPasskeys from "discourse/components/user-preferences/user-passkeys";
|
|||
import icon from "discourse/helpers/d-icon";
|
||||
import formatDate from "discourse/helpers/format-date";
|
||||
import lazyHash from "discourse/helpers/lazy-hash";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
|
@ -47,30 +46,24 @@ export default RouteTemplate(
|
|||
</div>
|
||||
|
||||
{{#unless @controller.model.no_password}}
|
||||
{{#if @controller.associatedAccountsLoaded}}
|
||||
{{#if @controller.canRemovePassword}}
|
||||
<div class="controls">
|
||||
<a
|
||||
href
|
||||
{{on "click" @controller.removePassword}}
|
||||
hidden={{@controller.removePasswordInProgress}}
|
||||
id="remove-password-link"
|
||||
>
|
||||
{{icon "trash-can"}}
|
||||
{{i18n "user.change_password.remove"}}
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="controls">
|
||||
<DButton
|
||||
@action={{fn (routeAction "checkEmail") @controller.model}}
|
||||
@title="admin.users.check_email.title"
|
||||
@icon="envelope"
|
||||
@label="admin.users.check_email.text"
|
||||
/>
|
||||
<div class="controls">
|
||||
<button
|
||||
{{on "click" @controller.removePassword}}
|
||||
disabled={{not @controller.canRemovePassword}}
|
||||
hidden={{@controller.removePasswordInProgress}}
|
||||
class="btn btn-transparent"
|
||||
id="remove-password-link"
|
||||
>
|
||||
{{icon "trash-can"}}
|
||||
{{i18n "user.change_password.remove"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{#unless @controller.canRemovePassword}}
|
||||
<div class="instructions">
|
||||
{{i18n "user.change_password.remove_disabled"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
|
@ -102,6 +95,8 @@ export default RouteTemplate(
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet @name="user-preferences-security-after-password" />
|
||||
|
||||
{{#if @controller.canCheckEmails}}
|
||||
<div
|
||||
class="control-group pref-auth-tokens"
|
||||
|
|
|
@ -294,7 +294,7 @@ export default RouteTemplate(
|
|||
@label="user.invited.edit"
|
||||
@action={{fn @controller.editInvite invite}}
|
||||
@title="user.invited.edit"
|
||||
class="btn-small edit-invite"
|
||||
class="btn-default btn-small edit-invite"
|
||||
/>
|
||||
<DMenu
|
||||
@identifier="invites-menu"
|
||||
|
|
|
@ -234,17 +234,17 @@ acceptance("User Preferences - Security", function (needs) {
|
|||
await visit("/u/eviltrout/preferences/security");
|
||||
// eviltrout starts with an entry in associated_accounts, can remove password
|
||||
assert
|
||||
.dom("#remove-password-link")
|
||||
.exists("shows for user with associated account");
|
||||
.dom("#remove-password-link:not(:disabled)")
|
||||
.exists("is enabled for user with associated account");
|
||||
|
||||
updateCurrentUser({
|
||||
associated_accounts: null,
|
||||
});
|
||||
|
||||
assert
|
||||
.dom("#remove-password-link")
|
||||
.doesNotExist(
|
||||
"does not show for user with no associated account and no passkeys"
|
||||
.dom("#remove-password-link:disabled")
|
||||
.exists(
|
||||
"is disabled for user with no associated account and no passkeys"
|
||||
);
|
||||
|
||||
updateCurrentUser({
|
||||
|
@ -257,7 +257,9 @@ acceptance("User Preferences - Security", function (needs) {
|
|||
},
|
||||
],
|
||||
});
|
||||
assert.dom("#remove-password-link").exists("shows for user with passkey");
|
||||
assert
|
||||
.dom("#remove-password-link:not(:disabled)")
|
||||
.exists("is enabled for user with passkey");
|
||||
});
|
||||
|
||||
test("Removing User Password", async function (assert) {
|
||||
|
|
|
@ -25,13 +25,6 @@ module("Integration | Component | d-button", function (hooks) {
|
|||
assert.dom("button span.d-button-label").exists("has the label");
|
||||
});
|
||||
|
||||
test("text only button", async function (assert) {
|
||||
await render(<template><DButton @label="topic.create" /></template>);
|
||||
|
||||
assert.dom("button.btn.btn-text").exists("has all the classes");
|
||||
assert.dom("button span.d-button-label").exists("has the label");
|
||||
});
|
||||
|
||||
test("form attribute", async function (assert) {
|
||||
await render(<template><DButton @form="login-form" /></template>);
|
||||
|
||||
|
@ -40,7 +33,6 @@ module("Integration | Component | d-button", function (hooks) {
|
|||
|
||||
test("link-styled button", async function (assert) {
|
||||
await render(<template><DButton @display="link" /></template>);
|
||||
|
||||
assert.dom("button.btn-link:not(.btn)").exists("has the right classes");
|
||||
});
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import { click, render, triggerKeyEvent } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import FlatButton from "discourse/components/flat-button";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | flat-button", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("press Enter", async function (assert) {
|
||||
const self = this;
|
||||
|
||||
this.set("foo", null);
|
||||
this.set("action", () => {
|
||||
this.set("foo", "bar");
|
||||
});
|
||||
|
||||
await render(<template><FlatButton @action={{self.action}} /></template>);
|
||||
|
||||
await triggerKeyEvent(".btn-flat", "keydown", "Space");
|
||||
assert.strictEqual(this.foo, null);
|
||||
|
||||
await triggerKeyEvent(".btn-flat", "keydown", "Enter");
|
||||
assert.strictEqual(this.foo, "bar");
|
||||
});
|
||||
|
||||
test("click", async function (assert) {
|
||||
const self = this;
|
||||
|
||||
this.set("foo", null);
|
||||
this.set("action", () => {
|
||||
this.set("foo", "bar");
|
||||
});
|
||||
|
||||
await render(<template><FlatButton @action={{self.action}} /></template>);
|
||||
|
||||
await click(".btn-flat");
|
||||
assert.strictEqual(this.foo, "bar");
|
||||
});
|
||||
});
|
|
@ -47,19 +47,6 @@ module("Integration | Component | Widget | button", function (hooks) {
|
|||
assert.dom("button span.d-button-label").exists("it renders the label");
|
||||
});
|
||||
|
||||
test("text only button", async function (assert) {
|
||||
const self = this;
|
||||
|
||||
this.set("args", { label: "topic.create" });
|
||||
|
||||
await render(
|
||||
<template><MountWidget @widget="button" @args={{self.args}} /></template>
|
||||
);
|
||||
|
||||
assert.dom("button.btn.btn-text").exists("has all the classes");
|
||||
assert.dom("button span.d-button-label").exists("has the label");
|
||||
});
|
||||
|
||||
test("translatedLabel", async function (assert) {
|
||||
const self = this;
|
||||
|
||||
|
|
|
@ -181,10 +181,6 @@
|
|||
.discourse-no-touch &:hover,
|
||||
.discourse-no-touch &:focus {
|
||||
background-color: var(--d-hover);
|
||||
|
||||
> .d-icon {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1503,7 +1503,6 @@ span.mention {
|
|||
}
|
||||
|
||||
.post-notice {
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
background-color: var(--tertiary-very-low);
|
||||
border-top: 1px solid var(--content-border-color);
|
||||
|
@ -1514,6 +1513,7 @@ span.mention {
|
|||
font-size: var(--font-down-1-rem);
|
||||
border-radius: var(--d-border-radius);
|
||||
gap: var(--space-2);
|
||||
box-sizing: border-box;
|
||||
|
||||
@include viewport.until(sm) {
|
||||
margin-bottom: var(--space-4);
|
||||
|
|
|
@ -28,6 +28,19 @@
|
|||
|
||||
.topic-navigation {
|
||||
overflow-anchor: none;
|
||||
|
||||
.btn-toggle-localized-content.--active {
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgb(var(--primary-rgb), 0.6) 100%,
|
||||
rgb(var(--primary-rgb), 0.6) 100%
|
||||
);
|
||||
color: var(--d-button-default-text-color--hover);
|
||||
|
||||
.d-icon {
|
||||
color: var(--d-button-default-text-color--hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timeline
|
||||
|
|
|
@ -42,8 +42,8 @@
|
|||
--d-button-flat-icon-color: var(--primary-low-mid);
|
||||
--d-button-flat-icon-color--hover: var(--primary);
|
||||
--d-button-flat-bg-color: transparent;
|
||||
--d-button-flat-bg-color--hover: transparent;
|
||||
--d-button-flat-bg-color--focus: var(--primary-low);
|
||||
--d-button-flat-bg-color--hover: var(--d-hover);
|
||||
--d-button-flat-bg-color--focus: var(--d-hover);
|
||||
--d-button-flat-text-color--disabled: var(--primary);
|
||||
|
||||
// .btn-flat.close
|
||||
|
@ -68,23 +68,9 @@
|
|||
$hover-bg-color: var(--d-button-default-bg-color--hover),
|
||||
$hover-icon-color: var(--d-button-default-icon-color--hover)
|
||||
) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
color: $text-color;
|
||||
background-color: $bg-color;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgb(0, 0, 0, 0),
|
||||
rgb(0, 0, 0, 0)
|
||||
);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
transition: var(--d-button-transition);
|
||||
cursor: pointer;
|
||||
|
||||
@include form-item-sizing;
|
||||
border: var(--d-button-border);
|
||||
|
||||
&:visited {
|
||||
|
@ -142,30 +128,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.discourse-no-touch &:active:not(:hover, :focus),
|
||||
.discourse-no-touch &.btn-active:not(:hover, :focus),
|
||||
&:active:not(:hover, :focus),
|
||||
&.btn-active:not(:hover, :focus) {
|
||||
@include darken-background($bg-color, 0.6);
|
||||
color: $hover-text-color;
|
||||
|
||||
.d-icon {
|
||||
color: $hover-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
.discourse-no-touch &:active,
|
||||
.discourse-no-touch &.btn-active,
|
||||
&:active,
|
||||
&.btn-active {
|
||||
@include darken-background($bg-color, 0.3);
|
||||
color: $hover-text-color;
|
||||
|
||||
.d-icon {
|
||||
color: $hover-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
@ -190,12 +152,10 @@
|
|||
}
|
||||
|
||||
&.is-loading {
|
||||
&.btn-text {
|
||||
&.btn-small {
|
||||
.loading-icon {
|
||||
font-size: var(--font-down-1);
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
&.btn-small {
|
||||
.loading-icon {
|
||||
font-size: var(--font-down-1);
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,9 +167,75 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Default button */
|
||||
/* Generic btn */
|
||||
.btn {
|
||||
@include btn;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgb(0, 0, 0, 0),
|
||||
rgb(0, 0, 0, 0)
|
||||
);
|
||||
box-sizing: border-box;
|
||||
padding: $vpad $hpad;
|
||||
cursor: pointer;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/* link button */
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: var(--tertiary);
|
||||
|
||||
@include hover {
|
||||
background-color: transparent;
|
||||
color: var(--tertiary-hover);
|
||||
|
||||
.d-icon {
|
||||
color: var(--tertiary-hover);
|
||||
|
||||
/* For Windows High Contrast (see whcm.scss for more) */
|
||||
@media (forced-colors: active) {
|
||||
color: Highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--tertiary);
|
||||
background: transparent;
|
||||
|
||||
@include default-focus;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.disabled {
|
||||
color: var(--d-button-transparent-text-color);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:not(.is-loading) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
|
||||
.d-icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Default button */
|
||||
.btn-default {
|
||||
border-radius: var(--d-button-border-radius);
|
||||
}
|
||||
|
||||
/* Primary button */
|
||||
|
@ -222,6 +248,7 @@
|
|||
$hover-bg-color: var(--d-button-primary-bg-color--hover),
|
||||
$hover-icon-color: var(--d-button-primary-icon-color--hover)
|
||||
);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
}
|
||||
|
||||
/* Danger button */
|
||||
|
@ -235,6 +262,7 @@
|
|||
$hover-bg-color: var(--d-button-danger-bg-color--hover),
|
||||
$hover-icon-color: var(--d-button-danger-icon-color--hover)
|
||||
);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
}
|
||||
|
||||
/* Success button */
|
||||
|
@ -248,6 +276,7 @@
|
|||
$hover-bg-color: var(--d-button-success-bg-color--hover),
|
||||
$hover-icon-color: var(--d-button-success-icon-color--hover)
|
||||
);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
}
|
||||
|
||||
/* Social buttons */
|
||||
|
@ -359,28 +388,17 @@
|
|||
|
||||
/* Bonus Buttons */
|
||||
.btn-flat {
|
||||
background: var(--d-button-flat-bg-color);
|
||||
transition: var(--d-button-transition);
|
||||
|
||||
.d-icon {
|
||||
color: var(--d-button-flat-icon-color);
|
||||
transition: var(--d-button-transition);
|
||||
}
|
||||
@include btn(
|
||||
$text-color: var(--d-button-default-text-color),
|
||||
$bg-color: var(--d-button-flat-bg-color),
|
||||
$icon-color: var(--d-button-flat-icon-color),
|
||||
$hover-text-color: var(--d-button-flat-text-color--hover),
|
||||
$hover-bg-color: var(--d-button-flat-bg-color--hover),
|
||||
$hover-icon-color: var(--d-button-flat-icon-color--hover)
|
||||
);
|
||||
border-radius: var(--d-button-border-radius);
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--d-button-flat-text-color--hover);
|
||||
|
||||
.d-icon {
|
||||
color: var(--d-button-flat-icon-color--hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--d-button-flat-bg-color--hover);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: var(--d-button-flat-bg-color--focus);
|
||||
}
|
||||
|
@ -413,36 +431,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.btn-text {
|
||||
color: var(--d-button-flat-text-color);
|
||||
|
||||
&[disabled] {
|
||||
&,
|
||||
.discourse-no-touch & {
|
||||
&:hover,
|
||||
&.btn-hover,
|
||||
&:focus-visible {
|
||||
color: var(--d-button-flat-text-color--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:not([disabled]) {
|
||||
&:hover,
|
||||
&.btn-hover,
|
||||
&:focus-visible {
|
||||
color: var(--d-button-flat-text-color--hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.btn-active {
|
||||
@include darken-background(transparent, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--primary-low);
|
||||
|
@ -457,27 +445,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: var(--tertiary);
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
color: var(--tertiary);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--tertiary);
|
||||
background: transparent;
|
||||
|
||||
@include default-focus;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin btn-colors($btn-type) {
|
||||
color: var(--d-button-#{$btn-type}-bg-color);
|
||||
|
||||
|
@ -498,17 +465,18 @@
|
|||
|
||||
.btn-transparent {
|
||||
&,
|
||||
&.btn-default,
|
||||
&.btn-text,
|
||||
&.btn-default, // preferably NOT used in conjuction with btn-default
|
||||
&.btn-icon,
|
||||
&.no-text {
|
||||
background: transparent;
|
||||
@include btn(
|
||||
$text-color: var(--d-button-transparent-text-color),
|
||||
$bg-color: transparent,
|
||||
$icon-color: var(--d-button-transparent-icon-color),
|
||||
$hover-text-color: var(--d-button-transparent-text-color--hover),
|
||||
$hover-bg-color: transparent,
|
||||
$hover-icon-color: currentcolor
|
||||
);
|
||||
border: 0;
|
||||
color: var(--d-button-transparent-text-color);
|
||||
|
||||
.d-icon {
|
||||
color: var(--d-button-transparent-icon-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: transparent;
|
||||
|
@ -518,17 +486,6 @@
|
|||
color: currentcolor;
|
||||
}
|
||||
}
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: var(--d-button-transparent-text-color--hover);
|
||||
|
||||
.d-icon {
|
||||
color: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
|
||||
button.bulk-select {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin-right: var(--space-2);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
|
|
@ -442,15 +442,12 @@ class TopicsController < ApplicationController
|
|||
success = true
|
||||
|
||||
if changes.length > 0
|
||||
bypass_bump = should_bypass_bump?(changes)
|
||||
|
||||
first_post = topic.ordered_posts.first
|
||||
success =
|
||||
PostRevisor.new(first_post, topic).revise!(
|
||||
current_user,
|
||||
changes,
|
||||
validate_post: false,
|
||||
bypass_bump: bypass_bump,
|
||||
keep_existing_draft: params[:keep_existing_draft].to_s == "true",
|
||||
)
|
||||
|
||||
|
@ -1257,11 +1254,6 @@ class TopicsController < ApplicationController
|
|||
Promotion.new(current_user).review if current_user.present?
|
||||
end
|
||||
|
||||
def should_bypass_bump?(changes)
|
||||
(changes[:category_id].present? && SiteSetting.disable_category_edit_notifications) ||
|
||||
(changes[:tags].present? && SiteSetting.disable_tags_edit_notifications)
|
||||
end
|
||||
|
||||
def slugs_do_not_match
|
||||
if SiteSetting.slug_generation_method != "encoded"
|
||||
params[:slug] && @topic_view.topic.slug != params[:slug]
|
||||
|
|
|
@ -1529,7 +1529,8 @@ class UsersController < ApplicationController
|
|||
number_of_suspensions
|
||||
warnings_received_count
|
||||
number_of_rejected_posts
|
||||
].each { |info| result[info] = @user.public_send(info) }
|
||||
can_remove_password?
|
||||
].each { |info| result[info.delete_suffix("?")] = @user.public_send(info) }
|
||||
|
||||
render json: result
|
||||
end
|
||||
|
|
|
@ -28,18 +28,6 @@ class Bookmark < ActiveRecord::Base
|
|||
belongs_to :user
|
||||
belongs_to :bookmarkable, polymorphic: true
|
||||
|
||||
has_many :reminder_notifications,
|
||||
->(bookmark) do
|
||||
where(notification_type: Notification.types[:bookmark_reminder]).where(
|
||||
"data::jsonb->>'bookmark_id' = ?",
|
||||
bookmark.id.to_s,
|
||||
)
|
||||
end,
|
||||
class_name: "Notification",
|
||||
foreign_key: :user_id,
|
||||
primary_key: :user_id,
|
||||
dependent: :destroy
|
||||
|
||||
def self.auto_delete_preferences
|
||||
@auto_delete_preferences ||=
|
||||
Enum.new(never: 0, when_reminder_sent: 1, on_owner_reply: 2, clear_reminder: 3)
|
||||
|
|
|
@ -104,6 +104,8 @@ class Category < ActiveRecord::Base
|
|||
},
|
||||
allow_nil: true
|
||||
validates :slug, exclusion: { in: RESERVED_SLUGS }
|
||||
validates :color, format: { with: /\A(\h{6}|\h{3})\z/ }
|
||||
validates :text_color, format: { with: /\A(\h{6}|\h{3})\z/ }
|
||||
|
||||
after_create :create_category_definition
|
||||
after_destroy :trash_category_definition
|
||||
|
|
|
@ -929,8 +929,12 @@ class User < ActiveRecord::Base
|
|||
@raw_password = pw # still required to maintain compatibility with usage of password-related User interface
|
||||
end
|
||||
|
||||
def can_remove_password?
|
||||
associated_accounts.present? || passkey_credential_ids.present?
|
||||
end
|
||||
|
||||
def remove_password
|
||||
raise Discourse::InvalidAccess if associated_accounts.blank? && passkey_credential_ids.blank?
|
||||
raise Discourse::InvalidAccess if !can_remove_password?
|
||||
|
||||
user_password.destroy if user_password
|
||||
end
|
||||
|
|
|
@ -1799,6 +1799,7 @@ en:
|
|||
verify_identity: "To continue, please verify your identity."
|
||||
title: "Password Reset"
|
||||
remove: "Remove Password"
|
||||
remove_disabled: "The account needs at least one other login method (e.g. a passkey or a linked external account) before removing the password."
|
||||
remove_detail: "Your account will no longer be accessible with a password unless you reset it."
|
||||
|
||||
second_factor_backup:
|
||||
|
@ -4460,6 +4461,10 @@ en:
|
|||
style: "Styles"
|
||||
background_color: "Color"
|
||||
foreground_color: "Text color"
|
||||
color_validations:
|
||||
cant_be_empty: "can't be empty"
|
||||
incorrect_length: "must match the format #RRGGBB or #RGB"
|
||||
non_hexdecimal: "must only contain hexadecimal characters (0-9, A-F)"
|
||||
styles:
|
||||
type: "Style"
|
||||
icon: "Icon"
|
||||
|
|
|
@ -22,9 +22,6 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::C
|
|||
end
|
||||
end
|
||||
cookie_jar(request)[@key] = cookie
|
||||
rescue ActionDispatch::Cookies::CookieOverflow
|
||||
Rails.logger.error("Cookie overflow occurred for #{@key}: #{request.session.to_h.inspect}")
|
||||
raise
|
||||
end
|
||||
|
||||
def session_has_changed?(request, session)
|
||||
|
|
|
@ -209,7 +209,7 @@ class Auth::DefaultCurrentUserProvider
|
|||
# under no conditions to suspended or inactive accounts get current_user
|
||||
current_user = nil if current_user && (current_user.suspended? || !current_user.active)
|
||||
|
||||
if current_user && should_update_last_seen?
|
||||
if current_user && !current_user.is_impersonating && should_update_last_seen?
|
||||
ip = request.ip
|
||||
user_id = current_user.id
|
||||
old_ip = current_user.ip_address
|
||||
|
|
|
@ -587,7 +587,7 @@ class PostRevisor
|
|||
end
|
||||
|
||||
def create_revision
|
||||
modifications = post_changes.merge(@topic_changes.diff)
|
||||
modifications = post_changes.merge(topic_diff)
|
||||
|
||||
modifications["raw"][0] = cached_original_raw || modifications["raw"][0] if modifications["raw"]
|
||||
|
||||
|
@ -608,7 +608,7 @@ class PostRevisor
|
|||
def update_revision
|
||||
return unless revision = PostRevision.find_by(post_id: @post.id, number: @post.version)
|
||||
revision.user_id = @post.last_editor_id
|
||||
modifications = post_changes.merge(@topic_changes.diff)
|
||||
modifications = post_changes.merge(topic_diff)
|
||||
|
||||
modifications.each_key do |field|
|
||||
if revision.modifications.has_key?(field)
|
||||
|
@ -641,7 +641,7 @@ class PostRevisor
|
|||
end
|
||||
|
||||
def topic_diff
|
||||
@topic_changes.diff
|
||||
@topic_changes.diff.with_indifferent_access
|
||||
end
|
||||
|
||||
def perform_edit
|
||||
|
@ -682,7 +682,7 @@ class PostRevisor
|
|||
def only_hidden_tags_changed?
|
||||
return false if (hidden_tag_names = DiscourseTagging.hidden_tag_names).blank?
|
||||
|
||||
modifications = post_changes.merge(@topic_changes.diff)
|
||||
modifications = post_changes.merge(topic_diff)
|
||||
if modifications.keys.size == 1 && (tags_diff = modifications["tags"]).present?
|
||||
a, b = tags_diff[0] || [], tags_diff[1] || []
|
||||
changed_tags = ((a + b) - (a & b)).map(&:presence).compact
|
||||
|
@ -745,7 +745,7 @@ class PostRevisor
|
|||
|
||||
def publish_changes
|
||||
options =
|
||||
if !@topic_changes.diff.empty? && !@topic_changes.errored?
|
||||
if !topic_diff.empty? && !@topic_changes.errored?
|
||||
{ reload_topic: true }
|
||||
else
|
||||
{}
|
||||
|
@ -777,6 +777,16 @@ class PostRevisor
|
|||
!@topic_changes.errored?
|
||||
end
|
||||
|
||||
def topic_category_changed?
|
||||
topic_changed? && @fields.has_key?(:category_id) && topic_diff.has_key?(:category_id) &&
|
||||
!@topic_changes.errored?
|
||||
end
|
||||
|
||||
def topic_tags_changed?
|
||||
topic_changed? && @fields.has_key?(:tags) && topic_diff.has_key?(:tags) &&
|
||||
!@topic_changes.errored?
|
||||
end
|
||||
|
||||
def reviewable_content_changed?
|
||||
raw_changed? || topic_title_changed?
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"pikaday": "1.8.2",
|
||||
"playwright": "1.55.1",
|
||||
"prettier": "3.5.3",
|
||||
"puppeteer-core": "^24.22.3",
|
||||
"puppeteer-core": "^24.23.0",
|
||||
"squoosh": "https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d",
|
||||
"stylelint": "16.23.1",
|
||||
"terser": "^5.44.0",
|
||||
|
|
|
@ -44,7 +44,7 @@ import DAutocompleteModifier, {
|
|||
SKIP,
|
||||
} from "discourse/modifiers/d-autocomplete";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import Button from "discourse/plugins/chat/discourse/components/chat/composer/button";
|
||||
import DButton from "discourse/plugins/chat/discourse/components/chat/composer/button";
|
||||
import ChatComposerDropdown from "discourse/plugins/chat/discourse/components/chat-composer-dropdown";
|
||||
import ChatComposerMessageDetails from "discourse/plugins/chat/discourse/components/chat-composer-message-details";
|
||||
import ChatComposerUploads from "discourse/plugins/chat/discourse/components/chat-composer-uploads";
|
||||
|
@ -758,7 +758,7 @@ export default class ChatComposer extends Component {
|
|||
|
||||
{{#if this.inlineButtons.length}}
|
||||
{{#each this.inlineButtons as |button|}}
|
||||
<Button
|
||||
<DButton
|
||||
@icon={{button.icon}}
|
||||
class="-{{button.id}}"
|
||||
disabled={{or this.disabled button.disabled}}
|
||||
|
@ -780,7 +780,7 @@ export default class ChatComposer extends Component {
|
|||
/>
|
||||
|
||||
{{#if this.site.desktopView}}
|
||||
<Button
|
||||
<DButton
|
||||
@icon="paper-plane"
|
||||
class="-send"
|
||||
title={{i18n "chat.composer.send"}}
|
||||
|
@ -794,7 +794,7 @@ export default class ChatComposer extends Component {
|
|||
{{/if}}
|
||||
</div>
|
||||
{{#if this.site.mobileView}}
|
||||
<Button
|
||||
<DButton
|
||||
@icon="paper-plane"
|
||||
class="-send"
|
||||
title={{i18n "chat.composer.send"}}
|
||||
|
|
|
@ -103,37 +103,6 @@ RSpec.describe "Bookmark message", type: :system do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with reminder notification cleanup" do
|
||||
it "removes bookmark reminder notification when bookmark is deleted" do
|
||||
chat_page.visit_channel(category_channel_1)
|
||||
channel_page.bookmark_message(message_1)
|
||||
bookmark_modal.fill_name("Check this out later")
|
||||
bookmark_modal.select_preset_reminder(:tomorrow)
|
||||
|
||||
expect(channel_page).to have_bookmarked_message(message_1)
|
||||
|
||||
bookmark = Bookmark.find_by(bookmarkable: message_1, user: current_user)
|
||||
Chat::MessageBookmarkable.send_reminder_notification(
|
||||
bookmark,
|
||||
data: {
|
||||
title: bookmark.bookmarkable.chat_channel.title(current_user),
|
||||
bookmarkable_url: bookmark.bookmarkable.url,
|
||||
},
|
||||
)
|
||||
|
||||
user_menu.open
|
||||
|
||||
expect(page).to have_css("#quick-access-all-notifications .bookmark-reminder")
|
||||
|
||||
channel_page.bookmark_message(message_1)
|
||||
bookmark_modal.delete
|
||||
bookmark_modal.confirm_delete
|
||||
user_menu.open
|
||||
|
||||
expect(page).to have_no_css("#quick-access-all-notifications .bookmark-reminder")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when mobile", mobile: true do
|
||||
|
|
|
@ -5,6 +5,13 @@ import { action } from "@ember/object";
|
|||
import { LinkTo } from "@ember/routing";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
|
||||
function isPreseeded(llm) {
|
||||
if (llm.id < 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandableList extends Component {
|
||||
@tracked isExpanded = false;
|
||||
|
@ -142,13 +149,24 @@ export default class AiFeatureCard extends Component {
|
|||
@maxItemsToShow={{5}}
|
||||
as |llm index isLastItem|
|
||||
>
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
@model={{llm.id}}
|
||||
class="ai-feature-card__llm-link"
|
||||
>
|
||||
{{concat llm.name (unless (isLastItem index) ", ")}}
|
||||
</LinkTo>
|
||||
{{#if (isPreseeded llm)}}
|
||||
<DTooltip>
|
||||
<:trigger>
|
||||
{{concat llm.name (unless (isLastItem index) ", ")}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{i18n "discourse_ai.llms.seeded_warning"}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.discourse-ai-llms.edit"
|
||||
@model={{llm.id}}
|
||||
class="ai-feature-card__llm-link"
|
||||
>
|
||||
{{concat llm.name (unless (isLastItem index) ", ")}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ExpandableList>
|
||||
{{else}}
|
||||
<span class="ai-feature-card__label">
|
||||
|
|
|
@ -224,7 +224,7 @@ export default class AiSearchDiscoveries extends Component {
|
|||
<DButton
|
||||
@action={{this.continueConversation}}
|
||||
@label={{this.continueConvoBtnLabel}}
|
||||
class="btn-small"
|
||||
class="btn-default btn-small"
|
||||
>
|
||||
<AiIndicatorWave @loading={{this.loadingConversationTopic}} />
|
||||
</DButton>
|
||||
|
|
|
@ -120,7 +120,7 @@ export default class AiToolListEditor extends Component {
|
|||
<DButton
|
||||
@translatedLabel={{i18n "discourse_ai.tools.import"}}
|
||||
@icon="upload"
|
||||
class="btn btn-small ai-tool-list-editor__import-button"
|
||||
class="btn btn-default btn-small ai-tool-list-editor__import-button"
|
||||
@action={{this.importTool}}
|
||||
/>
|
||||
<DMenu
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class AiEditSuggestionButton extends Component {
|
|||
<template>
|
||||
{{#unless @outletArgs.newValue}}
|
||||
<DButton
|
||||
class="btn-small btn-ai-suggest-edit"
|
||||
class="btn-default btn-small btn-ai-suggest-edit"
|
||||
@action={{this.suggest}}
|
||||
@icon="discourse-sparkles"
|
||||
@label="discourse_ai.ai_helper.fast_edit.suggest_button"
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ai-discobot-discoveries {
|
||||
padding: var(--search-result-element-padding);
|
||||
}
|
||||
|
||||
.ai-search-discoveries {
|
||||
&__regular-results-title {
|
||||
margin-top: 0.5em;
|
||||
|
@ -146,7 +150,6 @@
|
|||
grid-column-start: 2;
|
||||
grid-row: 1 / -1;
|
||||
box-sizing: border-box;
|
||||
padding: var(--search-result-element-padding);
|
||||
margin: 0.75em 0 0 0;
|
||||
border-left: 1px solid var(--primary-low);
|
||||
|
||||
|
|
|
@ -85,7 +85,6 @@ en:
|
|||
composer_ai_helper_allowed_groups: "Users on these groups will see the AI helper button in the composer."
|
||||
ai_helper_allowed_in_pm: "Enable the composer's AI helper in PMs."
|
||||
ai_helper_model: "Model to use for the AI helper."
|
||||
ai_helper_custom_prompts_allowed_groups: "Users on these groups will see the custom prompt option in the AI helper."
|
||||
ai_helper_automatic_chat_thread_title_delay: "Delay in minutes before the AI helper automatically sets the chat thread title."
|
||||
ai_helper_automatic_chat_thread_title: "Automatically set the chat thread titles based on thread contents."
|
||||
ai_helper_illustrate_post_model: "Model to use for the composer AI helper's illustrate post feature"
|
||||
|
|
|
@ -16,7 +16,15 @@ module DiscourseAi
|
|||
end
|
||||
|
||||
plugin.add_to_serializer(:current_user, :can_use_custom_prompts) do
|
||||
scope.user.in_any_groups?(SiteSetting.ai_helper_custom_prompts_allowed_groups_map)
|
||||
return [] if !SiteSetting.ai_helper_enabled
|
||||
|
||||
custom_prompt_allowed_group_ids =
|
||||
DB.query_single(
|
||||
"SELECT allowed_group_ids FROM ai_personas WHERE id = :customp_prompt_persona_id",
|
||||
customp_prompt_persona_id: SiteSetting.ai_helper_custom_prompt_persona,
|
||||
).flatten
|
||||
|
||||
scope.user.in_any_groups?(custom_prompt_allowed_group_ids)
|
||||
end
|
||||
|
||||
plugin.on(:chat_message_created) do |message, channel, user, extra|
|
||||
|
|
|
@ -5,10 +5,15 @@ RSpec.describe "AI Composer helper", type: :system do
|
|||
fab!(:non_member_group) { Fabricate(:group) }
|
||||
fab!(:embedding_definition)
|
||||
|
||||
fab!(:custom_prompts_persona) do
|
||||
Fabricate(:ai_persona, allowed_group_ids: [Group::AUTO_GROUPS[:admins]])
|
||||
end
|
||||
|
||||
before do
|
||||
enable_current_plugin
|
||||
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
|
||||
assign_fake_provider_to(:ai_default_llm_model)
|
||||
SiteSetting.ai_helper_custom_prompt_persona = custom_prompts_persona.id
|
||||
SiteSetting.ai_helper_enabled = true
|
||||
Jobs.run_immediately!
|
||||
sign_in(user)
|
||||
|
@ -116,7 +121,7 @@ RSpec.describe "AI Composer helper", type: :system do
|
|||
|
||||
context "when not a member of custom prompt group" do
|
||||
let(:mode) { DiscourseAi::AiHelper::Assistant::CUSTOM_PROMPT }
|
||||
before { SiteSetting.ai_helper_custom_prompts_allowed_groups = non_member_group.id.to_s }
|
||||
before { custom_prompts_persona.update!(allowed_group_ids: [non_member_group.id]) }
|
||||
|
||||
it "does not show custom prompt option" do
|
||||
trigger_composer_helper(input)
|
||||
|
|
|
@ -254,7 +254,7 @@ export default class DiscoursePostEvent extends Component {
|
|||
|
||||
{{#if @onClose}}
|
||||
<DButton
|
||||
class="btn-small discourse-post-event-close"
|
||||
class="btn-default btn-small discourse-post-event-close"
|
||||
@icon="xmark"
|
||||
@action={{@onClose}}
|
||||
/>
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class EditChannel extends Component {
|
|||
<DButton
|
||||
@action={{@closeModal}}
|
||||
@label="chat_integration.edit_channel_modal.cancel"
|
||||
class="btn-large"
|
||||
class="btn-default btn-large"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
||||
|
|
|
@ -232,7 +232,7 @@ export default class EditRule extends Component {
|
|||
<DButton
|
||||
@label="chat_integration.edit_rule_modal.cancel"
|
||||
@action={{@closeModal}}
|
||||
class="btn-large"
|
||||
class="btn-default btn-large"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class TestIntegration extends Component {
|
|||
<DButton
|
||||
@action={{@closeModal}}
|
||||
@label="chat_integration.test_modal.close"
|
||||
class="btn-large"
|
||||
class="btn-default btn-large"
|
||||
/>
|
||||
</ConditionalLoadingSpinner>
|
||||
</:footer>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { on } from "@ember/modifier";
|
|||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import FlatButton from "discourse/components/flat-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import StyleguideExample from "discourse/plugins/styleguide/discourse/components/styleguide-example";
|
||||
|
||||
|
@ -14,7 +13,7 @@ const Buttons = <template>
|
|||
@icon="xmark"
|
||||
@translatedTitle={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -25,7 +24,7 @@ const Buttons = <template>
|
|||
@icon="xmark"
|
||||
@translatedTitle={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -35,7 +34,7 @@ const Buttons = <template>
|
|||
<DButton
|
||||
@translatedLabel={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -45,7 +44,7 @@ const Buttons = <template>
|
|||
<DButton
|
||||
@translatedLabel={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -58,7 +57,7 @@ const Buttons = <template>
|
|||
@icon="plus"
|
||||
@translatedLabel={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -69,7 +68,7 @@ const Buttons = <template>
|
|||
@icon="plus"
|
||||
@translatedLabel={{bs.text}}
|
||||
@disabled={{bs.disabled}}
|
||||
class={{bs.class}}
|
||||
class={{concatClass "btn-default" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
@ -123,30 +122,20 @@ const Buttons = <template>
|
|||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample @title=".btn-flat - sizes (large, default, small)">
|
||||
{{#each @dummy.buttonSizes as |bs|}}
|
||||
<FlatButton
|
||||
@icon="trash-can"
|
||||
@disabled={{bs.disabled}}
|
||||
@translatedTitle={{bs.title}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample @title=".btn-flat - states">
|
||||
{{#each @dummy.buttonStates as |bs|}}
|
||||
<FlatButton
|
||||
@icon="trash-can"
|
||||
@disabled={{bs.disabled}}
|
||||
@translatedTitle={{bs.title}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample
|
||||
@title="<DButton> btn-flat btn-text - sizes (large, default, small)"
|
||||
>
|
||||
{{#each @dummy.buttonSizes as |bs|}}
|
||||
<DButton
|
||||
@icon="trash-can"
|
||||
@disabled={{bs.disabled}}
|
||||
@translatedTitle={{bs.title}}
|
||||
class={{concatClass "btn-flat" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample @title="btn-flat - states">
|
||||
{{#each @dummy.buttonStates as |bs|}}
|
||||
<DButton
|
||||
@icon="trash-can"
|
||||
@disabled={{bs.disabled}}
|
||||
@translatedLabel={{bs.text}}
|
||||
class={{concatClass "btn-flat" bs.class}}
|
||||
|
@ -154,12 +143,25 @@ const Buttons = <template>
|
|||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample @title="<DButton> btn-flat btn-text - states">
|
||||
<StyleguideExample @title="btn-transparent - states">
|
||||
{{#each @dummy.buttonStates as |bs|}}
|
||||
<DButton
|
||||
@icon="trash-can"
|
||||
@disabled={{bs.disabled}}
|
||||
@translatedLabel={{bs.text}}
|
||||
class={{concatClass "btn-flat" bs.class}}
|
||||
class={{concatClass "btn-transparent" bs.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample @title="Button link">
|
||||
{{#each @dummy.buttonStates as |bs|}}
|
||||
<DButton
|
||||
@icon="trash-can"
|
||||
@translatedLabel={{bs.text}}
|
||||
@display="link"
|
||||
class={{bs.class}}
|
||||
@disabled={{bs.disabled}}
|
||||
/>
|
||||
{{/each}}
|
||||
</StyleguideExample>
|
||||
|
|
|
@ -351,13 +351,13 @@ export function createData(store) {
|
|||
|
||||
buttonSizes: [
|
||||
{ class: "btn-large", text: "large" },
|
||||
{ class: "btn-default", text: "default" },
|
||||
{ class: "", text: "default" },
|
||||
{ class: "btn-small", text: "small" },
|
||||
],
|
||||
|
||||
buttonStates: [
|
||||
{ class: "", text: "normal" },
|
||||
{ class: "btn-hover", text: "hover" },
|
||||
{ class: "btn-active", text: "active" },
|
||||
{ disabled: true, text: "disabled" },
|
||||
],
|
||||
|
||||
|
|
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
|
@ -116,8 +116,8 @@ importers:
|
|||
specifier: 3.5.3
|
||||
version: 3.5.3
|
||||
puppeteer-core:
|
||||
specifier: ^24.22.3
|
||||
version: 24.22.3
|
||||
specifier: ^24.23.0
|
||||
version: 24.23.0
|
||||
squoosh:
|
||||
specifier: https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d
|
||||
version: https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d
|
||||
|
@ -1132,7 +1132,7 @@ importers:
|
|||
devDependencies:
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/node@24.6.0)(jsdom@25.0.1)(terser@5.44.0)
|
||||
version: 3.2.4(@types/node@24.6.1)(jsdom@25.0.1)(terser@5.44.0)
|
||||
|
||||
app/assets/javascripts/truth-helpers:
|
||||
dependencies:
|
||||
|
@ -2904,8 +2904,8 @@ packages:
|
|||
'@types/minimatch@3.0.5':
|
||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||
|
||||
'@types/node@24.6.0':
|
||||
resolution: {integrity: sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==}
|
||||
'@types/node@24.6.1':
|
||||
resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==}
|
||||
|
||||
'@types/qs@6.14.0':
|
||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||
|
@ -3368,8 +3368,8 @@ packages:
|
|||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
b4a@1.7.2:
|
||||
resolution: {integrity: sha512-DyUOdz+E8R6+sruDpQNOaV0y/dBbV6X/8ZkxrDcR0Ifc3BgKlpgG0VAtfOozA0eMtJO5GGe9FsZhueLs00pTww==}
|
||||
b4a@1.7.3:
|
||||
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
|
||||
peerDependencies:
|
||||
react-native-b4a: '*'
|
||||
peerDependenciesMeta:
|
||||
|
@ -3486,8 +3486,8 @@ packages:
|
|||
bare-events@2.7.0:
|
||||
resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==}
|
||||
|
||||
bare-fs@4.4.4:
|
||||
resolution: {integrity: sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==}
|
||||
bare-fs@4.4.5:
|
||||
resolution: {integrity: sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==}
|
||||
engines: {bare: '>=1.16.0'}
|
||||
peerDependencies:
|
||||
bare-buffer: '*'
|
||||
|
@ -4479,8 +4479,8 @@ packages:
|
|||
resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
devtools-protocol@0.0.1495869:
|
||||
resolution: {integrity: sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==}
|
||||
devtools-protocol@0.0.1508733:
|
||||
resolution: {integrity: sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==}
|
||||
|
||||
diff@7.0.0:
|
||||
resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==}
|
||||
|
@ -7623,8 +7623,8 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
puppeteer-core@24.22.3:
|
||||
resolution: {integrity: sha512-M/Jhg4PWRANSbL/C9im//Yb55wsWBS5wdp+h59iwM+EPicVQQCNs56iC5aEAO7avfDPRfxs4MM16wHjOYHNJEw==}
|
||||
puppeteer-core@24.23.0:
|
||||
resolution: {integrity: sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
qs@6.13.0:
|
||||
|
@ -9055,8 +9055,8 @@ packages:
|
|||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webdriver-bidi-protocol@0.2.11:
|
||||
resolution: {integrity: sha512-Y9E1/oi4XMxcR8AT0ZC4OvYntl34SPgwjmELH+owjBr0korAX4jKgZULBWILGCVGdVCQ0dodTToIETozhG8zvA==}
|
||||
webdriver-bidi-protocol@0.3.6:
|
||||
resolution: {integrity: sha512-mlGndEOA9yK9YAbvtxaPTqdi/kaCWYYfwrZvGzcmkr/3lWM+tQj53BxtpVd6qbC6+E5OnHXgCcAhre6AkXzxjA==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
@ -11400,7 +11400,7 @@ snapshots:
|
|||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/chai-as-promised@7.1.8':
|
||||
dependencies:
|
||||
|
@ -11414,11 +11414,11 @@ snapshots:
|
|||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
|
@ -11441,7 +11441,7 @@ snapshots:
|
|||
|
||||
'@types/express-serve-static-core@4.19.6':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.5
|
||||
|
@ -11455,11 +11455,11 @@ snapshots:
|
|||
|
||||
'@types/fs-extra@5.1.0':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/fs-extra@8.1.5':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/glob@9.0.0':
|
||||
dependencies:
|
||||
|
@ -11488,7 +11488,7 @@ snapshots:
|
|||
|
||||
'@types/minimatch@3.0.5': {}
|
||||
|
||||
'@types/node@24.6.0':
|
||||
'@types/node@24.6.1':
|
||||
dependencies:
|
||||
undici-types: 7.13.0
|
||||
|
||||
|
@ -11503,19 +11503,19 @@ snapshots:
|
|||
'@types/rimraf@2.0.5':
|
||||
dependencies:
|
||||
'@types/glob': 9.0.0
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/rsvp@4.0.9': {}
|
||||
|
||||
'@types/send@0.17.5':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
|
||||
'@types/serve-static@1.15.8':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
'@types/send': 0.17.5
|
||||
|
||||
'@types/sizzle@2.3.10': {}
|
||||
|
@ -11534,7 +11534,7 @@ snapshots:
|
|||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.39.0(typescript@5.9.2)':
|
||||
|
@ -11591,13 +11591,13 @@ snapshots:
|
|||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.0.0(@types/node@24.6.0)(terser@5.44.0))':
|
||||
'@vitest/mocker@3.2.4(vite@7.0.0(@types/node@24.6.1)(terser@5.44.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.19
|
||||
optionalDependencies:
|
||||
vite: 7.0.0(@types/node@24.6.0)(terser@5.44.0)
|
||||
vite: 7.0.0(@types/node@24.6.1)(terser@5.44.0)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
|
@ -12029,7 +12029,7 @@ snapshots:
|
|||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
b4a@1.7.2: {}
|
||||
b4a@1.7.3: {}
|
||||
|
||||
babel-import-util@1.4.1: {}
|
||||
|
||||
|
@ -12163,7 +12163,7 @@ snapshots:
|
|||
|
||||
bare-events@2.7.0: {}
|
||||
|
||||
bare-fs@4.4.4:
|
||||
bare-fs@4.4.5:
|
||||
dependencies:
|
||||
bare-events: 2.7.0
|
||||
bare-path: 3.0.0
|
||||
|
@ -12837,7 +12837,7 @@ snapshots:
|
|||
|
||||
chrome-launcher@1.2.1:
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
escape-string-regexp: 4.0.0
|
||||
is-wsl: 2.2.0
|
||||
lighthouse-logger: 2.0.2
|
||||
|
@ -12854,9 +12854,9 @@ snapshots:
|
|||
|
||||
chrome-trace-event@1.0.4: {}
|
||||
|
||||
chromium-bidi@9.1.0(devtools-protocol@0.0.1495869):
|
||||
chromium-bidi@9.1.0(devtools-protocol@0.0.1508733):
|
||||
dependencies:
|
||||
devtools-protocol: 0.0.1495869
|
||||
devtools-protocol: 0.0.1508733
|
||||
mitt: 3.0.1
|
||||
zod: 3.25.76
|
||||
|
||||
|
@ -13275,7 +13275,7 @@ snapshots:
|
|||
|
||||
detect-newline@4.0.1: {}
|
||||
|
||||
devtools-protocol@0.0.1495869: {}
|
||||
devtools-protocol@0.0.1508733: {}
|
||||
|
||||
diff@7.0.0: {}
|
||||
|
||||
|
@ -14014,7 +14014,7 @@ snapshots:
|
|||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.18
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
|
@ -15750,7 +15750,7 @@ snapshots:
|
|||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
|
@ -17256,14 +17256,14 @@ snapshots:
|
|||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
puppeteer-core@24.22.3:
|
||||
puppeteer-core@24.23.0:
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.10.10
|
||||
chromium-bidi: 9.1.0(devtools-protocol@0.0.1495869)
|
||||
chromium-bidi: 9.1.0(devtools-protocol@0.0.1508733)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
devtools-protocol: 0.0.1495869
|
||||
devtools-protocol: 0.0.1508733
|
||||
typed-query-selector: 2.12.0
|
||||
webdriver-bidi-protocol: 0.2.11
|
||||
webdriver-bidi-protocol: 0.3.6
|
||||
ws: 8.18.3
|
||||
transitivePeerDependencies:
|
||||
- bare-buffer
|
||||
|
@ -18351,7 +18351,7 @@ snapshots:
|
|||
pump: 3.0.3
|
||||
tar-stream: 3.1.7
|
||||
optionalDependencies:
|
||||
bare-fs: 4.4.4
|
||||
bare-fs: 4.4.5
|
||||
bare-path: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- bare-buffer
|
||||
|
@ -18359,7 +18359,7 @@ snapshots:
|
|||
|
||||
tar-stream@3.1.7:
|
||||
dependencies:
|
||||
b4a: 1.7.2
|
||||
b4a: 1.7.3
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.23.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -18490,7 +18490,7 @@ snapshots:
|
|||
|
||||
text-decoder@1.2.3:
|
||||
dependencies:
|
||||
b4a: 1.7.2
|
||||
b4a: 1.7.3
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
|
@ -18866,13 +18866,13 @@ snapshots:
|
|||
x-is-array: 0.1.0
|
||||
x-is-string: 0.1.0
|
||||
|
||||
vite-node@3.2.4(@types/node@24.6.0)(terser@5.44.0):
|
||||
vite-node@3.2.4(@types/node@24.6.1)(terser@5.44.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.0.0(@types/node@24.6.0)(terser@5.44.0)
|
||||
vite: 7.0.0(@types/node@24.6.1)(terser@5.44.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
|
@ -18887,7 +18887,7 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@7.0.0(@types/node@24.6.0)(terser@5.44.0):
|
||||
vite@7.0.0(@types/node@24.6.1)(terser@5.44.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.10
|
||||
fdir: 6.4.6(picomatch@4.0.2)
|
||||
|
@ -18896,15 +18896,15 @@ snapshots:
|
|||
rollup: 4.44.0
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
fsevents: 2.3.3
|
||||
terser: 5.44.0
|
||||
|
||||
vitest@3.2.4(@types/node@24.6.0)(jsdom@25.0.1)(terser@5.44.0):
|
||||
vitest@3.2.4(@types/node@24.6.1)(jsdom@25.0.1)(terser@5.44.0):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.0.0(@types/node@24.6.0)(terser@5.44.0))
|
||||
'@vitest/mocker': 3.2.4(vite@7.0.0(@types/node@24.6.1)(terser@5.44.0))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
|
@ -18922,11 +18922,11 @@ snapshots:
|
|||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.0.0(@types/node@24.6.0)(terser@5.44.0)
|
||||
vite-node: 3.2.4(@types/node@24.6.0)(terser@5.44.0)
|
||||
vite: 7.0.0(@types/node@24.6.1)(terser@5.44.0)
|
||||
vite-node: 3.2.4(@types/node@24.6.1)(terser@5.44.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.6.0
|
||||
'@types/node': 24.6.1
|
||||
jsdom: 25.0.1(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
|
@ -19050,7 +19050,7 @@ snapshots:
|
|||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webdriver-bidi-protocol@0.2.11: {}
|
||||
webdriver-bidi-protocol@0.3.6: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
|
|
|
@ -13,24 +13,4 @@ describe ActionDispatch::Session::DiscourseCookieStore, type: :request do
|
|||
expect(response.cookies["_forum_session"]).not_to be_present
|
||||
expect(session[:_csrf_token]).to eq(csrf_token)
|
||||
end
|
||||
|
||||
describe "Cookie overflow" do
|
||||
context "when cookie size exceeds limit" do
|
||||
let(:fake_logger) { FakeLogger.new }
|
||||
|
||||
before do
|
||||
Rails.logger.broadcast_to(fake_logger)
|
||||
allow_any_instance_of(ActionController::RequestForgeryProtection).to receive(
|
||||
:generate_csrf_token,
|
||||
).and_return(SecureRandom.urlsafe_base64(4097))
|
||||
end
|
||||
|
||||
after { Rails.logger.stop_broadcasting_to(fake_logger) }
|
||||
|
||||
it "logs an error" do
|
||||
get "/session/csrf.json"
|
||||
expect(fake_logger.errors).to include(/Cookie overflow occurred.*"_csrf_token"=>/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -285,6 +285,19 @@ RSpec.describe Auth::DefaultCurrentUserProvider do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when impersonating another user" do
|
||||
it "should not update User#last_seen_at" do
|
||||
old_timestamp = 1.week.ago
|
||||
user.update!(last_seen_at: old_timestamp)
|
||||
User.any_instance.stubs(:is_impersonating).returns(true)
|
||||
|
||||
provider2 = provider("/", "HTTP_COOKIE" => "_t=#{cookie}")
|
||||
u = provider2.current_user
|
||||
|
||||
expect(u.reload.last_seen_at).to eq_time(old_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
it "should not cache an invalid user when Rails hasn't set `path_parameters` on the request yet" do
|
||||
SiteSetting.login_required = true
|
||||
user = Fabricate(:user)
|
||||
|
|
|
@ -1621,6 +1621,32 @@ describe PostRevisor do
|
|||
}.not_to change { post.topic.bumped_at }
|
||||
end
|
||||
|
||||
it "doesn't bump the topic when editing the topic title" do
|
||||
expect {
|
||||
post_revisor.revise!(
|
||||
post.user,
|
||||
{ title: "This is an updated topic title" },
|
||||
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.seconds,
|
||||
)
|
||||
}.not_to change { post.topic.bumped_at }
|
||||
end
|
||||
|
||||
it "doesn't bump the topic when editing the topic category" do
|
||||
expect {
|
||||
post_revisor.revise!(
|
||||
post.user,
|
||||
{ category_id: Fabricate(:category).id },
|
||||
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.seconds,
|
||||
)
|
||||
}.not_to change { post.topic.bumped_at }
|
||||
end
|
||||
|
||||
it "doesn't bump the topic when editing tags" do
|
||||
expect { post_revisor.revise!(post.user, { tags: %w[totally update] }) }.not_to change {
|
||||
post.topic.bumped_at
|
||||
}
|
||||
end
|
||||
|
||||
describe "should_bump_topic plugin modifier" do
|
||||
let(:plugin_instance) { Plugin::Instance.new }
|
||||
let(:modifier_return_value) { nil }
|
||||
|
|
|
@ -178,100 +178,4 @@ RSpec.describe Bookmark do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "reminder notifications cleanup" do
|
||||
fab!(:user)
|
||||
|
||||
it "deletes reminder notifications when bookmark is destroyed" do
|
||||
bookmark = Fabricate(:bookmark, user: user, bookmarkable: post)
|
||||
|
||||
notification =
|
||||
Fabricate(
|
||||
:bookmark_reminder_notification,
|
||||
user:,
|
||||
post:,
|
||||
data: {
|
||||
bookmark_id: bookmark.id,
|
||||
bookmarkable_type: "Post",
|
||||
bookmarkable_id: post.id,
|
||||
title: post.topic.title,
|
||||
bookmarkable_url: post.url,
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
expect { bookmark.destroy }.to change { Notification.find_by(id: notification.id) }.from(
|
||||
notification,
|
||||
).to(nil)
|
||||
end
|
||||
|
||||
it "does not delete other user's reminder notifications" do
|
||||
other_user = Fabricate(:user)
|
||||
bookmark = Fabricate(:bookmark, user: user, bookmarkable: post)
|
||||
|
||||
user_notification =
|
||||
Fabricate(
|
||||
:bookmark_reminder_notification,
|
||||
user:,
|
||||
post:,
|
||||
data: {
|
||||
bookmark_id: bookmark.id,
|
||||
bookmarkable_type: "Post",
|
||||
bookmarkable_id: post.id,
|
||||
title: post.topic.title,
|
||||
bookmarkable_url: post.url,
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
other_notification =
|
||||
Fabricate(
|
||||
:bookmark_reminder_notification,
|
||||
user: other_user,
|
||||
post: post,
|
||||
data: {
|
||||
bookmark_id: bookmark.id,
|
||||
bookmarkable_type: "Post",
|
||||
bookmarkable_id: post.id,
|
||||
title: post.topic.title,
|
||||
bookmarkable_url: post.url,
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
expect { bookmark.destroy }.to change { Notification.find_by(id: user_notification.id) }
|
||||
.from(user_notification)
|
||||
.to(nil)
|
||||
.and(not_change { Notification.find_by(id: other_notification.id) })
|
||||
end
|
||||
|
||||
it "does not delete non-reminder notifications" do
|
||||
bookmark = Fabricate(:bookmark, user: user, bookmarkable: post)
|
||||
|
||||
reminder_notification =
|
||||
Fabricate(
|
||||
:bookmark_reminder_notification,
|
||||
user:,
|
||||
post:,
|
||||
data: {
|
||||
bookmark_id: bookmark.id,
|
||||
bookmarkable_type: "Post",
|
||||
bookmarkable_id: post.id,
|
||||
title: post.topic.title,
|
||||
bookmarkable_url: post.url,
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
other_notification =
|
||||
Fabricate(
|
||||
:notification,
|
||||
user:,
|
||||
post:,
|
||||
notification_type: Notification.types[:liked],
|
||||
data: { post_number: post.post_number }.to_json,
|
||||
)
|
||||
|
||||
expect { bookmark.destroy }.to change { Notification.find_by(id: reminder_notification.id) }
|
||||
.from(reminder_notification)
|
||||
.to(nil)
|
||||
.and(not_change { Notification.find_by(id: other_notification.id) })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,6 +38,34 @@ RSpec.describe Category do
|
|||
expect(cats.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it "validates format of color" do
|
||||
expect(Fabricate.build(:category, color: "fff", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, color: "1ac", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, color: "fffeee", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, color: "FF11CC", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, color: "ABC", user:)).to be_valid
|
||||
|
||||
expect(Fabricate.build(:category, color: "ffq1e1", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, color: "", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, color: "f", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, color: "21", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, color: "XCA", user:)).to_not be_valid
|
||||
end
|
||||
|
||||
it "validates format of text_color" do
|
||||
expect(Fabricate.build(:category, text_color: "fff", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, text_color: "1ac", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, text_color: "fffeee", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, text_color: "FF11CC", user:)).to be_valid
|
||||
expect(Fabricate.build(:category, text_color: "ABC", user:)).to be_valid
|
||||
|
||||
expect(Fabricate.build(:category, text_color: "ffq1e1", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, text_color: "", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, text_color: "f", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, text_color: "21", user:)).to_not be_valid
|
||||
expect(Fabricate.build(:category, text_color: "XCA", user:)).to_not be_valid
|
||||
end
|
||||
|
||||
describe "Associations" do
|
||||
it { is_expected.to have_one(:category_setting).dependent(:destroy) }
|
||||
|
||||
|
|
|
@ -1859,64 +1859,6 @@ RSpec.describe TopicsController do
|
|||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
context "when using SiteSetting.disable_category_edit_notifications" do
|
||||
it "doesn't bump the topic if the setting is enabled" do
|
||||
SiteSetting.disable_category_edit_notifications = true
|
||||
last_bumped_at = topic.bumped_at
|
||||
expect(last_bumped_at).not_to be_nil
|
||||
|
||||
expect do
|
||||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||||
end.to change { topic.reload.category_id }.to(category.id)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(topic.reload.bumped_at).to eq_time(last_bumped_at)
|
||||
end
|
||||
|
||||
it "bumps the topic if the setting is disabled" do
|
||||
SiteSetting.disable_category_edit_notifications = false
|
||||
last_bumped_at = topic.bumped_at
|
||||
expect(last_bumped_at).not_to be_nil
|
||||
|
||||
expect do
|
||||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||||
end.to change { topic.reload.category_id }.to(category.id)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(topic.reload.bumped_at).not_to eq_time(last_bumped_at)
|
||||
end
|
||||
end
|
||||
|
||||
context "when using SiteSetting.disable_tags_edit_notifications" do
|
||||
fab!(:t1, :tag)
|
||||
fab!(:t2, :tag)
|
||||
let(:tags) { [t1, t2] }
|
||||
|
||||
it "doesn't bump the topic if the setting is enabled" do
|
||||
SiteSetting.disable_tags_edit_notifications = true
|
||||
last_bumped_at = topic.bumped_at
|
||||
expect(last_bumped_at).not_to be_nil
|
||||
|
||||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: tags.map(&:name) }
|
||||
|
||||
expect(topic.reload.tags).to match_array(tags)
|
||||
expect(response.status).to eq(200)
|
||||
expect(topic.reload.bumped_at).to eq_time(last_bumped_at)
|
||||
end
|
||||
|
||||
it "bumps the topic if the setting is disabled" do
|
||||
SiteSetting.disable_tags_edit_notifications = false
|
||||
last_bumped_at = topic.bumped_at
|
||||
expect(last_bumped_at).not_to be_nil
|
||||
|
||||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: tags.map(&:name) }
|
||||
|
||||
expect(topic.reload.tags).to match_array(tags)
|
||||
expect(response.status).to eq(200)
|
||||
expect(topic.reload.bumped_at).not_to eq_time(last_bumped_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when first post is locked" do
|
||||
it "blocks user from editing even if they are in 'edit_all_topic_groups' and 'edit_all_post_groups'" do
|
||||
SiteSetting.edit_all_topic_groups = Group::AUTO_GROUPS[:trust_level_3]
|
||||
|
|
|
@ -7705,14 +7705,15 @@ RSpec.describe UsersController do
|
|||
expect(notifications.first["data"]["bookmark_id"]).to eq(bookmark_with_reminder.id)
|
||||
end
|
||||
|
||||
it "doesn’t show unread notifications when the bookmark has been deleted" do
|
||||
it "shows unread notifications even if the bookmark has been deleted if they have bookmarkable data" do
|
||||
bookmark_with_reminder.destroy!
|
||||
|
||||
get "/u/#{user.username}/user-menu-bookmarks"
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
notifications = response.parsed_body["notifications"]
|
||||
expect(notifications.size).to eq(0)
|
||||
expect(notifications.size).to eq(1)
|
||||
expect(notifications.first["data"]["bookmark_id"]).to eq(bookmark_with_reminder.id)
|
||||
end
|
||||
|
||||
it "does not show unread notifications if the bookmark has been deleted if they only have the bookmark_id data" do
|
||||
|
@ -8022,9 +8023,10 @@ RSpec.describe UsersController do
|
|||
number_of_suspensions
|
||||
warnings_received_count
|
||||
number_of_rejected_posts
|
||||
can_remove_password?
|
||||
].each do |info|
|
||||
user_instance.expects(info).returns(user.public_send(info))
|
||||
result[info.to_s] = user.public_send(info)
|
||||
result[info.to_s.delete_suffix("?")] = user.public_send(info)
|
||||
end
|
||||
|
||||
sign_in(admin)
|
||||
|
|
73
spec/system/edit_category_general_spec.rb
Normal file
73
spec/system/edit_category_general_spec.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Edit Category General", type: :system do
|
||||
fab!(:admin)
|
||||
fab!(:category)
|
||||
let(:category_page) { PageObjects::Pages::Category.new }
|
||||
let(:form) { PageObjects::Components::FormKit.new(".form-kit") }
|
||||
before { sign_in(admin) }
|
||||
|
||||
context "when changing background color" do
|
||||
it "displays an error when the hex code is invalid" do
|
||||
category_page.visit_general(category)
|
||||
|
||||
form.field("color").component.find("input.hex-input").fill_in(with: "ABZ")
|
||||
category_page.save_settings
|
||||
expect(form.field("color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.non_hexdecimal"),
|
||||
)
|
||||
|
||||
form.field("color").component.find("input.hex-input").fill_in(with: "")
|
||||
category_page.save_settings
|
||||
expect(form.field("color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.cant_be_empty"),
|
||||
)
|
||||
|
||||
form.field("color").component.find("input.hex-input").fill_in(with: "A")
|
||||
category_page.save_settings
|
||||
expect(form.field("color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.incorrect_length"),
|
||||
)
|
||||
end
|
||||
|
||||
it "saves successfully when the hex code is valid" do
|
||||
category_page.visit_general(category)
|
||||
|
||||
form.field("color").component.find("input.hex-input").fill_in(with: "AB1")
|
||||
category_page.save_settings
|
||||
expect(form.field("color")).to have_no_errors
|
||||
end
|
||||
end
|
||||
|
||||
context "when changing text color" do
|
||||
it "displays an error when the hex code is invalid" do
|
||||
category_page.visit_general(category)
|
||||
|
||||
form.field("text_color").component.find("input.hex-input").fill_in(with: "ABZ")
|
||||
category_page.save_settings
|
||||
expect(form.field("text_color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.non_hexdecimal"),
|
||||
)
|
||||
|
||||
form.field("text_color").component.find("input.hex-input").fill_in(with: "")
|
||||
category_page.save_settings
|
||||
expect(form.field("text_color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.cant_be_empty"),
|
||||
)
|
||||
|
||||
form.field("text_color").component.find("input.hex-input").fill_in(with: "A")
|
||||
category_page.save_settings
|
||||
expect(form.field("text_color")).to have_errors(
|
||||
I18n.t("js.category.color_validations.incorrect_length"),
|
||||
)
|
||||
end
|
||||
|
||||
it "saves successfully when the hex code is valid" do
|
||||
category_page.visit_general(category)
|
||||
|
||||
form.field("text_color").component.find("input.hex-input").fill_in(with: "AB1")
|
||||
category_page.save_settings
|
||||
expect(form.field("text_color")).to have_no_errors
|
||||
end
|
||||
end
|
||||
end
|
|
@ -80,7 +80,7 @@ module PageObjects
|
|||
end
|
||||
|
||||
def has_no_errors?
|
||||
!has_css?(".form-kit__errors")
|
||||
has_no_css?(".form-kit__errors")
|
||||
end
|
||||
|
||||
def control_type
|
||||
|
|
|
@ -15,6 +15,11 @@ module PageObjects
|
|||
self
|
||||
end
|
||||
|
||||
def visit_general(category)
|
||||
page.visit("/c/#{category.slug}/edit/general")
|
||||
self
|
||||
end
|
||||
|
||||
def visit_edit_template(category)
|
||||
page.visit("/c/#{category.slug}/edit/topic-template")
|
||||
self
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue