+
+
+
+
@@ -63,7 +76,6 @@
@id="save-section"
@action={{action "save"}}
@class="btn-primary"
- @icon="plus"
@label="sidebar.sections.custom.save"
@disabled={{not this.model.valid}}
/>
diff --git a/app/assets/javascripts/discourse/app/widgets/render-glimmer.js b/app/assets/javascripts/discourse/app/widgets/render-glimmer.js
index a1176215c9..23caaa6f73 100644
--- a/app/assets/javascripts/discourse/app/widgets/render-glimmer.js
+++ b/app/assets/javascripts/discourse/app/widgets/render-glimmer.js
@@ -2,6 +2,7 @@ import templateOnly from "@ember/component/template-only";
import { setComponentTemplate } from "@ember/component";
import { tracked } from "@glimmer/tracking";
import { assert } from "@ember/debug";
+import { createWidgetFrom } from "discourse/widgets/widget";
/*
@@ -89,7 +90,9 @@ export default class RenderGlimmer {
template.name === "factory"
);
this.renderInto = renderInto;
- this.widget = widget;
+ if (widget) {
+ this.widget = widget;
+ }
this.template = template;
this.data = data;
}
@@ -108,7 +111,9 @@ export default class RenderGlimmer {
destroy() {
if (this._componentInfo) {
- this.widget._findView().unmountChildComponent(this._componentInfo);
+ this.parentMountWidgetComponent.unmountChildComponent(
+ this._componentInfo
+ );
}
}
@@ -132,7 +137,7 @@ export default class RenderGlimmer {
}
connectComponent() {
- const { element, template, widget } = this;
+ const { element, template } = this;
const component = templateOnly();
component.name = "Widgets/RenderGlimmer";
@@ -143,9 +148,39 @@ export default class RenderGlimmer {
component,
@tracked data: this.data,
};
- const parentMountWidgetComponent = widget._findView();
- parentMountWidgetComponent.mountChildComponent(this._componentInfo);
+
+ this.parentMountWidgetComponent.mountChildComponent(this._componentInfo);
+ }
+
+ get parentMountWidgetComponent() {
+ return this.widget?._findView() || this._emberView;
}
}
RenderGlimmer.prototype.type = "Widget";
+
+/**
+ * Define a widget shim which renders a Glimmer template. Designed for incrementally migrating
+ * a widget-based UI to Glimmer. Widget attrs will be made available to your template at `@data`.
+ * For more details, see documentation for the RenderGlimmer class.
+ * @param name - the widget's name (which can then be used in `.attach` elsewhere)
+ * @param tagName - a string describing a new wrapper element (e.g. `div.my-class`)
+ * @param template - a glimmer template compiled via ember-cli-htmlbars
+ */
+export function registerWidgetShim(name, tagName, template) {
+ const RenderGlimmerShim = class MyClass extends RenderGlimmer {
+ constructor(attrs) {
+ super(null, tagName, template, attrs);
+ return this;
+ }
+
+ get widget() {
+ return this.parentWidget;
+ }
+
+ didRenderWidget() {}
+ willRerenderWidget() {}
+ };
+
+ createWidgetFrom(RenderGlimmerShim, name, {});
+}
diff --git a/app/assets/javascripts/discourse/app/widgets/widget.js b/app/assets/javascripts/discourse/app/widgets/widget.js
index 0985f0283f..896edfefd3 100644
--- a/app/assets/javascripts/discourse/app/widgets/widget.js
+++ b/app/assets/javascripts/discourse/app/widgets/widget.js
@@ -30,6 +30,10 @@ export function queryRegistry(name) {
return _registry[name];
}
+export function deleteFromRegistry(name) {
+ return delete _registry[name];
+}
+
const _decorators = {};
export function decorateWidget(widgetName, cb) {
diff --git a/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js
index c70d2a37b6..0c8c12e842 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/ace-editor-test.js
@@ -22,6 +22,11 @@ module("Integration | Component | ace-editor", function (hooks) {
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
+ test("yaml editor", async function (assert) {
+ await render(hbs`
`);
+ assert.ok(exists(".ace_editor"), "it renders the ace editor");
+ });
+
test("disabled editor", async function (assert) {
await render(hbs`
diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/render-glimmer-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/render-glimmer-test.js
index 53d8b8d62d..497725a05b 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/widgets/render-glimmer-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/render-glimmer-test.js
@@ -4,9 +4,11 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, fillIn, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import widgetHbs from "discourse/widgets/hbs-compiler";
-import Widget from "discourse/widgets/widget";
+import Widget, { deleteFromRegistry } from "discourse/widgets/widget";
import ClassicComponent from "@ember/component";
-import RenderGlimmer from "discourse/widgets/render-glimmer";
+import RenderGlimmer, {
+ registerWidgetShim,
+} from "discourse/widgets/render-glimmer";
import { bind } from "discourse-common/utils/decorators";
class DemoWidget extends Widget {
@@ -126,12 +128,18 @@ module("Integration | Component | Widget | render-glimmer", function (hooks) {
this.registry.register("widget:demo-widget", DemoWidget);
this.registry.register("widget:toggle-demo-widget", ToggleDemoWidget);
this.registry.register("component:demo-component", DemoComponent);
+ registerWidgetShim(
+ "render-glimmer-test-shim",
+ "div.my-wrapper",
+ hbs`
{{@data.attr1}}`
+ );
});
hooks.afterEach(function () {
this.registry.unregister("widget:demo-widget");
this.registry.unregister("widget:toggle-demo-widget");
this.registry.unregister("component:demo-component");
+ deleteFromRegistry("render-glimmer-test-shim");
});
test("argument handling", async function (assert) {
@@ -310,4 +318,13 @@ module("Integration | Component | Widget | render-glimmer", function (hooks) {
await click(".toggleButton");
assert.strictEqual(query("div.glimmer-wrapper").innerText, "One");
});
+
+ test("registerWidgetShim can register a fake widget", async function (assert) {
+ await render(
+ hbs`
`
+ );
+
+ assert.dom("div.my-wrapper span.shim-content").exists();
+ assert.dom("div.my-wrapper span.shim-content").hasText("val1");
+ });
});
diff --git a/app/assets/stylesheets/common/admin/json_schema_editor.scss b/app/assets/stylesheets/common/admin/json_schema_editor.scss
index 2fb20a20bb..c12d074617 100644
--- a/app/assets/stylesheets/common/admin/json_schema_editor.scss
+++ b/app/assets/stylesheets/common/admin/json_schema_editor.scss
@@ -98,10 +98,25 @@
.row div[data-schematype="array"] {
padding: 0.5em;
background-color: var(--primary-very-low);
+
+ > .card-title {
+ width: 100%;
+ border-bottom: 1px solid var(--primary-low);
+ > .json-editor-btn-collapse {
+ float: right;
+ }
+ }
}
.desktop-view & .modal-inner-container {
--modal-max-width: 75vw;
min-width: 55vw;
}
+
+ .card-title.level-1,
+ .card-title.je-object__title {
+ > .json-editor-btn-collapse {
+ display: none;
+ }
+ }
}
diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss
index 458933a8d3..5ef4362f9b 100644
--- a/app/assets/stylesheets/common/base/group.scss
+++ b/app/assets/stylesheets/common/base/group.scss
@@ -2,6 +2,8 @@
// To style group content differently, use the existing classes with a .group parent class.
// For example: .group .user-secondary-navigation
+@use "sass:math";
+
.group-details-container {
background: var(--primary-very-low);
padding: 20px;
@@ -55,7 +57,7 @@
}
$size: 50px;
- $icon-size: $size / 1.8;
+ $icon-size: math.div($size, 1.8);
.avatar-flair-image {
width: $size;
diff --git a/app/assets/stylesheets/common/base/groups.scss b/app/assets/stylesheets/common/base/groups.scss
index 0fc72d0b99..b05cf1d383 100644
--- a/app/assets/stylesheets/common/base/groups.scss
+++ b/app/assets/stylesheets/common/base/groups.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
.groups-header {
display: flex;
flex-wrap: wrap;
@@ -114,7 +116,7 @@
}
$size: 40px;
- $icon-size: $size / 1.8;
+ $icon-size: math.div($size, 1.8);
.group-avatar-flair {
display: inline-block;
diff --git a/app/assets/stylesheets/common/base/magnific-popup.scss b/app/assets/stylesheets/common/base/magnific-popup.scss
index bd23ba1e89..bc83d8dc67 100644
--- a/app/assets/stylesheets/common/base/magnific-popup.scss
+++ b/app/assets/stylesheets/common/base/magnific-popup.scss
@@ -27,6 +27,8 @@
// 1. Default Settings
////////////////////////
+@use "sass:math";
+
$overlay-color: #0b0b0b !default;
$overlay-opacity: 0.8 !default;
$shadow: 0 0 8px rgba(0, 0, 0, 0.6) !default; // shadow on image or iframe
@@ -46,7 +48,7 @@ $include-iframe-type: true !default;
$iframe-padding-top: 40px !default;
$iframe-background: #000 !default;
$iframe-max-width: 900px !default;
-$iframe-ratio: 9/16 !default;
+$iframe-ratio: math.div(9, 16) !default;
// Image-type options
$include-image-type: true !default;
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index be7821e32c..d807b32855 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -279,6 +279,12 @@
}
.reply-where-modal {
+ .dialog-content {
+ width: 100%;
+ min-width: unset;
+ max-width: 30em;
+ }
+
.dialog-footer {
display: block;
}
@@ -291,19 +297,69 @@
overflow: hidden;
width: 100%;
- &:first-of-type {
- margin-top: 0.25em;
+ &.dialog-close {
+ display: none;
+ }
+
+ &.btn-reply-on-original {
+ --text-color: var(--secondary);
+ }
+
+ &.btn-reply-where__cancel {
+ padding-left: 0;
+ margin: 0;
}
- &.btn-reply-on-original,
&.btn-reply-here {
- font-size: var(--font-up-1);
- line-height: var(--line-height-medium);
- font-weight: bold;
+ --text-color: var(--primary);
+ .discourse-no-touch & {
+ &:hover {
+ --text-color: var(--secondary);
+ }
+ }
+ }
+
+ &.btn-reply-where {
+ min-height: 3em; // for situations when there are no categories/tags
}
.topic-title {
- font-weight: normal;
+ .d-icon {
+ color: var(--text-color);
+ margin: 0;
+ }
+ }
+
+ .fancy-title {
+ display: inline-block;
+ width: 100%;
+
+ @include ellipsis;
+ }
+
+ .topic-title__top-line {
+ display: flex;
+ align-items: baseline;
+ color: var(--text-color);
+ font-size: var(--font-up-1);
+ }
+
+ .topic-statuses {
+ display: flex;
+ font-size: 0.95em;
+ }
+
+ .topic-title__bottom-line {
+ margin-top: 0.15em;
+ display: flex;
+ align-items: baseline;
+ .discourse-tags {
+ font-size: var(--font-down-1);
+ }
+ .category-name,
+ .discourse-tag {
+ color: var(--text-color);
+ }
}
}
}
@@ -704,3 +760,14 @@
top: 21px;
}
}
+
+.json-editor-btn-delete {
+ @extend .btn-danger;
+ @extend .no-text;
+}
+
+.json-editor-btn-collapse {
+ @extend .no-text;
+ @extend .btn-flat;
+ @extend .btn-small;
+}
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
index 6683e0c242..a2957d35dd 100644
--- a/app/assets/stylesheets/common/base/sidebar.scss
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -86,6 +86,17 @@
transition-delay: 0s;
}
}
+ .sidebar-footer-wrapper {
+ .btn-flat.add-section {
+ padding: 0.25em 0.4em;
+ &:hover {
+ background: var(--d-sidebar-highlight-color);
+ svg {
+ color: var(--primary-medium);
+ }
+ }
+ }
+ }
}
.sidebar-hamburger-dropdown {
@@ -112,44 +123,17 @@
}
.sidebar-custom-sections {
- .btn-flat.add-section {
- margin-left: calc(var(--d-sidebar-section-link-prefix-width) / 2);
- margin-right: calc(var(--d-sidebar-section-link-prefix-width) / 2);
- width: calc(100% - var(--d-sidebar-section-link-prefix-width));
- svg {
- height: 0.75em;
- width: 0.75em;
- padding-left: 0.5em;
- padding-right: 0.5em;
- }
- &:before,
- &:after {
- content: "";
- flex: 1 1;
- border-bottom: 1px solid var(--primary-low-mid);
- margin: auto;
- }
- &:hover {
- background: var(--d-sidebar-highlight-color);
- border-radius: 5px;
- &:before,
- &:after {
- border-bottom: 1px solid var(--primary-high);
- }
- }
- }
- a.sidebar-section-link {
- padding-left: calc(
- var(--d-sidebar-section-link-prefix-width) +
- var(--d-sidebar-section-link-prefix-margin-right) +
- var(--d-sidebar-row-horizontal-padding)
- );
+ .sidebar-section-wrapper {
+ padding-bottom: 0;
}
}
.sidebar-section-form-modal {
.modal-inner-container {
width: var(--modal-max-width);
}
+ form {
+ margin-bottom: 0;
+ }
input {
width: 100%;
}
@@ -158,7 +142,7 @@
}
.row-wrapper {
display: grid;
- grid-template-columns: auto auto 2em;
+ grid-template-columns: auto auto auto 2em;
gap: 1em;
margin-top: 1em;
}
@@ -169,13 +153,20 @@
margin-right: 1em;
}
.btn-flat.add-link {
- float: right;
margin-top: 1em;
- margin-right: -0.5em;
+ margin-left: -0.65em;
&:active,
&:focus {
background: none;
}
+ svg {
+ color: var(--tertiary);
+ width: 0.75em;
+ height: 0.75em;
+ }
+ &:hover svg {
+ color: var(--tertiary-hover);
+ }
}
.modal-footer {
display: flex;
diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss
index 1014899d57..5f669ad3f4 100644
--- a/app/assets/stylesheets/common/base/tagging.scss
+++ b/app/assets/stylesheets/common/base/tagging.scss
@@ -148,20 +148,6 @@
}
}
-.mobile-view .topic-list-item .discourse-tags {
- display: inline-flex;
- flex-wrap: wrap;
- font-size: var(--font-down-1);
- margin-top: 0;
- .discourse-tag {
- margin-right: 0.2em;
- }
- .discourse-tag.box {
- position: relative;
- top: 0;
- }
-}
-
header .discourse-tag {
color: var(--primary-medium);
}
diff --git a/app/assets/stylesheets/common/components/user-card.scss b/app/assets/stylesheets/common/components/user-card.scss
index 8e7f2c1a49..b0224c7ced 100644
--- a/app/assets/stylesheets/common/components/user-card.scss
+++ b/app/assets/stylesheets/common/components/user-card.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
$card_width: 580px;
$avatar_width: 120px;
$avatar_margin: -50px; // negative margin makes avatars extend above cards
@@ -265,7 +267,7 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
color: var(--primary);
.d-icon {
margin: auto;
- font-size: $avatar_width / 1.5;
+ font-size: math.div($avatar_width, 1.5);
}
&.rounded {
border-radius: 50%;
diff --git a/app/assets/stylesheets/common/foundation/math.scss b/app/assets/stylesheets/common/foundation/math.scss
index 4d57257545..02dd4aceea 100644
--- a/app/assets/stylesheets/common/foundation/math.scss
+++ b/app/assets/stylesheets/common/foundation/math.scss
@@ -4,6 +4,8 @@
// https://github.com/terkel/mathsass
// Constants
+@use "sass:math";
+
$E: 2.718281828459045;
$PI: 3.141592653589793;
$LN2: 0.6931471805599453;
@@ -48,7 +50,7 @@ $SQRT2: 1.4142135623730951;
}
} @else if $x >= 1 {
@while $x >= 1 {
- $x: $x / 2;
+ $x: $x * 0.5;
$exp: $exp + 1;
}
}
@@ -59,7 +61,7 @@ $SQRT2: 1.4142135623730951;
// @param {Number} $x
// @param {Number} $exp
@function ldexp($x, $exp) {
- $b: if($exp >= 0, 2, 1 / 2);
+ $b: if($exp >= 0, 2, 1 * 0.5);
@if $exp < 0 {
$exp: $exp * -1;
}
@@ -80,11 +82,11 @@ $SQRT2: 1.4142135623730951;
// log(10) // 2.30259
@function log($x) {
@if $x <= 0 {
- @return 0 / 0;
+ @return math.div(0, 0);
}
- $k: nth(frexp($x / $SQRT2), 2);
- $x: $x / ldexp(1, $k);
- $x: ($x - 1) / ($x + 1);
+ $k: nth(frexp(math.div($x, $SQRT2)), 2);
+ $x: math.div($x, ldexp(1, $k));
+ $x: math.div($x - 1, $x + 1);
$x2: $x * $x;
$i: 1;
$s: $x;
@@ -93,7 +95,7 @@ $SQRT2: 1.4142135623730951;
$x: $x * $x2;
$i: $i + 2;
$sp: $s;
- $s: $s + $x / $i;
+ $s: $s + math.div($x, $i);
}
@return $LN2 * $k + 2 * $s;
}
@@ -115,7 +117,7 @@ $SQRT2: 1.4142135623730951;
$exp: floor($exp * 0.5);
$base: $base * $base;
}
- @return if($s != 0, 1 / $r, $r);
+ @return if($s != 0, math.div(1, $r), $r);
}
// Returns E^x, where x is the argument, and E is Euler's constant, the base of the natural logarithms.
@@ -126,7 +128,7 @@ $SQRT2: 1.4142135623730951;
@function exp($x) {
$ret: 0;
@for $n from 0 to 24 {
- $ret: $ret + ipow($x, $n) / fact($n);
+ $ret: $ret + math.div(ipow($x, $n), fact($n));
}
@return $ret;
}
@@ -158,7 +160,7 @@ $SQRT2: 1.4142135623730951;
}
$ret: 1;
@for $i from 1 through 24 {
- $ret: $ret - (pow($ret, 2) - $x) / (2 * $ret);
+ $ret: $ret - math.div(pow($ret, 2) - $x, 2 * $ret);
}
@return $ret;
}
diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss
index 1a8c2f8fef..118bded6ec 100644
--- a/app/assets/stylesheets/common/foundation/mixins.scss
+++ b/app/assets/stylesheets/common/foundation/mixins.scss
@@ -5,6 +5,8 @@
// Media queries
// --------------------------------------------------
+@use "sass:math";
+
$breakpoints: (
mobile-small: 320px,
mobile-medium: 350px,
@@ -162,7 +164,7 @@ $hpad: 0.65em;
$encoded: "";
$slice: 2000;
$index: 0;
- $loops: ceil(str-length($svg) / $slice);
+ $loops: ceil(math.div(str-length($svg), $slice));
@for $i from 1 through $loops {
$chunk: str-slice($svg, $index, $index + $slice - 1);
diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss
index 7ac2a24d31..d6ef33e620 100644
--- a/app/assets/stylesheets/common/foundation/variables.scss
+++ b/app/assets/stylesheets/common/foundation/variables.scss
@@ -6,6 +6,8 @@
// Layout dimensions
// --------------------------------------------------
+@use "sass:math";
+
$small-width: 800px !default;
$medium-width: 995px !default;
$large-width: 1110px !default;
@@ -165,7 +167,7 @@ $box-shadow: (
// Uses an approximation of sRGB blending, GAMMA=2 instead of GAMMA=2.2
@function srgb-scale($foreground, $background, $percent) {
- $ratio: ($percent / 100%);
+ $ratio: math.div($percent, 100%);
$iratio: 1 - $ratio;
$f_r2: red($foreground) * red($foreground);
$f_g2: green($foreground) * green($foreground);
diff --git a/app/assets/stylesheets/common/select-kit/flair-row.scss b/app/assets/stylesheets/common/select-kit/flair-row.scss
index 224ce3836d..f47142da2f 100644
--- a/app/assets/stylesheets/common/select-kit/flair-row.scss
+++ b/app/assets/stylesheets/common/select-kit/flair-row.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
$flair-size: 18px;
.select-kit.flair-chooser {
@@ -15,14 +17,14 @@ $flair-size: 18px;
width: $flair-size;
&.rounded {
- background-size: ($flair-size / 1.4) ($flair-size / 1.4);
+ background-size: math.div($flair-size, 1.4) math.div($flair-size, 1.4);
border-radius: 50%;
}
.d-icon {
display: block;
- height: ($flair-size / 1.8);
- width: ($flair-size / 1.8);
+ height: math.div($flair-size, 1.8);
+ width: math.div($flair-size, 1.8);
}
}
diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss
index 5c5a63753a..d85de686ff 100644
--- a/app/assets/stylesheets/desktop/modal.scss
+++ b/app/assets/stylesheets/desktop/modal.scss
@@ -123,8 +123,3 @@
max-width: 100vw; // prevent overflow if user font-size is enormous
}
}
-
-.json-editor-btn-delete {
- @extend .btn-danger !optional;
- @extend .no-text !optional;
-}
diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss
index 39d4a02878..ac23951757 100644
--- a/app/assets/stylesheets/mobile/topic-list.scss
+++ b/app/assets/stylesheets/mobile/topic-list.scss
@@ -162,11 +162,6 @@
}
.topic-item-stats {
- .category,
- .discourse-tags {
- // disabling clicks because these targets are too small on mobile
- pointer-events: none;
- }
position: relative;
display: flex;
align-items: baseline;
@@ -186,6 +181,47 @@
}
}
+ .topic-item-stats {
+ span.relative-date {
+ vertical-align: text-top;
+ }
+ }
+
+ .topic-item-stats__category-tags {
+ margin-right: 0.5em;
+ .category,
+ .discourse-tags {
+ display: inline;
+ // disabling clicks because these targets are too small on mobile
+ pointer-events: none;
+ }
+
+ .discourse-tags .discourse-tag {
+ margin: 0;
+ }
+
+ .badge-wrapper {
+ vertical-align: bottom;
+ }
+
+ .badge-wrapper {
+ &.bullet {
+ + .discourse-tags {
+ .discourse-tag {
+ vertical-align: bottom;
+ }
+ }
+ }
+ &.box {
+ + .discourse-tags {
+ .discourse-tag {
+ vertical-align: text-bottom;
+ }
+ }
+ }
+ }
+ }
+
.age {
white-space: nowrap;
a {
diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb
index fd429312e7..99bc0ca2d3 100644
--- a/app/controllers/admin/backups_controller.rb
+++ b/app/controllers/admin/backups_controller.rb
@@ -84,7 +84,7 @@ class Admin::BackupsController < Admin::AdminController
StaffActionLogger.new(current_user).log_backup_download(backup)
if store.remote?
- redirect_to backup.source
+ redirect_to backup.source, allow_other_host: true
else
headers["Content-Length"] = File.size(backup.source).to_s
send_file backup.source
diff --git a/app/controllers/sidebar_sections_controller.rb b/app/controllers/sidebar_sections_controller.rb
index bad744ccce..42c083119e 100644
--- a/app/controllers/sidebar_sections_controller.rb
+++ b/app/controllers/sidebar_sections_controller.rb
@@ -42,7 +42,7 @@ class SidebarSectionsController < ApplicationController
end
def links_params
- params.permit(links: %i[name value id _destroy])["links"]
+ params.permit(links: %i[icon name value id _destroy])["links"]
end
def check_if_member_of_group
diff --git a/app/models/category.rb b/app/models/category.rb
index c5fd3d7abb..00e3bc3aee 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -73,6 +73,12 @@ class Category < ActiveRecord::Base
validate :ensure_slug
validate :permissions_compatibility_validator
+ validates :default_slow_mode_seconds,
+ numericality: {
+ only_integer: true,
+ greater_than: 0,
+ },
+ allow_nil: true
validates :auto_close_hours,
numericality: {
greater_than: 0,
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index f9c2a5098d..ccd8d92e93 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -448,10 +448,11 @@ class ColorScheme < ActiveRecord::Base
end
def resolved_colors
- from_base = base_colors.except("hover", "selected")
+ from_base = ColorScheme.base_colors
+ from_custom_scheme = base_colors
from_db = colors.map { |c| [c.name, c.hex] }.to_h
- resolved = from_base.merge(from_db)
+ resolved = from_base.merge(from_custom_scheme).except("hover", "selected").merge(from_db)
# Equivalent to primary-100 in light mode, or primary-low in dark mode
resolved["hover"] ||= ColorMath.dark_light_diff(
diff --git a/app/models/concerns/trashable.rb b/app/models/concerns/trashable.rb
index cdd52c0842..bc75463de7 100644
--- a/app/models/concerns/trashable.rb
+++ b/app/models/concerns/trashable.rb
@@ -6,6 +6,7 @@ module Trashable
included do
default_scope { where(deleted_at: nil) }
scope :with_deleted, -> { unscope(where: :deleted_at) }
+ scope :only_deleted, -> { with_deleted.where.not(deleted_at: nil) }
belongs_to :deleted_by, class_name: "User"
end
diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb
index 4894a8a73c..f80bbf2698 100644
--- a/app/models/remote_theme.rb
+++ b/app/models/remote_theme.rb
@@ -13,7 +13,16 @@ class RemoteTheme < ActiveRecord::Base
class ImportError < StandardError
end
- ALLOWED_FIELDS = %w[scss embedded_scss head_tag header after_header body_tag footer]
+ ALLOWED_FIELDS = %w[
+ scss
+ embedded_scss
+ embedded_header
+ head_tag
+ header
+ after_header
+ body_tag
+ footer
+ ]
GITHUB_REGEXP = %r{\Ahttps?://github\.com/}
GITHUB_SSH_REGEXP = %r{\Assh://git@github\.com:}
diff --git a/app/models/reviewable_queued_post.rb b/app/models/reviewable_queued_post.rb
index ecf6297cdc..873d425e91 100644
--- a/app/models/reviewable_queued_post.rb
+++ b/app/models/reviewable_queued_post.rb
@@ -32,7 +32,7 @@ class ReviewableQueuedPost < Reviewable
end
end
- unless rejected?
+ if pending?
actions.add(:reject_post) do |a|
a.icon = "times"
a.label = "reviewables.actions.reject_post.title"
@@ -45,17 +45,19 @@ class ReviewableQueuedPost < Reviewable
end
def build_editable_fields(fields, guardian, args)
- # We can edit category / title if it's a new topic
- if topic_id.blank?
- # Only staff can edit category for now, since in theory a category group reviewer could
- # post in a category they don't have access to.
- fields.add("category_id", :category) if guardian.is_staff?
+ if pending?
+ # We can edit category / title if it's a new topic
+ if topic_id.blank?
+ # Only staff can edit category for now, since in theory a category group reviewer could
+ # post in a category they don't have access to.
+ fields.add("category_id", :category) if guardian.is_staff?
- fields.add("payload.title", :text)
- fields.add("payload.tags", :tags)
+ fields.add("payload.title", :text)
+ fields.add("payload.tags", :tags)
+ end
+
+ fields.add("payload.raw", :editor)
end
-
- fields.add("payload.raw", :editor)
end
def create_options
diff --git a/app/models/sidebar_section.rb b/app/models/sidebar_section.rb
index 826edc529f..65fdc0091c 100644
--- a/app/models/sidebar_section.rb
+++ b/app/models/sidebar_section.rb
@@ -10,7 +10,7 @@ class SidebarSection < ActiveRecord::Base
accepts_nested_attributes_for :sidebar_urls, allow_destroy: true
- validates :title, presence: true, uniqueness: { scope: %i[user_id] }
+ validates :title, presence: true, uniqueness: { scope: %i[user_id] }, length: { maximum: 30 }
end
# == Schema Information
@@ -19,7 +19,7 @@ end
#
# id :bigint not null, primary key
# user_id :integer not null
-# title :string not null
+# title :string(30) not null
# created_at :datetime not null
# updated_at :datetime not null
#
diff --git a/app/models/sidebar_url.rb b/app/models/sidebar_url.rb
index 9f3de5babd..3558ca0777 100644
--- a/app/models/sidebar_url.rb
+++ b/app/models/sidebar_url.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
class SidebarUrl < ActiveRecord::Base
- validates :name, presence: true
- validates :value, presence: true
+ validates :icon, presence: true, length: { maximum: 40 }
+ validates :name, presence: true, length: { maximum: 80 }
+ validates :value, presence: true, length: { maximum: 200 }
+
validate :path_validator
def path_validator
@@ -20,8 +22,9 @@ end
# Table name: sidebar_urls
#
# id :bigint not null, primary key
-# name :string not null
-# value :string not null
+# name :string(80) not null
+# value :string(200) not null
# created_at :datetime not null
# updated_at :datetime not null
+# icon :string(40) not null
#
diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb
index a2404c7f83..e7f51ce76f 100644
--- a/app/models/theme_field.rb
+++ b/app/models/theme_field.rb
@@ -337,7 +337,7 @@ class ThemeField < ActiveRecord::Base
end
def self.html_fields
- @html_fields ||= %w[body_tag head_tag header footer after_header]
+ @html_fields ||= %w[body_tag head_tag header footer after_header embedded_header]
end
def self.scss_fields
@@ -460,7 +460,7 @@ class ThemeField < ActiveRecord::Base
else
self.error = nil unless error.nil?
end
- rescue SassC::SyntaxError => e
+ rescue SassC::SyntaxError, SassC::NotRenderedError => e
self.error = e.message unless self.destroyed?
end
self.compiler_version = Theme.compiler_version
diff --git a/app/models/user.rb b/app/models/user.rb
index bd058f6e58..f188ec78f5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1790,14 +1790,17 @@ class User < ActiveRecord::Base
end
def set_status!(description, emoji, ends_at = nil)
- status = { description: description, emoji: emoji, set_at: Time.zone.now, ends_at: ends_at }
-
- if user_status
- user_status.update!(status)
- else
- self.user_status = UserStatus.create!(status)
- end
+ status = {
+ description: description,
+ emoji: emoji,
+ set_at: Time.zone.now,
+ ends_at: ends_at,
+ user_id: id,
+ }
+ validate_status!(status)
+ UserStatus.upsert(status)
+ reload_user_status
publish_user_status(user_status)
end
@@ -2175,6 +2178,10 @@ class User < ActiveRecord::Base
)
SQL
end
+
+ def validate_status!(status)
+ UserStatus.new(status).validate!
+ end
end
# == Schema Information
diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb
index 90711df5d2..8c095e69d9 100644
--- a/app/views/layouts/embed.html.erb
+++ b/app/views/layouts/embed.html.erb
@@ -19,6 +19,7 @@
<%= yield :head %>
+ <%= theme_lookup("embedded_header") %>
<%= yield %>