| @@ -0,0 +1,73 @@ | ||
| import { action, computed } from "@ember/object"; | ||
| import loadScript, { loadCSS } from "discourse/lib/load-script"; | ||
| import Component from "@ember/component"; | ||
| import { observes } from "discourse-common/utils/decorators"; | ||
| import { schedule } from "@ember/runloop"; | ||
|
|
||
| /** | ||
| An input field for a color. | ||
| @param hexValue is a reference to the color's hex value. | ||
| @param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor. | ||
| @params valid is a boolean indicating if the input field is a valid color. | ||
| **/ | ||
| export default Component.extend({ | ||
| classNames: ["color-picker"], | ||
|
|
||
| onlyHex: true, | ||
|
|
||
| styleSelection: true, | ||
|
|
||
| maxlength: computed("onlyHex", function () { | ||
| return this.onlyHex ? 6 : null; | ||
| }), | ||
|
|
||
| @action | ||
| onHexInput(color) { | ||
| this.attrs.onChangeColor && this.attrs.onChangeColor(color || ""); | ||
| }, | ||
|
|
||
| @observes("hexValue", "brightnessValue", "valid") | ||
| hexValueChanged: function () { | ||
| const hex = this.hexValue; | ||
| let text = this.element.querySelector("input.hex-input"); | ||
|
|
||
| this.attrs.onChangeColor && this.attrs.onChangeColor(hex); | ||
|
|
||
| if (this.valid) { | ||
| this.styleSelection && | ||
| text.setAttribute( | ||
| "style", | ||
| "color: " + | ||
| (this.brightnessValue > 125 ? "black" : "white") + | ||
| "; background-color: #" + | ||
| hex + | ||
| ";" | ||
| ); | ||
|
|
||
| if (this.pickerLoaded) { | ||
| $(this.element.querySelector(".picker")).spectrum({ | ||
| color: "#" + hex, | ||
| }); | ||
| } | ||
| } else { | ||
| this.styleSelection && text.setAttribute("style", ""); | ||
| } | ||
| }, | ||
|
|
||
| didInsertElement() { | ||
| loadScript("/javascripts/spectrum.js").then(() => { | ||
| loadCSS("/javascripts/spectrum.css").then(() => { | ||
| schedule("afterRender", () => { | ||
| $(this.element.querySelector(".picker")) | ||
| .spectrum({ color: "#" + this.hexValue }) | ||
| .on("change.spectrum", (me, color) => { | ||
| this.set("hexValue", color.toHexString().replace("#", "")); | ||
| }); | ||
| this.set("pickerLoaded", true); | ||
| }); | ||
| }); | ||
| }); | ||
| schedule("afterRender", () => this.hexValueChanged()); | ||
| }, | ||
| }); |
| @@ -0,0 +1,33 @@ | ||
| import Component from "@ember/component"; | ||
| import { action, computed } from "@ember/object"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
|
|
||
| export default Component.extend({ | ||
| newFeatures: null, | ||
| classNames: ["section", "dashboard-new-features"], | ||
| classNameBindings: ["hasUnseenFeatures:ordered-first"], | ||
| releaseNotesLink: null, | ||
|
|
||
| init() { | ||
| this._super(...arguments); | ||
|
|
||
| ajax("/admin/dashboard/new-features.json").then((json) => { | ||
| this.setProperties({ | ||
| newFeatures: json.new_features, | ||
| hasUnseenFeatures: json.has_unseen_features, | ||
| releaseNotesLink: json.release_notes_link, | ||
| }); | ||
| }); | ||
| }, | ||
|
|
||
| columnCountClass: computed("newFeatures", function () { | ||
| return this.newFeatures.length > 2 ? "three-or-more-items" : ""; | ||
| }), | ||
|
|
||
| @action | ||
| dismissNewFeatures() { | ||
| ajax("/admin/dashboard/mark-new-features-as-seen.json", { | ||
| type: "PUT", | ||
| }).then(() => this.set("hasUnseenFeatures", false)); | ||
| }, | ||
| }); |
| @@ -0,0 +1,54 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { reads } from "@ember/object/computed"; | ||
|
|
||
| export default Component.extend({ | ||
| editorId: reads("fieldName"), | ||
|
|
||
| @discourseComputed("fieldName") | ||
| currentEditorMode(fieldName) { | ||
| return fieldName === "css" ? "scss" : fieldName; | ||
| }, | ||
|
|
||
| @discourseComputed("fieldName", "styles.html", "styles.css") | ||
| resetDisabled(fieldName) { | ||
| return ( | ||
| this.get(`styles.${fieldName}`) === | ||
| this.get(`styles.default_${fieldName}`) | ||
| ); | ||
| }, | ||
|
|
||
| @discourseComputed("styles", "fieldName") | ||
| editorContents: { | ||
| get(styles, fieldName) { | ||
| return styles[fieldName]; | ||
| }, | ||
| set(value, styles, fieldName) { | ||
| styles.setField(fieldName, value); | ||
| return value; | ||
| }, | ||
| }, | ||
|
|
||
| actions: { | ||
| reset() { | ||
| bootbox.confirm( | ||
| I18n.t("admin.customize.email_style.reset_confirm", { | ||
| fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`), | ||
| }), | ||
| I18n.t("no_value"), | ||
| I18n.t("yes_value"), | ||
| (result) => { | ||
| if (result) { | ||
| this.styles.setField( | ||
| this.fieldName, | ||
| this.styles.get(`default_${this.fieldName}`) | ||
| ); | ||
| this.notifyPropertyChange("editorContents"); | ||
| } | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,72 @@ | ||
| import Category from "discourse/models/category"; | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import bootbox from "bootbox"; | ||
| import { bufferedProperty } from "discourse/mixins/buffered-content"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { isEmpty } from "@ember/utils"; | ||
| import { or } from "@ember/object/computed"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
|
|
||
| export default Component.extend(bufferedProperty("host"), { | ||
| editToggled: false, | ||
| tagName: "tr", | ||
| categoryId: null, | ||
|
|
||
| editing: or("host.isNew", "editToggled"), | ||
|
|
||
| @discourseComputed("buffered.host", "host.isSaving") | ||
| cantSave(host, isSaving) { | ||
| return isSaving || isEmpty(host); | ||
| }, | ||
|
|
||
| actions: { | ||
| edit() { | ||
| this.set("categoryId", this.get("host.category.id")); | ||
| this.set("editToggled", true); | ||
| }, | ||
|
|
||
| save() { | ||
| if (this.cantSave) { | ||
| return; | ||
| } | ||
|
|
||
| const props = this.buffered.getProperties( | ||
| "host", | ||
| "allowed_paths", | ||
| "class_name" | ||
| ); | ||
| props.category_id = this.categoryId; | ||
|
|
||
| const host = this.host; | ||
|
|
||
| host | ||
| .save(props) | ||
| .then(() => { | ||
| host.set("category", Category.findById(this.categoryId)); | ||
| this.set("editToggled", false); | ||
| }) | ||
| .catch(popupAjaxError); | ||
| }, | ||
|
|
||
| delete() { | ||
| bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), (result) => { | ||
| if (result) { | ||
| this.host.destroyRecord().then(() => { | ||
| this.deleteHost(this.host); | ||
| }); | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
| cancel() { | ||
| const host = this.host; | ||
| if (host.get("isNew")) { | ||
| this.deleteHost(host); | ||
| } else { | ||
| this.rollbackBuffer(); | ||
| this.set("editToggled", false); | ||
| } | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,32 @@ | ||
| import Component from "@ember/component"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["embed-setting"], | ||
|
|
||
| @discourseComputed("field") | ||
| inputId(field) { | ||
| return field.dasherize(); | ||
| }, | ||
|
|
||
| @discourseComputed("field") | ||
| translationKey(field) { | ||
| return `admin.embedding.${field}`; | ||
| }, | ||
|
|
||
| @discourseComputed("type") | ||
| isCheckbox(type) { | ||
| return type === "checkbox"; | ||
| }, | ||
|
|
||
| @discourseComputed("value") | ||
| checked: { | ||
| get(value) { | ||
| return !!value; | ||
| }, | ||
| set(value) { | ||
| this.set("value", value); | ||
| return value; | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,4 @@ | ||
| import Component from "@ember/component"; | ||
| export default Component.extend({ | ||
| classNames: ["flag-user-lists"], | ||
| }); |
| @@ -0,0 +1,11 @@ | ||
| import { observes, on } from "discourse-common/utils/decorators"; | ||
| import Component from "@ember/component"; | ||
| import highlightSyntax from "discourse/lib/highlight-syntax"; | ||
|
|
||
| export default Component.extend({ | ||
| @on("didInsertElement") | ||
| @observes("code") | ||
| _refresh() { | ||
| highlightSyntax(this.element, this.siteSettings, this.session); | ||
| }, | ||
| }); |
| @@ -0,0 +1,44 @@ | ||
| import Component from "@ember/component"; | ||
| import { action } from "@ember/object"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["inline-edit"], | ||
|
|
||
| buffer: null, | ||
| bufferModelId: null, | ||
|
|
||
| didReceiveAttrs() { | ||
| this._super(...arguments); | ||
|
|
||
| if (this.modelId !== this.bufferModelId) { | ||
| // HACK: The condition above ensures this method is called only when its | ||
| // attributes are changed (i.e. only when `checked` changes). | ||
| // | ||
| // Reproduction steps: navigate to theme #1, switch to theme #2 from the | ||
| // left-side panel, then switch back to theme #1 and click on the <input> | ||
| // element wrapped by this component. It will call `didReceiveAttrs` even | ||
| // though none of the attributes have changed (only `buffer` does). | ||
| this.setProperties({ | ||
| buffer: this.checked, | ||
| bufferModelId: this.modelId, | ||
| }); | ||
| } | ||
| }, | ||
|
|
||
| @discourseComputed("checked", "buffer") | ||
| changed(checked, buffer) { | ||
| return !!checked !== !!buffer; | ||
| }, | ||
|
|
||
| @action | ||
| apply() { | ||
| this.set("checked", this.buffer); | ||
| this.action(); | ||
| }, | ||
|
|
||
| @action | ||
| cancel() { | ||
| this.set("buffer", this.checked); | ||
| }, | ||
| }); |
| @@ -0,0 +1,4 @@ | ||
| import Component from "@ember/component"; | ||
| export default Component.extend({ | ||
| classNames: ["install-theme-item"], | ||
| }); |
| @@ -0,0 +1,117 @@ | ||
| import AdminUser from "admin/models/admin-user"; | ||
| import Component from "@ember/component"; | ||
| import EmberObject from "@ember/object"; | ||
| import I18n from "I18n"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
| import bootbox from "bootbox"; | ||
| import copyText from "discourse/lib/copy-text"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { later } from "@ember/runloop"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["ip-lookup"], | ||
|
|
||
| @discourseComputed("other_accounts.length", "totalOthersWithSameIP") | ||
| otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) { | ||
| // can only delete up to 50 accounts at a time | ||
| const total = Math.min(50, totalOthersWithSameIP || 0); | ||
| const visible = Math.min(50, otherAccountsLength || 0); | ||
| return Math.max(visible, total); | ||
| }, | ||
|
|
||
| actions: { | ||
| lookup() { | ||
| this.set("show", true); | ||
|
|
||
| if (!this.location) { | ||
| ajax("/admin/users/ip-info", { | ||
| data: { ip: this.ip }, | ||
| }).then((location) => | ||
| this.set("location", EmberObject.create(location)) | ||
| ); | ||
| } | ||
|
|
||
| if (!this.other_accounts) { | ||
| this.set("otherAccountsLoading", true); | ||
|
|
||
| const data = { | ||
| ip: this.ip, | ||
| exclude: this.userId, | ||
| order: "trust_level DESC", | ||
| }; | ||
|
|
||
| ajax("/admin/users/total-others-with-same-ip", { | ||
| data, | ||
| }).then((result) => this.set("totalOthersWithSameIP", result.total)); | ||
|
|
||
| AdminUser.findAll("active", data).then((users) => { | ||
| this.setProperties({ | ||
| other_accounts: users, | ||
| otherAccountsLoading: false, | ||
| }); | ||
| }); | ||
| } | ||
| }, | ||
|
|
||
| hide() { | ||
| this.set("show", false); | ||
| }, | ||
|
|
||
| copy() { | ||
| let text = `IP: ${this.ip}\n`; | ||
| const location = this.location; | ||
| if (location) { | ||
| if (location.hostname) { | ||
| text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`; | ||
| } | ||
|
|
||
| text += I18n.t("ip_lookup.location"); | ||
| if (location.location) { | ||
| text += `: ${location.location}\n`; | ||
| } else { | ||
| text += `: ${I18n.t("ip_lookup.location_not_found")}\n`; | ||
| } | ||
|
|
||
| if (location.organization) { | ||
| text += I18n.t("ip_lookup.organisation"); | ||
| text += `: ${location.organization}\n`; | ||
| } | ||
| } | ||
|
|
||
| const $copyRange = $('<p id="copy-range"></p>'); | ||
| $copyRange.html(text.trim().replace(/\n/g, "<br>")); | ||
| $(document.body).append($copyRange); | ||
| if (copyText(text, $copyRange[0])) { | ||
| this.set("copied", true); | ||
| later(() => this.set("copied", false), 2000); | ||
| } | ||
| $copyRange.remove(); | ||
| }, | ||
|
|
||
| deleteOtherAccounts() { | ||
| bootbox.confirm( | ||
| I18n.t("ip_lookup.confirm_delete_other_accounts"), | ||
| I18n.t("no_value"), | ||
| I18n.t("yes_value"), | ||
| (confirmed) => { | ||
| if (confirmed) { | ||
| this.setProperties({ | ||
| other_accounts: null, | ||
| otherAccountsLoading: true, | ||
| totalOthersWithSameIP: null, | ||
| }); | ||
|
|
||
| ajax("/admin/users/delete-others-with-same-ip.json", { | ||
| type: "DELETE", | ||
| data: { | ||
| ip: this.ip, | ||
| exclude: this.userId, | ||
| order: "trust_level DESC", | ||
| }, | ||
| }).then(() => this.send("lookup")); | ||
| } | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,4 @@ | ||
| import Component from "@ember/component"; | ||
| export default Component.extend({ | ||
| tagName: "tr", | ||
| }); |
| @@ -0,0 +1,42 @@ | ||
| import discourseComputed, { | ||
| afterRender, | ||
| } from "discourse-common/utils/decorators"; | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import { equal } from "@ember/object/computed"; | ||
|
|
||
| const ACTIONS = ["delete", "delete_replies", "edit", "none"]; | ||
|
|
||
| export default Component.extend({ | ||
| postId: null, | ||
| postAction: null, | ||
| postEdit: null, | ||
|
|
||
| @discourseComputed | ||
| penaltyActions() { | ||
| return ACTIONS.map((id) => { | ||
| return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; | ||
| }); | ||
| }, | ||
|
|
||
| editing: equal("postAction", "edit"), | ||
|
|
||
| actions: { | ||
| penaltyChanged(postAction) { | ||
| this.set("postAction", postAction); | ||
|
|
||
| // If we switch to edit mode, jump to the edit textarea | ||
| if (postAction === "edit") { | ||
| this._focusEditTextarea(); | ||
| } | ||
| }, | ||
| }, | ||
|
|
||
| @afterRender | ||
| _focusEditTextarea() { | ||
| const elem = this.element; | ||
| const body = elem.closest(".modal-body"); | ||
| body.scrollTo(0, body.clientHeight); | ||
| elem.querySelector(".post-editor").focus(); | ||
| }, | ||
| }); |
| @@ -0,0 +1,89 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import Permalink from "admin/models/permalink"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { fmt } from "discourse/lib/computed"; | ||
| import { schedule } from "@ember/runloop"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["permalink-form"], | ||
| formSubmitted: false, | ||
| permalinkType: "topic_id", | ||
| permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"), | ||
|
|
||
| @discourseComputed | ||
| permalinkTypes() { | ||
| return [ | ||
| { id: "topic_id", name: I18n.t("admin.permalink.topic_id") }, | ||
| { id: "post_id", name: I18n.t("admin.permalink.post_id") }, | ||
| { id: "category_id", name: I18n.t("admin.permalink.category_id") }, | ||
| { id: "tag_name", name: I18n.t("admin.permalink.tag_name") }, | ||
| { id: "external_url", name: I18n.t("admin.permalink.external_url") }, | ||
| ]; | ||
| }, | ||
|
|
||
| didInsertElement() { | ||
| this._super(...arguments); | ||
|
|
||
| schedule("afterRender", () => { | ||
| $(this.element.querySelector(".external-url")).keydown((e) => { | ||
| // enter key | ||
| if (e.keyCode === 13) { | ||
| this.send("submit"); | ||
| } | ||
| }); | ||
| }); | ||
| }, | ||
|
|
||
| focusPermalink() { | ||
| schedule("afterRender", () => | ||
| this.element.querySelector(".permalink-url").focus() | ||
| ); | ||
| }, | ||
|
|
||
| actions: { | ||
| submit() { | ||
| if (!this.formSubmitted) { | ||
| this.set("formSubmitted", true); | ||
|
|
||
| Permalink.create({ | ||
| url: this.url, | ||
| permalink_type: this.permalinkType, | ||
| permalink_type_value: this.permalink_type_value, | ||
| }) | ||
| .save() | ||
| .then( | ||
| (result) => { | ||
| this.setProperties({ | ||
| url: "", | ||
| permalink_type_value: "", | ||
| formSubmitted: false, | ||
| }); | ||
|
|
||
| this.action(Permalink.create(result.permalink)); | ||
|
|
||
| this.focusPermalink(); | ||
| }, | ||
| (e) => { | ||
| this.set("formSubmitted", false); | ||
|
|
||
| let error; | ||
| if (e.responseJSON && e.responseJSON.errors) { | ||
| error = I18n.t("generic_error_with_reason", { | ||
| error: e.responseJSON.errors.join(". "), | ||
| }); | ||
| } else { | ||
| error = I18n.t("generic_error"); | ||
| } | ||
| bootbox.alert(error, () => this.focusPermalink()); | ||
| } | ||
| ); | ||
| } | ||
| }, | ||
|
|
||
| onChangePermalinkType(type) { | ||
| this.set("permalinkType", type); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,16 @@ | ||
| import FilterComponent from "admin/components/report-filters/filter"; | ||
| import { action } from "@ember/object"; | ||
|
|
||
| export default FilterComponent.extend({ | ||
| checked: false, | ||
|
|
||
| didReceiveAttrs() { | ||
| this._super(...arguments); | ||
| this.set("checked", !!this.filter.default); | ||
| }, | ||
|
|
||
| @action | ||
| onChange() { | ||
| this.applyFilter(this.filter.id, !this.checked || undefined); | ||
| }, | ||
| }); |
| @@ -0,0 +1,12 @@ | ||
| import FilterComponent from "admin/components/report-filters/filter"; | ||
| import { action } from "@ember/object"; | ||
| import { readOnly } from "@ember/object/computed"; | ||
|
|
||
| export default FilterComponent.extend({ | ||
| category: readOnly("filter.default"), | ||
|
|
||
| @action | ||
| onChange(categoryId) { | ||
| this.applyFilter(this.filter.id, categoryId || undefined); | ||
| }, | ||
| }); |
| @@ -0,0 +1,9 @@ | ||
| import Component from "@ember/component"; | ||
| import { action } from "@ember/object"; | ||
|
|
||
| export default Component.extend({ | ||
| @action | ||
| onChange(value) { | ||
| this.applyFilter(this.filter.id, value); | ||
| }, | ||
| }); |
| @@ -0,0 +1,18 @@ | ||
| import FilterComponent from "admin/components/report-filters/filter"; | ||
| import { computed } from "@ember/object"; | ||
|
|
||
| export default FilterComponent.extend({ | ||
| classNames: ["group-filter"], | ||
|
|
||
| @computed | ||
| get groupOptions() { | ||
| return (this.site.groups || []).map((group) => { | ||
| return { name: group["name"], value: group["id"] }; | ||
| }); | ||
| }, | ||
|
|
||
| @computed("filter.default") | ||
| get groupId() { | ||
| return this.filter.default ? parseInt(this.filter.default, 10) : null; | ||
| }, | ||
| }); |
| @@ -0,0 +1,3 @@ | ||
| import FilterComponent from "admin/components/report-filters/filter"; | ||
|
|
||
| export default FilterComponent.extend(); |
| @@ -0,0 +1,140 @@ | ||
| import discourseComputed, { on } from "discourse-common/utils/decorators"; | ||
| import { later, schedule } from "@ember/runloop"; | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import getURL from "discourse-common/lib/get-url"; | ||
| import { iconHTML } from "discourse-common/lib/icon-library"; | ||
|
|
||
| /*global Resumable:true */ | ||
|
|
||
| /** | ||
| Example usage: | ||
| {{resumable-upload | ||
| target="/admin/backups/upload" | ||
| success=(action "successAction") | ||
| error=(action "errorAction") | ||
| uploadText="UPLOAD" | ||
| }} | ||
| **/ | ||
| export default Component.extend({ | ||
| tagName: "button", | ||
| classNames: ["btn", "ru"], | ||
| classNameBindings: ["isUploading"], | ||
| attributeBindings: ["translatedTitle:title"], | ||
| resumable: null, | ||
| isUploading: false, | ||
| progress: 0, | ||
| rerenderTriggers: ["isUploading", "progress"], | ||
| uploadingIcon: null, | ||
| progressBar: null, | ||
|
|
||
| @on("init") | ||
| _initialize() { | ||
| this.resumable = new Resumable({ | ||
| target: getURL(this.target), | ||
| maxFiles: 1, // only 1 file at a time | ||
| headers: { | ||
| "X-CSRF-Token": document.querySelector("meta[name='csrf-token']") | ||
| .content, | ||
| }, | ||
| }); | ||
|
|
||
| this.resumable.on("fileAdded", () => { | ||
| // automatically upload the selected file | ||
| this.resumable.upload(); | ||
|
|
||
| // mark as uploading | ||
| later(() => { | ||
| this.set("isUploading", true); | ||
| this._updateIcon(); | ||
| }); | ||
| }); | ||
|
|
||
| this.resumable.on("fileProgress", (file) => { | ||
| // update progress | ||
| later(() => { | ||
| this.set("progress", parseInt(file.progress() * 100, 10)); | ||
| this._updateProgressBar(); | ||
| }); | ||
| }); | ||
|
|
||
| this.resumable.on("fileSuccess", (file) => { | ||
| later(() => { | ||
| // mark as not uploading anymore | ||
| this._reset(); | ||
|
|
||
| // fire an event to allow the parent route to reload its model | ||
| this.success(file.fileName); | ||
| }); | ||
| }); | ||
|
|
||
| this.resumable.on("fileError", (file, message) => { | ||
| later(() => { | ||
| // mark as not uploading anymore | ||
| this._reset(); | ||
|
|
||
| // fire an event to allow the parent route to display the error message | ||
| this.error(file.fileName, message); | ||
| }); | ||
| }); | ||
| }, | ||
|
|
||
| @on("didInsertElement") | ||
| _assignBrowse() { | ||
| schedule("afterRender", () => this.resumable.assignBrowse($(this.element))); | ||
| }, | ||
|
|
||
| @on("willDestroyElement") | ||
| _teardown() { | ||
| if (this.resumable) { | ||
| this.resumable.cancel(); | ||
| this.resumable = null; | ||
| } | ||
| }, | ||
|
|
||
| @discourseComputed("title", "text") | ||
| translatedTitle(title, text) { | ||
| return title ? I18n.t(title) : text; | ||
| }, | ||
|
|
||
| @discourseComputed("isUploading", "progress") | ||
| text(isUploading, progress) { | ||
| if (isUploading) { | ||
| return progress + " %"; | ||
| } else { | ||
| return this.uploadText; | ||
| } | ||
| }, | ||
|
|
||
| didReceiveAttrs() { | ||
| this._super(...arguments); | ||
| this._updateIcon(); | ||
| }, | ||
|
|
||
| click() { | ||
| if (this.isUploading) { | ||
| this.resumable.cancel(); | ||
| later(() => this._reset()); | ||
| return false; | ||
| } else { | ||
| return true; | ||
| } | ||
| }, | ||
|
|
||
| _updateIcon() { | ||
| const icon = this.isUploading ? "times" : "upload"; | ||
| this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe()); | ||
| }, | ||
|
|
||
| _updateProgressBar() { | ||
| const pb = `${"width:" + this.progress + "%"}`.htmlSafe(); | ||
| this.set("progressBar", pb); | ||
| }, | ||
|
|
||
| _reset() { | ||
| this.setProperties({ isUploading: false, progress: 0 }); | ||
| this._updateIcon(); | ||
| this._updateProgressBar(); | ||
| }, | ||
| }); |
| @@ -0,0 +1,92 @@ | ||
| import discourseComputed, { on } from "discourse-common/utils/decorators"; | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import ScreenedIpAddress from "admin/models/screened-ip-address"; | ||
| import bootbox from "bootbox"; | ||
| import { schedule } from "@ember/runloop"; | ||
|
|
||
| /** | ||
| A form to create an IP address that will be blocked or allowed. | ||
| Example usage: | ||
| {{screened-ip-address-form action=(action "recordAdded")}} | ||
| where action is a callback on the controller or route that will get called after | ||
| the new record is successfully saved. It is called with the new ScreenedIpAddress record | ||
| as an argument. | ||
| **/ | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["screened-ip-address-form"], | ||
| formSubmitted: false, | ||
| actionName: "block", | ||
|
|
||
| @discourseComputed("siteSettings.use_admin_ip_allowlist") | ||
| actionNames(adminAllowlistEnabled) { | ||
| if (adminAllowlistEnabled) { | ||
| return [ | ||
| { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, | ||
| { | ||
| id: "do_nothing", | ||
| name: I18n.t("admin.logs.screened_ips.actions.do_nothing"), | ||
| }, | ||
| { | ||
| id: "allow_admin", | ||
| name: I18n.t("admin.logs.screened_ips.actions.allow_admin"), | ||
| }, | ||
| ]; | ||
| } else { | ||
| return [ | ||
| { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, | ||
| { | ||
| id: "do_nothing", | ||
| name: I18n.t("admin.logs.screened_ips.actions.do_nothing"), | ||
| }, | ||
| ]; | ||
| } | ||
| }, | ||
|
|
||
| actions: { | ||
| submit() { | ||
| if (!this.formSubmitted) { | ||
| this.set("formSubmitted", true); | ||
| const screenedIpAddress = ScreenedIpAddress.create({ | ||
| ip_address: this.ip_address, | ||
| action_name: this.actionName, | ||
| }); | ||
| screenedIpAddress | ||
| .save() | ||
| .then((result) => { | ||
| this.setProperties({ ip_address: "", formSubmitted: false }); | ||
| this.action(ScreenedIpAddress.create(result.screened_ip_address)); | ||
| schedule("afterRender", () => | ||
| this.element.querySelector(".ip-address-input").focus() | ||
| ); | ||
| }) | ||
| .catch((e) => { | ||
| this.set("formSubmitted", false); | ||
| const msg = | ||
| e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors | ||
| ? I18n.t("generic_error_with_reason", { | ||
| error: e.jqXHR.responseJSON.errors.join(". "), | ||
| }) | ||
| : I18n.t("generic_error"); | ||
| bootbox.alert(msg, () => | ||
| this.element.querySelector(".ip-address-input").focus() | ||
| ); | ||
| }); | ||
| } | ||
| }, | ||
| }, | ||
|
|
||
| @on("didInsertElement") | ||
| _init() { | ||
| schedule("afterRender", () => { | ||
| $(this.element.querySelector(".ip-address-input")).keydown((e) => { | ||
| if (e.keyCode === 13) { | ||
| this.send("submit"); | ||
| } | ||
| }); | ||
| }); | ||
| }, | ||
| }); |
| @@ -0,0 +1,111 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import { isEmpty } from "@ember/utils"; | ||
| import { on } from "discourse-common/utils/decorators"; | ||
| import { set } from "@ember/object"; | ||
|
|
||
| export default Component.extend({ | ||
| classNameBindings: [":value-list", ":secret-value-list"], | ||
| inputDelimiter: null, | ||
| collection: null, | ||
| values: null, | ||
| validationMessage: null, | ||
|
|
||
| @on("didReceiveAttrs") | ||
| _setupCollection() { | ||
| const values = this.values; | ||
|
|
||
| this.set( | ||
| "collection", | ||
| this._splitValues(values, this.inputDelimiter || "\n") | ||
| ); | ||
| }, | ||
|
|
||
| actions: { | ||
| changeKey(index, newValue) { | ||
| if (this._checkInvalidInput(newValue)) { | ||
| return; | ||
| } | ||
| this._replaceValue(index, newValue, "key"); | ||
| }, | ||
|
|
||
| changeSecret(index, newValue) { | ||
| if (this._checkInvalidInput(newValue)) { | ||
| return; | ||
| } | ||
| this._replaceValue(index, newValue, "secret"); | ||
| }, | ||
|
|
||
| addValue() { | ||
| if (this._checkInvalidInput([this.newKey, this.newSecret])) { | ||
| return; | ||
| } | ||
| this._addValue(this.newKey, this.newSecret); | ||
| this.setProperties({ newKey: "", newSecret: "" }); | ||
| }, | ||
|
|
||
| removeValue(value) { | ||
| this._removeValue(value); | ||
| }, | ||
| }, | ||
|
|
||
| _checkInvalidInput(inputs) { | ||
| this.set("validationMessage", null); | ||
| for (let input of inputs) { | ||
| if (isEmpty(input) || input.includes("|")) { | ||
| this.set( | ||
| "validationMessage", | ||
| I18n.t("admin.site_settings.secret_list.invalid_input") | ||
| ); | ||
| return true; | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| _addValue(value, secret) { | ||
| this.collection.addObject({ key: value, secret: secret }); | ||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _removeValue(value) { | ||
| const collection = this.collection; | ||
| collection.removeObject(value); | ||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _replaceValue(index, newValue, keyName) { | ||
| let item = this.collection[index]; | ||
| set(item, keyName, newValue); | ||
|
|
||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _saveValues() { | ||
| this.set( | ||
| "values", | ||
| this.collection | ||
| .map(function (elem) { | ||
| return `${elem.key}|${elem.secret}`; | ||
| }) | ||
| .join("\n") | ||
| ); | ||
| }, | ||
|
|
||
| _splitValues(values, delimiter) { | ||
| if (values && values.length) { | ||
| const keys = ["key", "secret"]; | ||
| let res = []; | ||
| values.split(delimiter).forEach(function (str) { | ||
| let object = {}; | ||
| str.split("|").forEach(function (a, i) { | ||
| object[keys[i]] = a; | ||
| }); | ||
| res.push(object); | ||
| }); | ||
|
|
||
| return res; | ||
| } else { | ||
| return []; | ||
| } | ||
| }, | ||
| }); |
| @@ -0,0 +1,4 @@ | ||
| import Component from "@ember/component"; | ||
| export default Component.extend({ | ||
| tagName: "", | ||
| }); |
| @@ -0,0 +1,59 @@ | ||
| import Component from "@ember/component"; | ||
| import { action } from "@ember/object"; | ||
| import { empty } from "@ember/object/computed"; | ||
| import { on } from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend({ | ||
| classNameBindings: [":simple-list", ":value-list"], | ||
| inputEmpty: empty("newValue"), | ||
| inputDelimiter: null, | ||
| newValue: "", | ||
| collection: null, | ||
| values: null, | ||
|
|
||
| @on("didReceiveAttrs") | ||
| _setupCollection() { | ||
| this.set("collection", this._splitValues(this.values, this.inputDelimiter)); | ||
| }, | ||
|
|
||
| keyDown(event) { | ||
| if (event.which === 13) { | ||
| this.addValue(this.newValue); | ||
| return; | ||
| } | ||
| }, | ||
|
|
||
| @action | ||
| changeValue(index, newValue) { | ||
| this.collection.replace(index, 1, [newValue]); | ||
| this.collection.arrayContentDidChange(index); | ||
| this._onChange(); | ||
| }, | ||
|
|
||
| @action | ||
| addValue(newValue) { | ||
| if (this.inputEmpty) { | ||
| return; | ||
| } | ||
|
|
||
| this.set("newValue", null); | ||
| this.collection.addObject(newValue); | ||
| this._onChange(); | ||
| }, | ||
|
|
||
| @action | ||
| removeValue(value) { | ||
| this.collection.removeObject(value); | ||
| this._onChange(); | ||
| }, | ||
|
|
||
| _onChange() { | ||
| this.attrs.onChange && this.attrs.onChange(this.collection); | ||
| }, | ||
|
|
||
| _splitValues(values, delimiter) { | ||
| return values && values.length | ||
| ? values.split(delimiter || "\n").filter(Boolean) | ||
| : []; | ||
| }, | ||
| }); |
| @@ -0,0 +1,18 @@ | ||
| import BufferedContent from "discourse/mixins/buffered-content"; | ||
| import Component from "@ember/component"; | ||
| import SettingComponent from "admin/mixins/setting-component"; | ||
| import SiteSetting from "admin/models/site-setting"; | ||
| import { readOnly } from "@ember/object/computed"; | ||
|
|
||
| export default Component.extend(BufferedContent, SettingComponent, { | ||
| updateExistingUsers: null, | ||
|
|
||
| _save() { | ||
| const setting = this.buffered; | ||
| return SiteSetting.update(setting.get("setting"), setting.get("value"), { | ||
| updateExistingUsers: this.updateExistingUsers, | ||
| }); | ||
| }, | ||
|
|
||
| staffLogFilter: readOnly("setting.staffLogFilter"), | ||
| }); |
| @@ -0,0 +1,6 @@ | ||
| import ImageUploader from "discourse/components/image-uploader"; | ||
|
|
||
| export default ImageUploader.extend({ | ||
| layoutName: "components/image-uploader", | ||
| uploadUrlParams: "&for_site_setting=true", | ||
| }); |
| @@ -0,0 +1,19 @@ | ||
| import Component from "@ember/component"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { isEmpty } from "@ember/utils"; | ||
|
|
||
| export default Component.extend({ | ||
| @discourseComputed("value") | ||
| enabled: { | ||
| get(value) { | ||
| if (isEmpty(value)) { | ||
| return false; | ||
| } | ||
| return value.toString() === "true"; | ||
| }, | ||
| set(value) { | ||
| this.set("value", value ? "true" : "false"); | ||
| return value; | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,15 @@ | ||
| import Category from "discourse/models/category"; | ||
| import Component from "@ember/component"; | ||
| import { computed } from "@ember/object"; | ||
|
|
||
| export default Component.extend({ | ||
| selectedCategories: computed("value", function () { | ||
| return Category.findByIds(this.value.split("|").filter(Boolean)); | ||
| }), | ||
|
|
||
| actions: { | ||
| onChangeSelectedCategories(value) { | ||
| this.set("value", (value || []).mapBy("id").join("|")); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,52 @@ | ||
| import { action, computed } from "@ember/object"; | ||
| import Component from "@ember/component"; | ||
|
|
||
| function RGBToHex(rgb) { | ||
| // Choose correct separator | ||
| let sep = rgb.indexOf(",") > -1 ? "," : " "; | ||
| // Turn "rgb(r,g,b)" into [r,g,b] | ||
| rgb = rgb.substr(4).split(")")[0].split(sep); | ||
|
|
||
| let r = (+rgb[0]).toString(16), | ||
| g = (+rgb[1]).toString(16), | ||
| b = (+rgb[2]).toString(16); | ||
|
|
||
| if (r.length === 1) { | ||
| r = "0" + r; | ||
| } | ||
| if (g.length === 1) { | ||
| g = "0" + g; | ||
| } | ||
| if (b.length === 1) { | ||
| b = "0" + b; | ||
| } | ||
|
|
||
| return "#" + r + g + b; | ||
| } | ||
|
|
||
| export default Component.extend({ | ||
| valid: computed("value", function () { | ||
| let value = this.value.toLowerCase(); | ||
|
|
||
| let testColor = new Option().style; | ||
| testColor.color = value; | ||
|
|
||
| if (!testColor.color && !value.startsWith("#")) { | ||
| value = `#${value}`; | ||
| testColor = new Option().style; | ||
| testColor.color = value; | ||
| } | ||
|
|
||
| let hexifiedColor = RGBToHex(testColor.color); | ||
| if (hexifiedColor.includes("NaN")) { | ||
| hexifiedColor = testColor.color; | ||
| } | ||
|
|
||
| return testColor.color && hexifiedColor === value; | ||
| }), | ||
|
|
||
| @action | ||
| onChangeColor(color) { | ||
| this.set("value", color); | ||
| }, | ||
| }); |
| @@ -0,0 +1,40 @@ | ||
| import Component from "@ember/component"; | ||
| import { computed } from "@ember/object"; | ||
| import { makeArray } from "discourse-common/lib/helpers"; | ||
|
|
||
| export default Component.extend({ | ||
| tokenSeparator: "|", | ||
|
|
||
| createdChoices: null, | ||
|
|
||
| settingValue: computed("value", function () { | ||
| return this.value.toString().split(this.tokenSeparator).filter(Boolean); | ||
| }), | ||
|
|
||
| settingChoices: computed( | ||
| "settingValue", | ||
| "setting.choices.[]", | ||
| "createdChoices.[]", | ||
| function () { | ||
| return [ | ||
| ...new Set([ | ||
| ...makeArray(this.settingValue), | ||
| ...makeArray(this.setting.choices), | ||
| ...makeArray(this.createdChoices), | ||
| ]), | ||
| ]; | ||
| } | ||
| ), | ||
|
|
||
| actions: { | ||
| onChangeListSetting(value) { | ||
| this.set("value", value.join(this.tokenSeparator)); | ||
| }, | ||
|
|
||
| onChangeChoices(choices) { | ||
| this.set("createdChoices", [ | ||
| ...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]), | ||
| ]); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,25 @@ | ||
| import Component from "@ember/component"; | ||
| import { computed } from "@ember/object"; | ||
|
|
||
| export default Component.extend({ | ||
| tokenSeparator: "|", | ||
|
|
||
| nameProperty: "name", | ||
| valueProperty: "id", | ||
|
|
||
| groupChoices: computed("site.groups", function () { | ||
| return (this.site.groups || []).map((g) => { | ||
| return { name: g.name, id: g.id.toString() }; | ||
| }); | ||
| }), | ||
|
|
||
| settingValue: computed("value", function () { | ||
| return (this.value || "").split(this.tokenSeparator).filter(Boolean); | ||
| }), | ||
|
|
||
| actions: { | ||
| onChangeGroupListSetting(value) { | ||
| this.set("value", value.join(this.tokenSeparator)); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,11 @@ | ||
| import Component from "@ember/component"; | ||
| import { action } from "@ember/object"; | ||
|
|
||
| export default Component.extend({ | ||
| inputDelimiter: "|", | ||
|
|
||
| @action | ||
| onChange(value) { | ||
| this.set("value", value.join(this.inputDelimiter || "\n")); | ||
| }, | ||
| }); |
| @@ -0,0 +1,17 @@ | ||
| import Component from "@ember/component"; | ||
| import { action } from "@ember/object"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend({ | ||
| @discourseComputed("value") | ||
| selectedTags: { | ||
| get(value) { | ||
| return value.split("|").filter(Boolean); | ||
| }, | ||
| }, | ||
|
|
||
| @action | ||
| changeSelectedTags(tags) { | ||
| this.set("value", tags.join("|")); | ||
| }, | ||
| }); |
| @@ -0,0 +1,16 @@ | ||
| import Component from "@ember/component"; | ||
| import showModal from "discourse/lib/show-modal"; | ||
|
|
||
| export default Component.extend({ | ||
| actions: { | ||
| showUploadModal({ value, setting }) { | ||
| showModal("admin-uploaded-image-list", { | ||
| admin: true, | ||
| title: `admin.site_settings.${setting.setting}.title`, | ||
| model: { value, setting }, | ||
| }).setProperties({ | ||
| save: (v) => this.set("value", v), | ||
| }); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,42 @@ | ||
| import Component from "@ember/component"; | ||
| import highlightHTML from "discourse/lib/highlight-html"; | ||
| import { on } from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["site-text"], | ||
| classNameBindings: ["siteText.overridden"], | ||
|
|
||
| @on("didInsertElement") | ||
| highlightTerm() { | ||
| const term = this._searchTerm(); | ||
|
|
||
| if (term) { | ||
| highlightHTML( | ||
| this.element.querySelector(".site-text-id, .site-text-value"), | ||
| term, | ||
| { | ||
| className: "text-highlight", | ||
| } | ||
| ); | ||
| } | ||
| $(this.element.querySelector(".site-text-value")).ellipsis(); | ||
| }, | ||
|
|
||
| click() { | ||
| this.editAction(this.siteText); | ||
| }, | ||
|
|
||
| _searchTerm() { | ||
| const regex = this.searchRegex; | ||
| const siteText = this.siteText; | ||
|
|
||
| if (regex && siteText) { | ||
| const matches = siteText.value.match(new RegExp(regex, "i")); | ||
| if (matches) { | ||
| return matches[0]; | ||
| } | ||
| } | ||
|
|
||
| return this.term; | ||
| }, | ||
| }); |
| @@ -0,0 +1,39 @@ | ||
| import Component from "@ember/component"; | ||
| import DiscourseURL from "discourse/lib/url"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["table", "staff-actions"], | ||
|
|
||
| willDestroyElement() { | ||
| $(this.element).off("click.discourse-staff-logs"); | ||
| }, | ||
|
|
||
| didInsertElement() { | ||
| this._super(...arguments); | ||
|
|
||
| $(this.element).on( | ||
| "click.discourse-staff-logs", | ||
| "[data-link-post-id]", | ||
| (e) => { | ||
| let postId = $(e.target).attr("data-link-post-id"); | ||
|
|
||
| this.store.find("post", postId).then((p) => { | ||
| DiscourseURL.routeTo(p.get("url")); | ||
| }); | ||
| return false; | ||
| } | ||
| ); | ||
|
|
||
| $(this.element).on( | ||
| "click.discourse-staff-logs", | ||
| "[data-link-topic-id]", | ||
| (e) => { | ||
| let topicId = $(e.target).attr("data-link-topic-id"); | ||
|
|
||
| DiscourseURL.routeTo(`/t/${topicId}`); | ||
|
|
||
| return false; | ||
| } | ||
| ); | ||
| }, | ||
| }); |
| @@ -0,0 +1,52 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import { action } from "@ember/object"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { equal } from "@ember/object/computed"; | ||
|
|
||
| const CUSTOM_REASON_KEY = "custom"; | ||
|
|
||
| export default Component.extend({ | ||
| tagName: "", | ||
| selectedReason: CUSTOM_REASON_KEY, | ||
| customReason: "", | ||
| reasonKeys: [ | ||
| "not_listening_to_staff", | ||
| "consuming_staff_time", | ||
| "combatative", | ||
| "in_wrong_place", | ||
| "no_constructive_purpose", | ||
| CUSTOM_REASON_KEY, | ||
| ], | ||
| isCustomReason: equal("selectedReason", CUSTOM_REASON_KEY), | ||
|
|
||
| @discourseComputed("reasonKeys") | ||
| reasons(keys) { | ||
| return keys.map((key) => { | ||
| return { id: key, name: I18n.t(`admin.user.suspend_reasons.${key}`) }; | ||
| }); | ||
| }, | ||
|
|
||
| @action | ||
| setSelectedReason(value) { | ||
| this.set("selectedReason", value); | ||
| this.setReason(); | ||
| }, | ||
|
|
||
| @action | ||
| setCustomReason(value) { | ||
| this.set("customReason", value); | ||
| this.setReason(); | ||
| }, | ||
|
|
||
| setReason() { | ||
| if (this.isCustomReason) { | ||
| this.set("reason", this.customReason); | ||
| } else { | ||
| this.set( | ||
| "reason", | ||
| I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`) | ||
| ); | ||
| } | ||
| }, | ||
| }); |
| @@ -0,0 +1,23 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import UploadMixin from "discourse/mixins/upload"; | ||
| import { alias } from "@ember/object/computed"; | ||
| import bootbox from "bootbox"; | ||
|
|
||
| export default Component.extend(UploadMixin, { | ||
| type: "csv", | ||
| uploadUrl: "/tags/upload", | ||
| addDisabled: alias("uploading"), | ||
| elementId: "tag-uploader", | ||
|
|
||
| validateUploadedFilesOptions() { | ||
| return { csvOnly: true }; | ||
| }, | ||
|
|
||
| uploadDone() { | ||
| bootbox.alert(I18n.t("tagging.upload_successful"), () => { | ||
| this.refresh(); | ||
| this.closeModal(); | ||
| }); | ||
| }, | ||
| }); |
| @@ -0,0 +1,20 @@ | ||
| import BufferedContent from "discourse/mixins/buffered-content"; | ||
| import Component from "@ember/component"; | ||
| import SettingComponent from "admin/mixins/setting-component"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
| import { url } from "discourse/lib/computed"; | ||
|
|
||
| export default Component.extend(BufferedContent, SettingComponent, { | ||
| layoutName: "admin/templates/components/site-setting", | ||
| updateUrl: url("model.id", "/admin/themes/%@/setting"), | ||
|
|
||
| _save() { | ||
| return ajax(this.updateUrl, { | ||
| type: "PUT", | ||
| data: { | ||
| name: this.setting.setting, | ||
| value: this.get("buffered.value"), | ||
| }, | ||
| }); | ||
| }, | ||
| }); |
| @@ -0,0 +1,27 @@ | ||
| import BufferedContent from "discourse/mixins/buffered-content"; | ||
| import Component from "@ember/component"; | ||
| import SettingComponent from "admin/mixins/setting-component"; | ||
|
|
||
| export default Component.extend(BufferedContent, SettingComponent, { | ||
| layoutName: "admin/templates/components/site-setting", | ||
|
|
||
| _save() { | ||
| return this.model | ||
| .save({ [this.setting.setting]: this.convertNamesToIds() }) | ||
| .then(() => this.store.findAll("theme")); | ||
| }, | ||
|
|
||
| convertNamesToIds() { | ||
| return this.get("buffered.value") | ||
| .split("|") | ||
| .filter(Boolean) | ||
| .map((themeName) => { | ||
| if (themeName !== "") { | ||
| return this.setting.allThemes.find( | ||
| (theme) => theme.name === themeName | ||
| ).id; | ||
| } | ||
| return themeName; | ||
| }); | ||
| }, | ||
| }); |
| @@ -0,0 +1,18 @@ | ||
| import BufferedContent from "discourse/mixins/buffered-content"; | ||
| import Component from "@ember/component"; | ||
| import SettingComponent from "admin/mixins/setting-component"; | ||
| import { alias } from "@ember/object/computed"; | ||
|
|
||
| export default Component.extend(BufferedContent, SettingComponent, { | ||
| layoutName: "admin/templates/components/site-setting", | ||
| setting: alias("translation"), | ||
| type: "string", | ||
| settingName: alias("translation.key"), | ||
|
|
||
| _save() { | ||
| return this.model.saveTranslation( | ||
| this.get("translation.key"), | ||
| this.get("buffered.value") | ||
| ); | ||
| }, | ||
| }); |
| @@ -0,0 +1,147 @@ | ||
| import { and, gt } from "@ember/object/computed"; | ||
| import discourseComputed, { observes } from "discourse-common/utils/decorators"; | ||
| import Component from "@ember/component"; | ||
| import { escape } from "pretty-text/sanitizer"; | ||
| import { iconHTML } from "discourse-common/lib/icon-library"; | ||
| import { isTesting } from "discourse-common/config/environment"; | ||
| import { schedule } from "@ember/runloop"; | ||
|
|
||
| const MAX_COMPONENTS = 4; | ||
|
|
||
| export default Component.extend({ | ||
| childrenExpanded: false, | ||
| classNames: ["themes-list-item"], | ||
| classNameBindings: ["theme.selected:selected"], | ||
| hasComponents: gt("children.length", 0), | ||
| displayComponents: and("hasComponents", "theme.isActive"), | ||
| displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS), | ||
|
|
||
| click(e) { | ||
| if (!$(e.target).hasClass("others-count")) { | ||
| this.navigateToTheme(); | ||
| } | ||
| }, | ||
|
|
||
| init() { | ||
| this._super(...arguments); | ||
| this.scheduleAnimation(); | ||
| }, | ||
|
|
||
| @observes("theme.selected") | ||
| triggerAnimation() { | ||
| this.animate(); | ||
| }, | ||
|
|
||
| scheduleAnimation() { | ||
| schedule("afterRender", () => { | ||
| this.animate(true); | ||
| }); | ||
| }, | ||
|
|
||
| animate(isInitial) { | ||
| const $container = $(this.element); | ||
| const $list = $(this.element.querySelector(".components-list")); | ||
| if ($list.length === 0 || isTesting()) { | ||
| return; | ||
| } | ||
| const duration = 300; | ||
| if (this.get("theme.selected")) { | ||
| this.collapseComponentsList($container, $list, duration); | ||
| } else if (!isInitial) { | ||
| this.expandComponentsList($container, $list, duration); | ||
| } | ||
| }, | ||
|
|
||
| @discourseComputed( | ||
| "theme.component", | ||
| "theme.childThemes.@each.name", | ||
| "theme.childThemes.length", | ||
| "childrenExpanded" | ||
| ) | ||
| children() { | ||
| const theme = this.theme; | ||
| let children = theme.get("childThemes"); | ||
| if (theme.get("component") || !children) { | ||
| return []; | ||
| } | ||
| children = this.childrenExpanded | ||
| ? children | ||
| : children.slice(0, MAX_COMPONENTS); | ||
| return children.map((t) => { | ||
| const name = escape(t.name); | ||
| return t.enabled ? name : `${iconHTML("ban")} ${name}`; | ||
| }); | ||
| }, | ||
|
|
||
| @discourseComputed("children") | ||
| childrenString(children) { | ||
| return children.join(", "); | ||
| }, | ||
|
|
||
| @discourseComputed( | ||
| "theme.childThemes.length", | ||
| "theme.component", | ||
| "childrenExpanded", | ||
| "children.length" | ||
| ) | ||
| moreCount(childrenCount, component, expanded) { | ||
| if (component || !childrenCount || expanded) { | ||
| return 0; | ||
| } | ||
| return childrenCount - MAX_COMPONENTS; | ||
| }, | ||
|
|
||
| expandComponentsList($container, $list, duration) { | ||
| $container.css("height", `${$container.height()}px`); | ||
| $list.css("display", ""); | ||
| $container.animate( | ||
| { | ||
| height: `${$container.height() + $list.outerHeight(true)}px`, | ||
| }, | ||
| { | ||
| duration, | ||
| done: () => { | ||
| $list.css("display", ""); | ||
| $container.css("height", ""); | ||
| }, | ||
| } | ||
| ); | ||
| $list.animate( | ||
| { | ||
| opacity: 1, | ||
| }, | ||
| { | ||
| duration, | ||
| } | ||
| ); | ||
| }, | ||
|
|
||
| collapseComponentsList($container, $list, duration) { | ||
| $container.animate( | ||
| { | ||
| height: `${$container.height() - $list.outerHeight(true)}px`, | ||
| }, | ||
| { | ||
| duration, | ||
| done: () => { | ||
| $list.css("display", "none"); | ||
| $container.css("height", ""); | ||
| }, | ||
| } | ||
| ); | ||
| $list.animate( | ||
| { | ||
| opacity: 0, | ||
| }, | ||
| { | ||
| duration, | ||
| } | ||
| ); | ||
| }, | ||
|
|
||
| actions: { | ||
| toggleChildrenExpanded() { | ||
| this.toggleProperty("childrenExpanded"); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,81 @@ | ||
| import { COMPONENTS, THEMES } from "admin/models/theme"; | ||
| import { equal, gt } from "@ember/object/computed"; | ||
| import Component from "@ember/component"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { inject as service } from "@ember/service"; | ||
|
|
||
| export default Component.extend({ | ||
| router: service(), | ||
| THEMES, | ||
| COMPONENTS, | ||
|
|
||
| classNames: ["themes-list"], | ||
|
|
||
| hasThemes: gt("themesList.length", 0), | ||
| hasActiveThemes: gt("activeThemes.length", 0), | ||
| hasInactiveThemes: gt("inactiveThemes.length", 0), | ||
|
|
||
| themesTabActive: equal("currentTab", THEMES), | ||
| componentsTabActive: equal("currentTab", COMPONENTS), | ||
|
|
||
| @discourseComputed("themes", "components", "currentTab") | ||
| themesList(themes, components) { | ||
| if (this.themesTabActive) { | ||
| return themes; | ||
| } else { | ||
| return components; | ||
| } | ||
| }, | ||
|
|
||
| @discourseComputed( | ||
| "themesList", | ||
| "currentTab", | ||
| "themesList.@each.user_selectable", | ||
| "themesList.@each.default" | ||
| ) | ||
| inactiveThemes(themes) { | ||
| if (this.componentsTabActive) { | ||
| return themes.filter((theme) => theme.get("parent_themes.length") <= 0); | ||
| } | ||
| return themes.filter( | ||
| (theme) => !theme.get("user_selectable") && !theme.get("default") | ||
| ); | ||
| }, | ||
|
|
||
| @discourseComputed( | ||
| "themesList", | ||
| "currentTab", | ||
| "themesList.@each.user_selectable", | ||
| "themesList.@each.default" | ||
| ) | ||
| activeThemes(themes) { | ||
| if (this.componentsTabActive) { | ||
| return themes.filter((theme) => theme.get("parent_themes.length") > 0); | ||
| } else { | ||
| return themes | ||
| .filter((theme) => theme.get("user_selectable") || theme.get("default")) | ||
| .sort((a, b) => { | ||
| if (a.get("default") && !b.get("default")) { | ||
| return -1; | ||
| } else if (b.get("default")) { | ||
| return 1; | ||
| } | ||
| return a | ||
| .get("name") | ||
| .toLowerCase() | ||
| .localeCompare(b.get("name").toLowerCase()); | ||
| }); | ||
| } | ||
| }, | ||
|
|
||
| actions: { | ||
| changeView(newTab) { | ||
| if (newTab !== this.currentTab) { | ||
| this.set("currentTab", newTab); | ||
| } | ||
| }, | ||
| navigateToTheme(theme) { | ||
| this.router.transitionTo("adminCustomizeThemes.show", theme); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,109 @@ | ||
| import discourseComputed, { on } from "discourse-common/utils/decorators"; | ||
| import { empty, reads } from "@ember/object/computed"; | ||
| import Component from "@ember/component"; | ||
| import { makeArray } from "discourse-common/lib/helpers"; | ||
|
|
||
| export default Component.extend({ | ||
| classNameBindings: [":value-list"], | ||
| inputInvalid: empty("newValue"), | ||
| inputDelimiter: null, | ||
| inputType: null, | ||
| newValue: "", | ||
| collection: null, | ||
| values: null, | ||
| noneKey: reads("addKey"), | ||
|
|
||
| @on("didReceiveAttrs") | ||
| _setupCollection() { | ||
| const values = this.values; | ||
| if (this.inputType === "array") { | ||
| this.set("collection", values || []); | ||
| return; | ||
| } | ||
|
|
||
| this.set( | ||
| "collection", | ||
| this._splitValues(values, this.inputDelimiter || "\n") | ||
| ); | ||
| }, | ||
|
|
||
| @discourseComputed("choices.[]", "collection.[]") | ||
| filteredChoices(choices, collection) { | ||
| return makeArray(choices).filter((i) => collection.indexOf(i) < 0); | ||
| }, | ||
|
|
||
| keyDown(event) { | ||
| if (event.keyCode === 13) { | ||
| this.send("addValue", this.newValue); | ||
| } | ||
| }, | ||
|
|
||
| actions: { | ||
| changeValue(index, newValue) { | ||
| this._replaceValue(index, newValue); | ||
| }, | ||
|
|
||
| addValue(newValue) { | ||
| if (this.inputInvalid) { | ||
| return; | ||
| } | ||
|
|
||
| this.set("newValue", null); | ||
| this._addValue(newValue); | ||
| }, | ||
|
|
||
| removeValue(value) { | ||
| this._removeValue(value); | ||
| }, | ||
|
|
||
| selectChoice(choice) { | ||
| this._addValue(choice); | ||
| }, | ||
| }, | ||
|
|
||
| _addValue(value) { | ||
| this.collection.addObject(value); | ||
|
|
||
| if (this.choices) { | ||
| this.set("choices", this.choices.rejectBy("id", value)); | ||
| } else { | ||
| this.set("choices", []); | ||
| } | ||
|
|
||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _removeValue(value) { | ||
| this.collection.removeObject(value); | ||
|
|
||
| if (this.choices) { | ||
| this.set("choices", this.choices.concat([value]).uniq()); | ||
| } else { | ||
| this.set("choices", makeArray(value)); | ||
| } | ||
|
|
||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _replaceValue(index, newValue) { | ||
| this.collection.replace(index, 1, [newValue]); | ||
| this._saveValues(); | ||
| }, | ||
|
|
||
| _saveValues() { | ||
| if (this.inputType === "array") { | ||
| this.set("values", this.collection); | ||
| return; | ||
| } | ||
|
|
||
| this.set("values", this.collection.join(this.inputDelimiter || "\n")); | ||
| }, | ||
|
|
||
| _splitValues(values, delimiter) { | ||
| if (values && values.length) { | ||
| return values.split(delimiter).filter((x) => x); | ||
| } else { | ||
| return []; | ||
| } | ||
| }, | ||
| }); |
| @@ -0,0 +1,102 @@ | ||
| import discourseComputed, { | ||
| observes, | ||
| on, | ||
| } from "discourse-common/utils/decorators"; | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import WatchedWord from "admin/models/watched-word"; | ||
| import bootbox from "bootbox"; | ||
| import { isEmpty } from "@ember/utils"; | ||
| import { schedule } from "@ember/runloop"; | ||
|
|
||
| export default Component.extend({ | ||
| classNames: ["watched-word-form"], | ||
| formSubmitted: false, | ||
| actionKey: null, | ||
| showMessage: false, | ||
|
|
||
| @discourseComputed("regularExpressions") | ||
| placeholderKey(regularExpressions) { | ||
| return ( | ||
| "admin.watched_words.form.placeholder" + | ||
| (regularExpressions ? "_regexp" : "") | ||
| ); | ||
| }, | ||
|
|
||
| @observes("word") | ||
| removeMessage() { | ||
| if (this.showMessage && !isEmpty(this.word)) { | ||
| this.set("showMessage", false); | ||
| } | ||
| }, | ||
|
|
||
| @discourseComputed("word") | ||
| isUniqueWord(word) { | ||
| const words = this.filteredContent || []; | ||
| const filtered = words.filter( | ||
| (content) => content.action === this.actionKey | ||
| ); | ||
| return filtered.every( | ||
| (content) => content.word.toLowerCase() !== word.toLowerCase() | ||
| ); | ||
| }, | ||
|
|
||
| actions: { | ||
| submit() { | ||
| if (!this.isUniqueWord) { | ||
| this.setProperties({ | ||
| showMessage: true, | ||
| message: I18n.t("admin.watched_words.form.exists"), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| if (!this.formSubmitted) { | ||
| this.set("formSubmitted", true); | ||
|
|
||
| const watchedWord = WatchedWord.create({ | ||
| word: this.word, | ||
| action: this.actionKey, | ||
| }); | ||
|
|
||
| watchedWord | ||
| .save() | ||
| .then((result) => { | ||
| this.setProperties({ | ||
| word: "", | ||
| formSubmitted: false, | ||
| showMessage: true, | ||
| message: I18n.t("admin.watched_words.form.success"), | ||
| }); | ||
| this.action(WatchedWord.create(result)); | ||
| schedule("afterRender", () => | ||
| this.element.querySelector(".watched-word-input").focus() | ||
| ); | ||
| }) | ||
| .catch((e) => { | ||
| this.set("formSubmitted", false); | ||
| const msg = | ||
| e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors | ||
| ? I18n.t("generic_error_with_reason", { | ||
| error: e.jqXHR.responseJSON.errors.join(". "), | ||
| }) | ||
| : I18n.t("generic_error"); | ||
| bootbox.alert(msg, () => | ||
| this.element.querySelector(".watched-word-input").focus() | ||
| ); | ||
| }); | ||
| } | ||
| }, | ||
| }, | ||
|
|
||
| @on("didInsertElement") | ||
| _init() { | ||
| schedule("afterRender", () => { | ||
| $(this.element.querySelector(".watched-word-input")).keydown((e) => { | ||
| if (e.keyCode === 13) { | ||
| this.send("submit"); | ||
| } | ||
| }); | ||
| }); | ||
| }, | ||
| }); |
| @@ -0,0 +1,29 @@ | ||
| import Component from "@ember/component"; | ||
| import I18n from "I18n"; | ||
| import UploadMixin from "discourse/mixins/upload"; | ||
| import { alias } from "@ember/object/computed"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Component.extend(UploadMixin, { | ||
| type: "txt", | ||
| classNames: "watched-words-uploader", | ||
| uploadUrl: "/admin/logs/watched_words/upload", | ||
| addDisabled: alias("uploading"), | ||
|
|
||
| validateUploadedFilesOptions() { | ||
| return { skipValidation: true }; | ||
| }, | ||
|
|
||
| @discourseComputed("actionKey") | ||
| data(actionKey) { | ||
| return { action_key: actionKey }; | ||
| }, | ||
|
|
||
| uploadDone() { | ||
| if (this) { | ||
| bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); | ||
| this.done(); | ||
| } | ||
| }, | ||
| }); |
| @@ -0,0 +1,14 @@ | ||
| import Controller from "@ember/controller"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
|
|
||
| export default Controller.extend({ | ||
| actions: { | ||
| revokeKey(key) { | ||
| key.revoke().catch(popupAjaxError); | ||
| }, | ||
|
|
||
| undoRevokeKey(key) { | ||
| key.undoRevoke().catch(popupAjaxError); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,72 @@ | ||
| import Controller from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { isBlank } from "@ember/utils"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
| import { get } from "@ember/object"; | ||
| import showModal from "discourse/lib/show-modal"; | ||
|
|
||
| export default Controller.extend({ | ||
| userModes: [ | ||
| { id: "all", name: I18n.t("admin.api.all_users") }, | ||
| { id: "single", name: I18n.t("admin.api.single_user") }, | ||
| ], | ||
| useGlobalKey: false, | ||
| scopes: null, | ||
|
|
||
| @discourseComputed("userMode") | ||
| showUserSelector(mode) { | ||
| return mode === "single"; | ||
| }, | ||
|
|
||
| @discourseComputed("model.description", "model.username", "userMode") | ||
| saveDisabled(description, username, userMode) { | ||
| if (isBlank(description)) { | ||
| return true; | ||
| } | ||
| if (userMode === "single" && isBlank(username)) { | ||
| return true; | ||
| } | ||
| return false; | ||
| }, | ||
|
|
||
| actions: { | ||
| updateUsername(selected) { | ||
| this.set("model.username", get(selected, "firstObject")); | ||
| }, | ||
|
|
||
| changeUserMode(value) { | ||
| if (value === "all") { | ||
| this.model.set("username", null); | ||
| } | ||
| this.set("userMode", value); | ||
| }, | ||
|
|
||
| save() { | ||
| if (!this.useGlobalKey) { | ||
| const selectedScopes = Object.values(this.scopes) | ||
| .flat() | ||
| .filter((action) => { | ||
| return action.selected; | ||
| }); | ||
|
|
||
| this.model.set("scopes", selectedScopes); | ||
| } | ||
|
|
||
| this.model.save().catch(popupAjaxError); | ||
| }, | ||
|
|
||
| continue() { | ||
| this.transitionToRoute("adminApiKeys.show", this.model.id); | ||
| }, | ||
|
|
||
| showURLs(urls) { | ||
| return showModal("admin-api-key-urls", { | ||
| admin: true, | ||
| model: { | ||
| urls, | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,66 @@ | ||
| import Controller from "@ember/controller"; | ||
| import { bufferedProperty } from "discourse/mixins/buffered-content"; | ||
| import { empty } from "@ember/object/computed"; | ||
| import { isEmpty } from "@ember/utils"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
| import showModal from "discourse/lib/show-modal"; | ||
|
|
||
| export default Controller.extend(bufferedProperty("model"), { | ||
| isNew: empty("model.id"), | ||
|
|
||
| actions: { | ||
| saveDescription() { | ||
| const buffered = this.buffered; | ||
| const attrs = buffered.getProperties("description"); | ||
|
|
||
| this.model | ||
| .save(attrs) | ||
| .then(() => { | ||
| this.set("editingDescription", false); | ||
| this.rollbackBuffer(); | ||
| }) | ||
| .catch(popupAjaxError); | ||
| }, | ||
|
|
||
| cancel() { | ||
| const id = this.get("userField.id"); | ||
| if (isEmpty(id)) { | ||
| this.destroyAction(this.userField); | ||
| } else { | ||
| this.rollbackBuffer(); | ||
| this.set("editing", false); | ||
| } | ||
| }, | ||
|
|
||
| editDescription() { | ||
| this.toggleProperty("editingDescription"); | ||
| if (!this.editingDescription) { | ||
| this.rollbackBuffer(); | ||
| } | ||
| }, | ||
|
|
||
| revokeKey(key) { | ||
| key.revoke().catch(popupAjaxError); | ||
| }, | ||
|
|
||
| deleteKey(key) { | ||
| key | ||
| .destroyRecord() | ||
| .then(() => this.transitionToRoute("adminApiKeys.index")) | ||
| .catch(popupAjaxError); | ||
| }, | ||
|
|
||
| undoRevokeKey(key) { | ||
| key.undoRevoke().catch(popupAjaxError); | ||
| }, | ||
|
|
||
| showURLs(urls) { | ||
| return showModal("admin-api-key-urls", { | ||
| admin: true, | ||
| model: { | ||
| urls, | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,60 @@ | ||
| import Controller, { inject as controller } from "@ember/controller"; | ||
| import { alias, equal } from "@ember/object/computed"; | ||
| import { i18n, setting } from "discourse/lib/computed"; | ||
| import I18n from "I18n"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Controller.extend({ | ||
| adminBackups: controller(), | ||
| status: alias("adminBackups.model"), | ||
| uploadLabel: i18n("admin.backups.upload.label"), | ||
| backupLocation: setting("backup_location"), | ||
| localBackupStorage: equal("backupLocation", "local"), | ||
|
|
||
| @discourseComputed("status.allowRestore", "status.isOperationRunning") | ||
| restoreTitle(allowRestore, isOperationRunning) { | ||
| if (!allowRestore) { | ||
| return "admin.backups.operations.restore.is_disabled"; | ||
| } else if (isOperationRunning) { | ||
| return "admin.backups.operations.is_running"; | ||
| } else { | ||
| return "admin.backups.operations.restore.title"; | ||
| } | ||
| }, | ||
|
|
||
| actions: { | ||
| toggleReadOnlyMode() { | ||
| if (!this.site.get("isReadOnly")) { | ||
| bootbox.confirm( | ||
| I18n.t("admin.backups.read_only.enable.confirm"), | ||
| I18n.t("no_value"), | ||
| I18n.t("yes_value"), | ||
| (confirmed) => { | ||
| if (confirmed) { | ||
| this.set("currentUser.hideReadOnlyAlert", true); | ||
| this._toggleReadOnlyMode(true); | ||
| } | ||
| } | ||
| ); | ||
| } else { | ||
| this._toggleReadOnlyMode(false); | ||
| } | ||
| }, | ||
|
|
||
| download(backup) { | ||
| const link = backup.get("filename"); | ||
| ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() => | ||
| bootbox.alert(I18n.t("admin.backups.operations.download.alert")) | ||
| ); | ||
| }, | ||
| }, | ||
|
|
||
| _toggleReadOnlyMode(enable) { | ||
| ajax("/admin/backups/readonly", { | ||
| type: "PUT", | ||
| data: { enable }, | ||
| }).then(() => this.site.set("isReadOnly", enable)); | ||
| }, | ||
| }); |
| @@ -0,0 +1,13 @@ | ||
| import Controller, { inject as controller } from "@ember/controller"; | ||
| import { alias } from "@ember/object/computed"; | ||
|
|
||
| export default Controller.extend({ | ||
| adminBackups: controller(), | ||
| status: alias("adminBackups.model"), | ||
|
|
||
| init() { | ||
| this._super(...arguments); | ||
|
|
||
| this.logs = []; | ||
| }, | ||
| }); |
| @@ -0,0 +1,11 @@ | ||
| import { and, not } from "@ember/object/computed"; | ||
| import Controller from "@ember/controller"; | ||
| export default Controller.extend({ | ||
| noOperationIsRunning: not("model.isOperationRunning"), | ||
| rollbackEnabled: and( | ||
| "model.canRollback", | ||
| "model.restoreEnabled", | ||
| "noOperationIsRunning" | ||
| ), | ||
| rollbackDisabled: not("rollbackEnabled"), | ||
| }); |
| @@ -0,0 +1,39 @@ | ||
| import Controller from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
| import bootbox from "bootbox"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
|
|
||
| export default Controller.extend({ | ||
| saving: false, | ||
| replaceBadgeOwners: false, | ||
|
|
||
| actions: { | ||
| massAward() { | ||
| const file = document.querySelector("#massAwardCSVUpload").files[0]; | ||
|
|
||
| if (this.model && file) { | ||
| const options = { | ||
| type: "POST", | ||
| processData: false, | ||
| contentType: false, | ||
| data: new FormData(), | ||
| }; | ||
|
|
||
| options.data.append("file", file); | ||
| options.data.append("replace_badge_owners", this.replaceBadgeOwners); | ||
|
|
||
| this.set("saving", true); | ||
|
|
||
| ajax(`/admin/badges/award/${this.model.id}`, options) | ||
| .then(() => { | ||
| bootbox.alert(I18n.t("admin.badges.mass_award.success")); | ||
| }) | ||
| .catch(popupAjaxError) | ||
| .finally(() => this.set("saving", false)); | ||
| } else { | ||
| bootbox.alert(I18n.t("admin.badges.mass_award.aborted")); | ||
| } | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,173 @@ | ||
| import Controller, { inject as controller } from "@ember/controller"; | ||
| import discourseComputed, { observes } from "discourse-common/utils/decorators"; | ||
| import I18n from "I18n"; | ||
| import bootbox from "bootbox"; | ||
| import { bufferedProperty } from "discourse/mixins/buffered-content"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
| import { propertyNotEqual } from "discourse/lib/computed"; | ||
| import { reads } from "@ember/object/computed"; | ||
| import { run } from "@ember/runloop"; | ||
|
|
||
| export default Controller.extend(bufferedProperty("model"), { | ||
| adminBadges: controller(), | ||
| saving: false, | ||
| savingStatus: "", | ||
| badgeTypes: reads("adminBadges.badgeTypes"), | ||
| badgeGroupings: reads("adminBadges.badgeGroupings"), | ||
| badgeTriggers: reads("adminBadges.badgeTriggers"), | ||
| protectedSystemFields: reads("adminBadges.protectedSystemFields"), | ||
| readOnly: reads("buffered.system"), | ||
| showDisplayName: propertyNotEqual("name", "displayName"), | ||
|
|
||
| init() { | ||
| this._super(...arguments); | ||
|
|
||
| // this is needed because the model doesnt have default values | ||
| // and as we are using a bufferedProperty it's not accessible | ||
| // in any other way | ||
| run.next(() => { | ||
| if (this.model) { | ||
| if (!this.model.badge_type_id) { | ||
| this.model.set( | ||
| "badge_type_id", | ||
| this.get("badgeTypes.firstObject.id") | ||
| ); | ||
| } | ||
|
|
||
| if (!this.model.badge_grouping_id) { | ||
| this.model.set( | ||
| "badge_grouping_id", | ||
| this.get("badgeGroupings.firstObject.id") | ||
| ); | ||
| } | ||
|
|
||
| if (!this.model.trigger) { | ||
| this.model.set("trigger", this.get("badgeTriggers.firstObject.id")); | ||
| } | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
| @discourseComputed("model.query", "buffered.query") | ||
| hasQuery(modelQuery, bufferedQuery) { | ||
| if (bufferedQuery) { | ||
| return bufferedQuery.trim().length > 0; | ||
| } | ||
| return modelQuery && modelQuery.trim().length > 0; | ||
| }, | ||
|
|
||
| @discourseComputed("model.i18n_name") | ||
| textCustomizationPrefix(i18n_name) { | ||
| return `badges.${i18n_name}.`; | ||
| }, | ||
|
|
||
| @observes("model.id") | ||
| _resetSaving: function () { | ||
| this.set("saving", false); | ||
| this.set("savingStatus", ""); | ||
| }, | ||
|
|
||
| actions: { | ||
| save() { | ||
| if (!this.saving) { | ||
| let fields = [ | ||
| "allow_title", | ||
| "multiple_grant", | ||
| "listable", | ||
| "auto_revoke", | ||
| "enabled", | ||
| "show_posts", | ||
| "target_posts", | ||
| "name", | ||
| "description", | ||
| "long_description", | ||
| "icon", | ||
| "image", | ||
| "query", | ||
| "badge_grouping_id", | ||
| "trigger", | ||
| "badge_type_id", | ||
| ]; | ||
|
|
||
| if (this.get("buffered.system")) { | ||
| let protectedFields = this.protectedSystemFields || []; | ||
| fields = fields.filter((f) => !protectedFields.includes(f)); | ||
| } | ||
|
|
||
| this.set("saving", true); | ||
| this.set("savingStatus", I18n.t("saving")); | ||
|
|
||
| const boolFields = [ | ||
| "allow_title", | ||
| "multiple_grant", | ||
| "listable", | ||
| "auto_revoke", | ||
| "enabled", | ||
| "show_posts", | ||
| "target_posts", | ||
| ]; | ||
|
|
||
| const data = {}; | ||
| const buffered = this.buffered; | ||
| fields.forEach(function (field) { | ||
| let d = buffered.get(field); | ||
| if (boolFields.includes(field)) { | ||
| d = !!d; | ||
| } | ||
| data[field] = d; | ||
| }); | ||
|
|
||
| const newBadge = !this.id; | ||
| const model = this.model; | ||
| this.model | ||
| .save(data) | ||
| .then(() => { | ||
| if (newBadge) { | ||
| const adminBadges = this.get("adminBadges.model"); | ||
| if (!adminBadges.includes(model)) { | ||
| adminBadges.pushObject(model); | ||
| } | ||
| this.transitionToRoute("adminBadges.show", model.get("id")); | ||
| } else { | ||
| this.commitBuffer(); | ||
| this.set("savingStatus", I18n.t("saved")); | ||
| } | ||
| }) | ||
| .catch(popupAjaxError) | ||
| .finally(() => { | ||
| this.set("saving", false); | ||
| this.set("savingStatus", ""); | ||
| }); | ||
| } | ||
| }, | ||
|
|
||
| destroy() { | ||
| const adminBadges = this.get("adminBadges.model"); | ||
| const model = this.model; | ||
|
|
||
| if (!model.get("id")) { | ||
| this.transitionToRoute("adminBadges.index"); | ||
| return; | ||
| } | ||
|
|
||
| return bootbox.confirm( | ||
| I18n.t("admin.badges.delete_confirm"), | ||
| I18n.t("no_value"), | ||
| I18n.t("yes_value"), | ||
| (result) => { | ||
| if (result) { | ||
| model | ||
| .destroy() | ||
| .then(() => { | ||
| adminBadges.removeObject(model); | ||
| this.transitionToRoute("adminBadges.index"); | ||
| }) | ||
| .catch(() => { | ||
| bootbox.alert(I18n.t("generic_error")); | ||
| }); | ||
| } | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,18 @@ | ||
| import Controller from "@ember/controller"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { inject as service } from "@ember/service"; | ||
|
|
||
| export default Controller.extend({ | ||
| routing: service("-routing"), | ||
|
|
||
| @discourseComputed("routing.currentRouteName") | ||
| selectedRoute() { | ||
| const currentRoute = this.routing.currentRouteName; | ||
| const indexRoute = "adminBadges.index"; | ||
| if (currentRoute === indexRoute) { | ||
| return "adminBadges.show"; | ||
| } else { | ||
| return this.routing.currentRouteName; | ||
| } | ||
| }, | ||
| }); |
| @@ -0,0 +1,96 @@ | ||
| import Controller from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { later } from "@ember/runloop"; | ||
|
|
||
| export default Controller.extend({ | ||
| @discourseComputed("model.colors", "onlyOverridden") | ||
| colors(allColors, onlyOverridden) { | ||
| if (onlyOverridden) { | ||
| return allColors.filter((color) => color.get("overridden")); | ||
| } else { | ||
| return allColors; | ||
| } | ||
| }, | ||
|
|
||
| actions: { | ||
| revert: function (color) { | ||
| color.revert(); | ||
| }, | ||
|
|
||
| undo: function (color) { | ||
| color.undo(); | ||
| }, | ||
|
|
||
| copyToClipboard() { | ||
| $(".table.colors").hide(); | ||
| let area = $("<textarea id='copy-range'></textarea>"); | ||
| $(".table.colors").after(area); | ||
| area.text(this.model.schemeJson()); | ||
| let range = document.createRange(); | ||
| range.selectNode(area[0]); | ||
| window.getSelection().addRange(range); | ||
| let successful = document.execCommand("copy"); | ||
| if (successful) { | ||
| this.set( | ||
| "model.savingStatus", | ||
| I18n.t("admin.customize.copied_to_clipboard") | ||
| ); | ||
| } else { | ||
| this.set( | ||
| "model.savingStatus", | ||
| I18n.t("admin.customize.copy_to_clipboard_error") | ||
| ); | ||
| } | ||
|
|
||
| later(() => { | ||
| this.set("model.savingStatus", null); | ||
| }, 2000); | ||
|
|
||
| window.getSelection().removeAllRanges(); | ||
|
|
||
| $(".table.colors").show(); | ||
| $(area).remove(); | ||
| }, | ||
|
|
||
| copy() { | ||
| const newColorScheme = this.model.copy(); | ||
| newColorScheme.set( | ||
| "name", | ||
| I18n.t("admin.customize.colors.copy_name_prefix") + | ||
| " " + | ||
| this.get("model.name") | ||
| ); | ||
| newColorScheme.save().then(() => { | ||
| this.allColors.pushObject(newColorScheme); | ||
| this.replaceRoute("adminCustomize.colors.show", newColorScheme); | ||
| }); | ||
| }, | ||
|
|
||
| save: function () { | ||
| this.model.save(); | ||
| }, | ||
|
|
||
| applyUserSelectable() { | ||
| this.model.updateUserSelectable(this.get("model.user_selectable")); | ||
| }, | ||
|
|
||
| destroy: function () { | ||
| const model = this.model; | ||
| return bootbox.confirm( | ||
| I18n.t("admin.customize.colors.delete_confirm"), | ||
| I18n.t("no_value"), | ||
| I18n.t("yes_value"), | ||
| (result) => { | ||
| if (result) { | ||
| model.destroy().then(() => { | ||
| this.allColors.removeObject(model); | ||
| this.replaceRoute("adminCustomize.colors"); | ||
| }); | ||
| } | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,49 @@ | ||
| import Controller from "@ember/controller"; | ||
| import EmberObject from "@ember/object"; | ||
| import I18n from "I18n"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import showModal from "discourse/lib/show-modal"; | ||
|
|
||
| export default Controller.extend({ | ||
| @discourseComputed("model.@each.id") | ||
| baseColorScheme() { | ||
| return this.model.findBy("is_base", true); | ||
| }, | ||
|
|
||
| @discourseComputed("model.@each.id") | ||
| baseColorSchemes() { | ||
| return this.model.filterBy("is_base", true); | ||
| }, | ||
|
|
||
| @discourseComputed("baseColorScheme") | ||
| baseColors(baseColorScheme) { | ||
| const baseColorsHash = EmberObject.create({}); | ||
| baseColorScheme.get("colors").forEach((color) => { | ||
| baseColorsHash.set(color.get("name"), color); | ||
| }); | ||
| return baseColorsHash; | ||
| }, | ||
|
|
||
| actions: { | ||
| newColorSchemeWithBase(baseKey) { | ||
| const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey); | ||
| const newColorScheme = base.copy(); | ||
| newColorScheme.setProperties({ | ||
| name: I18n.t("admin.customize.colors.new_name"), | ||
| base_scheme_id: base.get("base_scheme_id"), | ||
| }); | ||
| newColorScheme.save().then(() => { | ||
| this.model.pushObject(newColorScheme); | ||
| newColorScheme.set("savingStatus", null); | ||
| this.replaceRoute("adminCustomize.colors.show", newColorScheme); | ||
| }); | ||
| }, | ||
|
|
||
| newColorScheme() { | ||
| showModal("admin-color-scheme-select-base", { | ||
| model: this.baseColorSchemes, | ||
| admin: true, | ||
| }); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,36 @@ | ||
| import Controller from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import bootbox from "bootbox"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
|
|
||
| export default Controller.extend({ | ||
| @discourseComputed("model.isSaving") | ||
| saveButtonText(isSaving) { | ||
| return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); | ||
| }, | ||
|
|
||
| @discourseComputed("model.changed", "model.isSaving") | ||
| saveDisabled(changed, isSaving) { | ||
| return !changed || isSaving; | ||
| }, | ||
|
|
||
| actions: { | ||
| save() { | ||
| if (!this.model.saving) { | ||
| this.set("saving", true); | ||
| this.model | ||
| .update(this.model.getProperties("html", "css")) | ||
| .catch((e) => { | ||
| const msg = | ||
| e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors | ||
| ? I18n.t("admin.customize.email_style.save_error_with_reason", { | ||
| error: e.jqXHR.responseJSON.errors.join(". "), | ||
| }) | ||
| : I18n.t("generic_error"); | ||
| bootbox.alert(msg); | ||
| }) | ||
| .finally(() => this.set("model.changed", false)); | ||
| } | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,61 @@ | ||
| import Controller, { inject as controller } from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import { action } from "@ember/object"; | ||
| import bootbox from "bootbox"; | ||
| import { bufferedProperty } from "discourse/mixins/buffered-content"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { popupAjaxError } from "discourse/lib/ajax-error"; | ||
|
|
||
| export default Controller.extend(bufferedProperty("emailTemplate"), { | ||
| adminCustomizeEmailTemplates: controller(), | ||
| emailTemplate: null, | ||
| saved: false, | ||
|
|
||
| @discourseComputed("buffered.body", "buffered.subject") | ||
| saveDisabled(body, subject) { | ||
| return ( | ||
| this.emailTemplate.body === body && this.emailTemplate.subject === subject | ||
| ); | ||
| }, | ||
|
|
||
| @discourseComputed("buffered") | ||
| hasMultipleSubjects(buffered) { | ||
| if (buffered.getProperties("subject")["subject"]) { | ||
| return false; | ||
| } else { | ||
| return buffered.getProperties("id")["id"]; | ||
| } | ||
| }, | ||
|
|
||
| @action | ||
| saveChanges() { | ||
| this.set("saved", false); | ||
| const buffered = this.buffered; | ||
| this.emailTemplate | ||
| .save(buffered.getProperties("subject", "body")) | ||
| .then(() => { | ||
| this.set("saved", true); | ||
| }) | ||
| .catch(popupAjaxError); | ||
| }, | ||
|
|
||
| @action | ||
| revertChanges() { | ||
| this.set("saved", false); | ||
| bootbox.confirm( | ||
| I18n.t("admin.customize.email_templates.revert_confirm"), | ||
| (result) => { | ||
| if (result) { | ||
| this.emailTemplate | ||
| .revert() | ||
| .then((props) => { | ||
| const buffered = this.buffered; | ||
| buffered.setProperties(props); | ||
| this.commitBuffer(); | ||
| }) | ||
| .catch(popupAjaxError); | ||
| } | ||
| } | ||
| ); | ||
| }, | ||
| }); |
| @@ -0,0 +1,18 @@ | ||
| import Controller from "@ember/controller"; | ||
| import { action } from "@ember/object"; | ||
| import { sort } from "@ember/object/computed"; | ||
|
|
||
| export default Controller.extend({ | ||
| sortedTemplates: sort("emailTemplates", "titleSorting"), | ||
|
|
||
| init() { | ||
| this._super(...arguments); | ||
|
|
||
| this.set("titleSorting", ["title"]); | ||
| }, | ||
|
|
||
| @action | ||
| onSelectTemplate(template) { | ||
| this.transitionToRoute("adminCustomizeEmailTemplates.edit", template); | ||
| }, | ||
| }); |
| @@ -0,0 +1,47 @@ | ||
| import Controller from "@ember/controller"; | ||
| import { ajax } from "discourse/lib/ajax"; | ||
| import { bufferedProperty } from "discourse/mixins/buffered-content"; | ||
| import { not } from "@ember/object/computed"; | ||
| import { propertyEqual } from "discourse/lib/computed"; | ||
|
|
||
| export default Controller.extend(bufferedProperty("model"), { | ||
| saved: false, | ||
| isSaving: false, | ||
| saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"), | ||
| resetDisbaled: not("model.overridden"), | ||
|
|
||
| actions: { | ||
| save() { | ||
| this.setProperties({ | ||
| isSaving: true, | ||
| saved: false, | ||
| }); | ||
|
|
||
| ajax("robots.json", { | ||
| type: "PUT", | ||
| data: { robots_txt: this.buffered.get("robots_txt") }, | ||
| }) | ||
| .then((data) => { | ||
| this.commitBuffer(); | ||
| this.set("saved", true); | ||
| this.set("model.overridden", data.overridden); | ||
| }) | ||
| .finally(() => this.set("isSaving", false)); | ||
| }, | ||
|
|
||
| reset() { | ||
| this.setProperties({ | ||
| isSaving: true, | ||
| saved: false, | ||
| }); | ||
| ajax("robots.json", { type: "DELETE" }) | ||
| .then((data) => { | ||
| this.buffered.set("robots_txt", data.robots_txt); | ||
| this.commitBuffer(); | ||
| this.set("saved", true); | ||
| this.set("model.overridden", false); | ||
| }) | ||
| .finally(() => this.set("isSaving", false)); | ||
| }, | ||
| }, | ||
| }); |
| @@ -0,0 +1,72 @@ | ||
| import Controller from "@ember/controller"; | ||
| import I18n from "I18n"; | ||
| import discourseComputed from "discourse-common/utils/decorators"; | ||
| import { url } from "discourse/lib/computed"; | ||
|
|
||
| export default Controller.extend({ | ||
| section: null, | ||
| currentTarget: 0, | ||
| maximized: false, | ||
| previewUrl: url("model.id", "/admin/themes/%@/preview"), | ||
| showAdvanced: false, | ||
| editRouteName: "adminCustomizeThemes.edit", | ||
| showRouteName: "adminCustomizeThemes.show", | ||
|
|
||
| setTargetName: function (name) { | ||
| const target = this.get("model.targets").find((t) => t.name === name); | ||
| this.set("currentTarget", target && target.id); | ||
| }, | ||
|
|
||
| @discourseComputed("currentTarget") | ||
| currentTargetName(id) { | ||
| const target = this.get("model.targets").find( | ||
| (t) => t.id === parseInt(id, 10) | ||
| ); | ||
| return target && target.name; | ||
| }, | ||
|
|
||
| @discourseComputed("model.isSaving") | ||
| saveButtonText(isSaving) { | ||
| return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); | ||
| }, | ||
|
|
||
| @discourseComputed("model.changed", "model.isSaving") | ||
| saveDisabled(changed, isSaving) { | ||
| return !changed || isSaving; | ||
| }, | ||
|
|
||
| actions: { | ||
| save() { | ||
| this.set("saving", true); | ||
| this.model.saveChanges("theme_fields").finally(() => { | ||
| this.set("saving", false); | ||
| }); | ||
| }, | ||
|
|
||
| fieldAdded(target, name) { | ||
| this.replaceRoute(this.editRouteName, this.get("model.id"), target, name); | ||
| }, | ||
|
|
||
| onlyOverriddenChanged(onlyShowOverridden) { | ||
| if (onlyShowOverridden) { | ||
| if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) { | ||
| let firstTarget = this.get("model.targets").find((t) => t.edited); | ||
| let firstField = this.get(`model.fields.${firstTarget.name}`).find( | ||
| (f) => f.edited | ||
| ); | ||
|
|
||
| this.replaceRoute( | ||
| this.editRouteName, | ||
| this.get("model.id"), | ||
| firstTarget.name, | ||
| firstField.name | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| goBack() { | ||
| this.replaceRoute(this.showRouteName, this.model.id); | ||
| }, | ||
| }, | ||
| }); |