diff --git a/.eslintignore b/.eslintignore
index 87cbecfae70..8e7c2d78fce 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,6 +7,7 @@ app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/defer/html-sanitizer-bundle.js
app/assets/javascripts/ember-addons/
+app/assets/javascripts/admin/lib/autosize.js.es6
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/javascripts/moment.js
diff --git a/app/assets/javascripts/admin/adapters/site-text-type.js.es6 b/app/assets/javascripts/admin/adapters/site-text-type.js.es6
deleted file mode 100644
index b547b06f3ca..00000000000
--- a/app/assets/javascripts/admin/adapters/site-text-type.js.es6
+++ /dev/null
@@ -1,2 +0,0 @@
-import CustomizationBase from 'admin/adapters/customization-base';
-export default CustomizationBase;
diff --git a/app/assets/javascripts/admin/components/expanding-text-area.js.es6 b/app/assets/javascripts/admin/components/expanding-text-area.js.es6
new file mode 100644
index 00000000000..3b5a260690a
--- /dev/null
+++ b/app/assets/javascripts/admin/components/expanding-text-area.js.es6
@@ -0,0 +1,24 @@
+import { on, observes } from 'ember-addons/ember-computed-decorators';
+import autosize from 'admin/lib/autosize';
+
+export default Ember.TextArea.extend({
+ @on('didInsertElement')
+ _startWatching() {
+ Ember.run.scheduleOnce('afterRender', () => {
+ this.$().focus();
+ autosize(this.element);
+ });
+ },
+
+ @observes('value')
+ _updateAutosize() {
+ const evt = document.createEvent('Event');
+ evt.initEvent('autosize:update', true, false);
+ this.element.dispatchEvent(evt);
+ },
+
+ @on('willDestroyElement')
+ _disableAutosize() {
+ autosize.destroy(this.$());
+ }
+});
diff --git a/app/assets/javascripts/admin/components/site-text-summary.js.es6 b/app/assets/javascripts/admin/components/site-text-summary.js.es6
new file mode 100644
index 00000000000..a54e615ef88
--- /dev/null
+++ b/app/assets/javascripts/admin/components/site-text-summary.js.es6
@@ -0,0 +1,24 @@
+import { on } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Component.extend({
+ classNames: ['site-text'],
+
+ @on('didInsertElement')
+ highlightTerm() {
+ const term = this.get('term');
+ if (term) {
+ this.$('.site-text-id, .site-text-value').highlight(term, {className: 'text-highlight'});
+ }
+ this.$('.site-text-value').ellipsis();
+ },
+
+ click() {
+ this.send('edit');
+ },
+
+ actions: {
+ edit() {
+ this.sendAction('editAction', this.get('siteText'));
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6
index 159d26e89b5..79de9efc354 100644
--- a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6
@@ -1,14 +1,29 @@
-export default Ember.Controller.extend({
- saved: false,
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+import { bufferedProperty } from 'discourse/mixins/buffered-content';
- saveDisabled: function() {
- return ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value')));
- }.property('model.iSaving', 'model.value'),
+export default Ember.Controller.extend(bufferedProperty('siteText'), {
+ saved: false,
actions: {
saveChanges() {
- const model = this.get('model');
- model.save(model.getProperties('value')).then(() => this.set('saved', true));
+ const buffered = this.get('buffered');
+ this.get('siteText').save(buffered.getProperties('value')).then(() => {
+ this.commitBuffer();
+ this.set('saved', true);
+ }).catch(popupAjaxError);
+ },
+
+ revertChanges() {
+ this.set('saved', false);
+ bootbox.confirm(I18n.t('admin.site_text.revert_confirm'), result => {
+ if (result) {
+ this.get('siteText').revert().then(props => {
+ const buffered = this.get('buffered');
+ buffered.setProperties(props);
+ this.commitBuffer();
+ }).catch(popupAjaxError);
+ }
+ });
}
}
});
diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6
new file mode 100644
index 00000000000..d5450c0d442
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6
@@ -0,0 +1,40 @@
+import { default as computed } from 'ember-addons/ember-computed-decorators';
+
+export default Ember.Controller.extend({
+ _q: null,
+ searching: false,
+ siteTexts: null,
+ preferred: false,
+
+ queryParams: ['q'],
+
+ @computed
+ q: {
+ set(value) {
+ if (Ember.isEmpty(value)) { value = null; }
+ this._q = value;
+ return value;
+ },
+ get() {
+ return this._q;
+ }
+ },
+
+ _performSearch() {
+ const q = this.get('q');
+ this.store.find('site-text', { q }).then(results => {
+ this.set('siteTexts', results);
+ }).finally(() => this.set('searching', false));
+ },
+
+ actions: {
+ edit(siteText) {
+ this.transitionToRoute('adminSiteText.edit', siteText.get('id'));
+ },
+
+ search() {
+ this.set('searching', true);
+ Ember.run.debounce(this, this._performSearch, 400);
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-site-text.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text.js.es6
deleted file mode 100644
index 24c4c051390..00000000000
--- a/app/assets/javascripts/admin/controllers/admin-site-text.js.es6
+++ /dev/null
@@ -1 +0,0 @@
-export default Ember.ArrayController.extend();
diff --git a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6
new file mode 100644
index 00000000000..aeb9f30b377
--- /dev/null
+++ b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6
@@ -0,0 +1,3 @@
+Em.Handlebars.helper('preserve-newlines', str => {
+ return new Handlebars.SafeString(Discourse.Utilities.escapeExpression(str).replace(/\n/g, "
"));
+});
diff --git a/app/assets/javascripts/admin/lib/autosize.js.es6 b/app/assets/javascripts/admin/lib/autosize.js.es6
new file mode 100644
index 00000000000..dbdb858093c
--- /dev/null
+++ b/app/assets/javascripts/admin/lib/autosize.js.es6
@@ -0,0 +1,200 @@
+const set = (typeof Set === "function") ? new Set() : (function () {
+ const list = [];
+
+ return {
+ has(key) {
+ return Boolean(list.indexOf(key) > -1);
+ },
+ add(key) {
+ list.push(key);
+ },
+ delete(key) {
+ list.splice(list.indexOf(key), 1);
+ },
+ };
+})();
+
+function assign(ta, {setOverflowX = true, setOverflowY = true} = {}) {
+ if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
+
+ let heightOffset = null;
+ let overflowY = null;
+ let clientWidth = ta.clientWidth;
+
+ function init() {
+ const style = window.getComputedStyle(ta, null);
+
+ overflowY = style.overflowY;
+
+ if (style.resize === 'vertical') {
+ ta.style.resize = 'none';
+ } else if (style.resize === 'both') {
+ ta.style.resize = 'horizontal';
+ }
+
+ if (style.boxSizing === 'content-box') {
+ heightOffset = -(parseFloat(style.paddingTop)+parseFloat(style.paddingBottom));
+ } else {
+ heightOffset = parseFloat(style.borderTopWidth)+parseFloat(style.borderBottomWidth);
+ }
+ // Fix when a textarea is not on document body and heightOffset is Not a Number
+ if (isNaN(heightOffset)) {
+ heightOffset = 0;
+ }
+
+ update();
+ }
+
+ function changeOverflow(value) {
+ {
+ // Chrome/Safari-specific fix:
+ // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+ // made available by removing the scrollbar. The following forces the necessary text reflow.
+ const width = ta.style.width;
+ ta.style.width = '0px';
+ // Force reflow:
+ /* jshint ignore:start */
+ ta.offsetWidth;
+ /* jshint ignore:end */
+ ta.style.width = width;
+ }
+
+ overflowY = value;
+
+ if (setOverflowY) {
+ ta.style.overflowY = value;
+ }
+
+ resize();
+ }
+
+ function resize() {
+ const htmlTop = window.pageYOffset;
+ const bodyTop = document.body.scrollTop;
+ const originalHeight = ta.style.height;
+
+ ta.style.height = 'auto';
+
+ let endHeight = ta.scrollHeight+heightOffset;
+
+ if (ta.scrollHeight === 0) {
+ // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+ ta.style.height = originalHeight;
+ return;
+ }
+
+ ta.style.height = endHeight+'px';
+
+ // used to check if an update is actually necessary on window.resize
+ clientWidth = ta.clientWidth;
+
+ // prevents scroll-position jumping
+ document.documentElement.scrollTop = htmlTop;
+ document.body.scrollTop = bodyTop;
+ }
+
+ function update() {
+ const startHeight = ta.style.height;
+
+ resize();
+
+ const style = window.getComputedStyle(ta, null);
+
+ if (style.height !== ta.style.height) {
+ if (overflowY !== 'visible') {
+ changeOverflow('visible');
+ }
+ } else {
+ if (overflowY !== 'hidden') {
+ changeOverflow('hidden');
+ }
+ }
+
+ if (startHeight !== ta.style.height) {
+ const evt = document.createEvent('Event');
+ evt.initEvent('autosize:resized', true, false);
+ ta.dispatchEvent(evt);
+ }
+ }
+
+ const pageResize = () => {
+ if (ta.clientWidth !== clientWidth) {
+ update();
+ }
+ };
+
+ const destroy = style => {
+ window.removeEventListener('resize', pageResize, false);
+ ta.removeEventListener('input', update, false);
+ ta.removeEventListener('keyup', update, false);
+ ta.removeEventListener('autosize:destroy', destroy, false);
+ ta.removeEventListener('autosize:update', update, false);
+ set.delete(ta);
+
+ Object.keys(style).forEach(key => {
+ ta.style[key] = style[key];
+ });
+ }.bind(ta, {
+ height: ta.style.height,
+ resize: ta.style.resize,
+ overflowY: ta.style.overflowY,
+ overflowX: ta.style.overflowX,
+ wordWrap: ta.style.wordWrap,
+ });
+
+ ta.addEventListener('autosize:destroy', destroy, false);
+
+ // IE9 does not fire onpropertychange or oninput for deletions,
+ // so binding to onkeyup to catch most of those events.
+ // There is no way that I know of to detect something like 'cut' in IE9.
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ ta.addEventListener('keyup', update, false);
+ }
+
+ window.addEventListener('resize', pageResize, false);
+ ta.addEventListener('input', update, false);
+ ta.addEventListener('autosize:update', update, false);
+ set.add(ta);
+
+ if (setOverflowX) {
+ ta.style.overflowX = 'hidden';
+ ta.style.wordWrap = 'break-word';
+ }
+
+ init();
+}
+
+function exportDestroy(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ const evt = document.createEvent('Event');
+ evt.initEvent('autosize:destroy', true, false);
+ ta.dispatchEvent(evt);
+}
+
+function exportUpdate(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ const evt = document.createEvent('Event');
+ evt.initEvent('autosize:update', true, false);
+ ta.dispatchEvent(evt);
+}
+
+let autosize = (el, options) => {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], x => assign(x, options));
+ }
+ return el;
+};
+autosize.destroy = el => {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], exportDestroy);
+ }
+ return el;
+};
+autosize.update = el => {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], exportUpdate);
+ }
+ return el;
+};
+
+export default autosize;
diff --git a/app/assets/javascripts/admin/models/site-text-type.js.es6 b/app/assets/javascripts/admin/models/site-text-type.js.es6
deleted file mode 100644
index 7cb1171e909..00000000000
--- a/app/assets/javascripts/admin/models/site-text-type.js.es6
+++ /dev/null
@@ -1,2 +0,0 @@
-import RestModel from 'discourse/models/rest';
-export default RestModel.extend();
diff --git a/app/assets/javascripts/admin/models/site-text.js.es6 b/app/assets/javascripts/admin/models/site-text.js.es6
index edbaf2a4447..0d18ad7ea82 100644
--- a/app/assets/javascripts/admin/models/site-text.js.es6
+++ b/app/assets/javascripts/admin/models/site-text.js.es6
@@ -1,8 +1,10 @@
import RestModel from 'discourse/models/rest';
+const { getProperties } = Ember;
export default RestModel.extend({
- markdown: Em.computed.equal('format', 'markdown'),
- plainText: Em.computed.equal('format', 'plain'),
- html: Em.computed.equal('format', 'html'),
- css: Em.computed.equal('format', 'css'),
+ revert() {
+ return Discourse.ajax(`/admin/customize/site_texts/${this.get('id')}`, {
+ method: 'DELETE'
+ }).then(result => getProperties(result.site_text, 'value', 'can_revert'));
+ }
});
diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
index b7c7144187c..406a0011706 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -22,8 +22,9 @@ export default {
});
this.resource('adminSiteText', { path: '/site_texts' }, function() {
- this.route('edit', {path: '/:text_type'});
+ this.route('edit', { path: '/:id' });
});
+
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
this.resource('adminPermalinks', { path: '/permalinks' });
diff --git a/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6
index 847746d0396..2774f0aec9e 100644
--- a/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6
@@ -1,5 +1,9 @@
-export default Discourse.Route.extend({
+export default Ember.Route.extend({
model(params) {
- return this.store.find('site-text', params.text_type);
+ return this.store.find('site-text', params.id);
+ },
+
+ setupController(controller, siteText) {
+ controller.setProperties({ siteText, saved: false });
}
});
diff --git a/app/assets/javascripts/admin/routes/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text-index.js.es6
new file mode 100644
index 00000000000..e312d389dfc
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-site-text-index.js.es6
@@ -0,0 +1,13 @@
+export default Ember.Route.extend({
+ queryParams: {
+ q: { replace: true }
+ },
+
+ model(params) {
+ return this.store.find('site-text', { q: params.q });
+ },
+
+ setupController(controller, model) {
+ controller.set('siteTexts', model);
+ }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-site-text.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text.js.es6
deleted file mode 100644
index f69c3c3954b..00000000000
--- a/app/assets/javascripts/admin/routes/admin-site-text.js.es6
+++ /dev/null
@@ -1,5 +0,0 @@
-export default Discourse.Route.extend({
- model() {
- return this.store.findAll('site-text-type');
- }
-});
diff --git a/app/assets/javascripts/admin/templates/components/save-controls.hbs b/app/assets/javascripts/admin/templates/components/save-controls.hbs
index e8e13cb9c05..00ac91331fa 100644
--- a/app/assets/javascripts/admin/templates/components/save-controls.hbs
+++ b/app/assets/javascripts/admin/templates/components/save-controls.hbs
@@ -1,5 +1,7 @@
-{{d-button action="saveChanges" disabled=buttonDisabled label=savingText class="btn-primary"}}
+{{d-button action="saveChanges" disabled=buttonDisabled label=savingText class="btn-primary save-changes"}}
{{yield}}
- {{#if saved}}{{i18n 'saved'}}{{/if}}
+ {{#if saved}}
+
{{i18n 'saved'}}
+ {{/if}}
diff --git a/app/assets/javascripts/admin/templates/components/site-text-summary.hbs b/app/assets/javascripts/admin/templates/components/site-text-summary.hbs
new file mode 100644
index 00000000000..bf5ee8c7f03
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/site-text-summary.hbs
@@ -0,0 +1,5 @@
+{{d-button label="admin.site_text.edit" class='edit' action="edit"}}
+{{siteText.id}}
+{{siteText.value}}
+
+
diff --git a/app/assets/javascripts/admin/templates/site-text-edit.hbs b/app/assets/javascripts/admin/templates/site-text-edit.hbs
index 5ff141d0c93..5e02eb07316 100644
--- a/app/assets/javascripts/admin/templates/site-text-edit.hbs
+++ b/app/assets/javascripts/admin/templates/site-text-edit.hbs
@@ -1,17 +1,20 @@
-{{model.title}}
-{{model.description}}
+
-{{#if model.markdown}}
- {{d-editor value=model.value}}
-{{/if}}
-{{#if model.plainText}}
- {{textarea value=model.value class="plain"}}
-{{/if}}
-{{#if model.html}}
- {{ace-editor content=model.value mode="html"}}
-{{/if}}
-{{#if model.css}}
- {{ace-editor content=model.value mode="css"}}
-{{/if}}
+
+
{{siteText.id}}
+
-{{save-controls model=model action="saveChanges" saveDisabled=saveDisabled saved=saved}}
+ {{expanding-text-area value=buffered.value rows="1" class="site-text-value"}}
+
+ {{#save-controls model=siteText action="saveChanges" saved=saved}}
+ {{#if siteText.can_revert}}
+ {{d-button action="revertChanges" label="admin.site_text.revert" class="revert-site-text"}}
+ {{/if}}
+ {{/save-controls}}
+
+ {{#link-to 'adminSiteText.index' class="go-back"}}
+ {{fa-icon 'arrow-left'}}
+ {{i18n 'admin.site_text.go_back'}}
+ {{/link-to}}
+
+
diff --git a/app/assets/javascripts/admin/templates/site-text-index.hbs b/app/assets/javascripts/admin/templates/site-text-index.hbs
index 5a448def37a..85b007dcc97 100644
--- a/app/assets/javascripts/admin/templates/site-text-index.hbs
+++ b/app/assets/javascripts/admin/templates/site-text-index.hbs
@@ -1 +1,19 @@
-{{i18n 'admin.site_text.none'}}
+
+
{{i18n "admin.site_text.description"}}
+
+ {{text-field value=q
+ placeholderKey="admin.site_text.search"
+ class="no-blur site-text-search"
+ autofocus="true"
+ keyUpAction="search"}}
+
+
+{{#conditional-loading-spinner condition=searching}}
+ {{#unless siteTexts.findArgs.q}}
+ {{i18n "admin.site_text.recommended"}}
+ {{/unless}}
+
+ {{#each siteTexts as |siteText|}}
+ {{site-text-summary siteText=siteText editAction="edit" term=q}}
+ {{/each}}
+{{/conditional-loading-spinner}}
diff --git a/app/assets/javascripts/admin/templates/site-text.hbs b/app/assets/javascripts/admin/templates/site-text.hbs
index 25924f99e97..e12a542d794 100644
--- a/app/assets/javascripts/admin/templates/site-text.hbs
+++ b/app/assets/javascripts/admin/templates/site-text.hbs
@@ -1,15 +1,3 @@
-
-
-
- {{#each c in model}}
- -
- {{#link-to 'adminSiteText.edit' c.text_type}}{{c.title}}{{/link-to}}
-
- {{/each}}
-
-
-
-
- {{outlet}}
-
+
+ {{outlet}}
diff --git a/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6 b/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6
deleted file mode 100644
index dd1ccd9871b..00000000000
--- a/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-import { on } from "ember-addons/ember-computed-decorators";
-
-export default Ember.TextField.extend({
-
- @on("didInsertElement")
- becomeFocused() {
- const input = this.get("element");
- input.focus();
- input.selectionStart = input.selectionEnd = input.value.length;
- }
-
-});
diff --git a/app/assets/javascripts/discourse/components/text-field.js.es6 b/app/assets/javascripts/discourse/components/text-field.js.es6
index a9246efa7db..cf572dd121c 100644
--- a/app/assets/javascripts/discourse/components/text-field.js.es6
+++ b/app/assets/javascripts/discourse/components/text-field.js.es6
@@ -6,5 +6,12 @@ export default Ember.TextField.extend({
@computed("placeholderKey")
placeholder(placeholderKey) {
return placeholderKey ? I18n.t(placeholderKey) : "";
+ },
+
+ keyUp() {
+ const act = this.get('keyUpAction');
+ if (act) {
+ this.sendAction('keyUpAction');
+ }
}
});
diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6
index f59a680b0d9..0ad607d3f4a 100644
--- a/app/assets/javascripts/discourse/initializers/localization.js.es6
+++ b/app/assets/javascripts/discourse/initializers/localization.js.es6
@@ -12,6 +12,7 @@ export default {
const overrides = PreloadStore.get('translationOverrides') || {};
Object.keys(overrides).forEach(k => {
const v = overrides[k];
+ k = k.replace('admin_js', 'js');
const segs = k.split('.');
let node = I18n.translations[I18n.locale];
diff --git a/app/assets/javascripts/discourse/models/result-set.js.es6 b/app/assets/javascripts/discourse/models/result-set.js.es6
index 754fdadd200..6d1b558a572 100644
--- a/app/assets/javascripts/discourse/models/result-set.js.es6
+++ b/app/assets/javascripts/discourse/models/result-set.js.es6
@@ -4,6 +4,13 @@ export default Ember.ArrayProxy.extend({
totalRows: 0,
refreshing: false,
+ content: null,
+ loadMoreUrl: null,
+ refreshUrl: null,
+ findArgs: null,
+ store: null,
+ __type: null,
+
canLoadMore: function() {
return this.get('length') < this.get('totalRows');
}.property('totalRows', 'length'),
diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6
index 57d60b0bbc3..b29b1033065 100644
--- a/app/assets/javascripts/discourse/models/store.js.es6
+++ b/app/assets/javascripts/discourse/models/store.js.es6
@@ -63,7 +63,7 @@ export default Ember.Object.extend({
_hydrateFindResults(result, type, findArgs) {
if (typeof findArgs === "object") {
- return this._resultSet(type, result);
+ return this._resultSet(type, result, findArgs);
} else {
return this._hydrate(type, result[Ember.String.underscore(type)], result);
}
@@ -81,7 +81,7 @@ export default Ember.Object.extend({
},
find(type, findArgs, opts) {
- return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => {
+ return this.adapterFor(type).find(this, type, findArgs, opts).then(result => {
return this._hydrateFindResults(result, type, findArgs, opts);
});
},
@@ -142,14 +142,14 @@ export default Ember.Object.extend({
});
},
- _resultSet(type, result) {
+ _resultSet(type, result, findArgs) {
const typeName = Ember.String.underscore(this.pluralize(type)),
content = result[typeName].map(obj => this._hydrate(type, obj, result)),
totalRows = result["total_rows_" + typeName] || content.length,
loadMoreUrl = result["load_more_" + typeName],
refreshUrl = result['refresh_' + typeName];
- return ResultSet.create({ content, totalRows, loadMoreUrl, refreshUrl, store: this, __type: type });
+ return ResultSet.create({ content, totalRows, loadMoreUrl, refreshUrl, findArgs, store: this, __type: type });
},
_build(type, obj) {
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index 26f5d7ad218..f26ff34e2a0 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -16,7 +16,8 @@
{{#if model.isPrivateMessage}}
{{fa-icon "envelope"}}
{{/if}}
- {{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
+
+ {{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
{{#if showCategoryChooser}}
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
diff --git a/app/assets/javascripts/main_include_admin.js b/app/assets/javascripts/main_include_admin.js
index 7816da8382a..1217131b4fd 100644
--- a/app/assets/javascripts/main_include_admin.js
+++ b/app/assets/javascripts/main_include_admin.js
@@ -9,6 +9,7 @@
//= require admin/routes/admin-email-logs
//= require admin/controllers/admin-email-skipped
//= require discourse/lib/export-result
+//= require_tree ./admin/lib
//= require_tree ./admin
//= require resumable.js
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index 8a3bdd297e7..9bec8c8c3a8 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -43,6 +43,59 @@ td.flaggers td {
border-top: none;
}
+.site-texts {
+ .search-area {
+ margin-bottom: 2em;
+ p {
+ margin-top: 0;
+ }
+
+ input {
+ padding: 0.5em;
+ font-size: 1em;
+ width: 50%;
+ }
+ }
+ .text-highlight {
+ font-weight: bold;
+ }
+
+ .site-text {
+ cursor: pointer;
+ border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
+ margin-bottom: 0.5em;
+
+ h3 {
+ font-weight: normal;
+ font-size: 1.1em;
+ }
+
+ button.edit {
+ float: right;
+ }
+ .site-text-value {
+ margin: 0.5em 5em 0.5em 0;
+ max-height: 100px;
+ color: dark-light-diff($primary, $secondary, 40%, -10%);
+ }
+
+ }
+
+ .edit-site-text {
+ textarea {
+ width: 80%;
+ }
+
+ .save-messages, .title {
+ margin-bottom: 1em;
+ }
+
+ .go-back {
+ margin-top: 1em;
+ }
+ }
+}
+
.content-list li a span.count {
font-size: 0.857em;
float: right;
diff --git a/app/controllers/admin/site_text_types_controller.rb b/app/controllers/admin/site_text_types_controller.rb
deleted file mode 100644
index f13e1ce704e..00000000000
--- a/app/controllers/admin/site_text_types_controller.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Admin::SiteTextTypesController < Admin::AdminController
-
- def index
- render_serialized(SiteText.text_types, SiteTextTypeSerializer, root: 'site_text_types')
- end
-
-end
diff --git a/app/controllers/admin/site_texts_controller.rb b/app/controllers/admin/site_texts_controller.rb
index 416d9901861..0a70519f25b 100644
--- a/app/controllers/admin/site_texts_controller.rb
+++ b/app/controllers/admin/site_texts_controller.rb
@@ -1,21 +1,54 @@
class Admin::SiteTextsController < Admin::AdminController
+ def self.preferred_keys
+ ['system_messages.usage_tips.text_body_template',
+ 'education.new-topic',
+ 'education.new-reply',
+ 'login_required.welcome_message']
+ end
+
+ def index
+ if params[:q].blank?
+ results = self.class.preferred_keys.map {|k| {id: k, value: I18n.t(k) }}
+ else
+ results = []
+ translations = I18n.search(params[:q])
+ translations.each do |k, v|
+ results << {id: k, value: v}
+ end
+ results.sort! do |x, y|
+ (x[:id].size + x[:value].size) <=> (y[:id].size + y[:value].size)
+ end
+ end
+
+ render_serialized(results[0..50], SiteTextSerializer, root: 'site_texts', rest_serializer: true)
+ end
+
def show
- site_text = SiteText.find_or_new(params[:id].to_s)
- render_serialized(site_text, SiteTextSerializer, root: 'site_text')
+ site_text = find_site_text
+ render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
end
def update
- site_text = SiteText.find_or_new(params[:id].to_s)
+ site_text = find_site_text
+ site_text[:value] = params[:site_text][:value]
- # Updating to nothing is the same as removing it
- if params[:site_text][:value].present?
- site_text.value = params[:site_text][:value]
- site_text.save!
- else
- site_text.destroy
+ TranslationOverride.upsert!(I18n.locale, site_text[:id], site_text[:value])
+ render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
+ end
+
+ def revert
+ site_text = find_site_text
+ TranslationOverride.revert!(I18n.locale, site_text[:id])
+ site_text = find_site_text
+ render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
+ end
+
+ protected
+
+ def find_site_text
+ raise Discourse::NotFound unless I18n.exists?(params[:id])
+ {id: params[:id], value: I18n.t(params[:id]) }
end
- render_serialized(site_text, SiteTextSerializer, root: 'site_text')
- end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 022141a73ab..9a12e5dbb0d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -149,18 +149,6 @@ module ApplicationHelper
result.join("\n")
end
- # Look up site content for a key. If the key is blank, you can supply a block and that
- # will be rendered instead.
- def markdown_content(key, replacements=nil)
- result = PrettyText.cook(SiteText.text_for(key, replacements || {})).html_safe
- if result.blank? && block_given?
- yield
- nil
- else
- result
- end
- end
-
def application_logo_url
@application_logo_url ||= (mobile_view? && SiteSetting.mobile_logo_url) || SiteSetting.logo_url
end
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index aa2aa049292..4b7b58afa52 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -18,7 +18,7 @@ class UserNotifications < ActionMailer::Base
build_email(user.email,
template: 'user_notifications.signup_after_approval',
email_token: opts[:email_token],
- new_user_tips: SiteText.text_for(:usage_tips, base_url: Discourse.base_url))
+ new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url))
end
def authorize_email(user, opts={})
diff --git a/app/models/site_text.rb b/app/models/site_text.rb
deleted file mode 100644
index 82a0f37407b..00000000000
--- a/app/models/site_text.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require_dependency 'site_text_type'
-require_dependency 'site_text_class_methods'
-require_dependency 'distributed_cache'
-
-class SiteText < ActiveRecord::Base
- extend SiteTextClassMethods
-
- self.primary_key = 'text_type'
-
- validates_presence_of :value
-
- after_save do
- SiteText.text_for_cache.clear
- end
-
- after_destroy do
- SiteText.text_for_cache.clear
- end
-
- def self.formats
- @formats ||= Enum.new(:plain, :markdown, :html, :css)
- end
-
- add_text_type :usage_tips, default_18n_key: 'system_messages.usage_tips.text_body_template'
- add_text_type :education_new_topic, default_18n_key: 'education.new-topic'
- add_text_type :education_new_reply, default_18n_key: 'education.new-reply'
- add_text_type :login_required_welcome_message, default_18n_key: 'login_required.welcome_message'
-
- def site_text_type
- @site_text_type ||= SiteText.find_text_type(text_type)
- end
-
-end
-
-# == Schema Information
-#
-# Table name: site_texts
-#
-# text_type :string(255) not null, primary key
-# value :text not null
-# created_at :datetime not null
-# updated_at :datetime not null
-#
-# Indexes
-#
-# index_site_texts_on_text_type (text_type) UNIQUE
-#
diff --git a/app/models/site_text_type.rb b/app/models/site_text_type.rb
deleted file mode 100644
index 3b505e870cf..00000000000
--- a/app/models/site_text_type.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-class SiteTextType
-
- attr_accessor :text_type, :format
-
- def initialize(text_type, format, opts=nil)
- @opts = opts || {}
- @text_type = text_type
- @format = format
- end
-
- def title
- I18n.t("content_types.#{text_type}.title")
- end
-
- def description
- I18n.t("content_types.#{text_type}.description")
- end
-
- def allow_blank?
- !!@opts[:allow_blank]
- end
-
- def default_text
- @opts[:default_18n_key].present? ? I18n.t(@opts[:default_18n_key]) : ""
- end
-
-end
diff --git a/app/serializers/site_text_serializer.rb b/app/serializers/site_text_serializer.rb
index fb8e7920318..0bd13d7520a 100644
--- a/app/serializers/site_text_serializer.rb
+++ b/app/serializers/site_text_serializer.rb
@@ -1,39 +1,20 @@
class SiteTextSerializer < ApplicationSerializer
-
- attributes :id,
- :text_type,
- :title,
- :description,
- :value,
- :format,
- :allow_blank?
+ attributes :id, :value, :can_revert?
def id
- text_type
- end
-
- def title
- object.site_text_type.title
- end
-
- def text_type
- object.text_type
- end
-
- def description
- object.site_text_type.description
- end
-
- def format
- object.site_text_type.format
+ object[:id]
end
def value
- return object.value if object.value.present?
- object.site_text_type.default_text
+ object[:value]
end
- def allow_blank?
- object.site_text_type.allow_blank?
+ def can_revert?
+ current_val = value
+
+ I18n.overrides_disabled do
+ return I18n.t(object[:id]) != current_val
+ end
end
end
+
diff --git a/app/serializers/site_text_type_serializer.rb b/app/serializers/site_text_type_serializer.rb
deleted file mode 100644
index 9c6f5f4164a..00000000000
--- a/app/serializers/site_text_type_serializer.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-class SiteTextTypeSerializer < ApplicationSerializer
-
- attributes :id, :text_type, :title
-
- def id
- text_type
- end
-
- def text_type
- object.text_type
- end
-
- def title
- object.title
- end
-
-end
diff --git a/app/views/static/login.html.erb b/app/views/static/login.html.erb
index 6e840e6e9ab..c484d05e31b 100644
--- a/app/views/static/login.html.erb
+++ b/app/views/static/login.html.erb
@@ -1,3 +1,3 @@
<% if SiteSetting.login_required %>
- <%= markdown_content(:login_required_welcome_message) %>
+ <%= PrettyText.cook(I18n.t('login_required.welcome_message')).html_safe %>
<% end %>
diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb
index 5d1ecc6b006..37a0dd050cc 100644
--- a/config/initializers/i18n.rb
+++ b/config/initializers/i18n.rb
@@ -3,6 +3,8 @@
require 'i18n/backend/discourse_i18n'
I18n.backend = I18n::Backend::DiscourseI18n.new
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
-I18n.reload!
+I18n.init_accelerator!
-MessageBus.subscribe("/i18n-flush") { I18n.reload! }
+unless Rails.env.test?
+ MessageBus.subscribe("/i18n-flush") { I18n.reload! }
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 0309d2a9c59..ad978826557 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2482,8 +2482,14 @@ en:
dropdown: "Dropdown"
site_text:
- none: "Choose a type of content to begin editing."
+ description: "You can customize any of the text on your forum. Please start by searching below:"
+ search: "Search for the text you'd like to edit"
title: 'Text Content'
+ edit: 'edit'
+ revert: "Revert Changes"
+ revert_confirm: "Are you sure you want to revert your changes?"
+ go_back: "Back to Search"
+ recommended: "We recommend customizing the following text to suit your needs:"
site_settings:
show_overriden: 'Only show overridden'
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 8fec99b07a9..045f96c8639 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -748,38 +748,6 @@ en:
notification_email_warning: "Notification emails are not being sent from a valid email address on your domain; email delivery will be erratic and unreliable. Please set notification_email to a valid local email address in
Site Settings."
subfolder_ends_in_slash: "Your subfolder setup is incorrect; the DISCOURSE_RELATIVE_URL_ROOT ends in a slash."
- content_types:
- education_new_reply:
- title: "New User Education: First Replies"
- description: "Pop up just-in-time guidance automatically displayed above the composer when new users begin typing their first two new replies."
- education_new_topic:
- title: "New User Education: First Topics"
- description: "Pop up just-in-time guidance automatically displayed above the composer when new users begin typing their first two new topics."
- usage_tips:
- title: "New User Guidance"
- description: "Guidance and essential information for new users."
- welcome_user:
- title: "Welcome: New User"
- description: "A message automatically sent to all new users when they sign up."
- welcome_invite:
- title: "Welcome: Invited User"
- description: "A message automatically sent to all new invited users when they accept the invitation from another user to participate."
- login_required_welcome_message:
- title: "Login Required: Welcome Message"
- description: "Welcome message that is displayed to logged out users when the 'login required' setting is enabled."
- login_required:
- title: "Login Required: Homepage"
- description: "The text displayed for unauthorized users when login is required on the site."
- head:
- title: "HTML head"
- description: "HTML that will be inserted inside the tags."
- top:
- title: "Top of the pages"
- description: "HTML that will be added at the top of every page (after the header, before the navigation or the topic title)."
- bottom:
- title: "Bottom of the pages"
- description: "HTML that will be added before the