diff --git a/app/assets/javascripts/admin/components/permalink-form.js.es6 b/app/assets/javascripts/admin/components/permalink-form.js.es6
new file mode 100644
index 00000000000..1bb29e52eb6
--- /dev/null
+++ b/app/assets/javascripts/admin/components/permalink-form.js.es6
@@ -0,0 +1,56 @@
+export default Ember.Component.extend({
+ classNames: ['permalink-form'],
+ formSubmitted: false,
+ permalinkType: 'topic_id',
+
+ permalinkTypes: function() {
+ 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: 'external_url', name: I18n.t('admin.permalink.external_url')}
+ ];
+ }.property(),
+
+ permalinkTypePlaceholder: function() {
+ return 'admin.permalink.' + this.get('permalinkType');
+ }.property('permalinkType'),
+
+ actions: {
+ submit: function() {
+ if (!this.get('formSubmitted')) {
+ const self = this;
+ self.set('formSubmitted', true);
+ const permalink = Discourse.Permalink.create({url: self.get('url'), permalink_type: self.get('permalinkType'), permalink_type_value: self.get('permalink_type_value')});
+ permalink.save().then(function(result) {
+ self.set('url', '');
+ self.set('permalink_type_value', '');
+ self.set('formSubmitted', false);
+ self.sendAction('action', Discourse.Permalink.create(result.permalink));
+ Em.run.schedule('afterRender', function() { self.$('.permalink-url').focus(); });
+ }, function(e) {
+ self.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, function() { self.$('.permalink-url').focus(); });
+ });
+ }
+ }
+ },
+
+ didInsertElement: function() {
+ var self = this;
+ self._super();
+ Em.run.schedule('afterRender', function() {
+ self.$('.external-url').keydown(function(e) {
+ if (e.keyCode === 13) { // enter key
+ self.send('submit');
+ }
+ });
+ });
+ }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 b/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6
new file mode 100644
index 00000000000..e03e5ebda45
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6
@@ -0,0 +1,36 @@
+export default Ember.ArrayController.extend({
+ loading: false,
+ filter: null,
+
+ show: Discourse.debounce(function() {
+ var self = this;
+ self.set('loading', true);
+ Discourse.Permalink.findAll(self.get("filter")).then(function(result) {
+ self.set('model', result);
+ self.set('loading', false);
+ });
+ }, 250).observes("filter"),
+
+ actions: {
+ recordAdded(arg) {
+ this.get("model").unshiftObject(arg);
+ },
+
+ destroy: function(record) {
+ const self = this;
+ return bootbox.confirm(I18n.t("admin.permalink.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
+ if (result) {
+ record.destroy().then(function(deleted) {
+ if (deleted) {
+ self.removeObject(record);
+ } else {
+ bootbox.alert(I18n.t("generic_error"));
+ }
+ }, function(){
+ bootbox.alert(I18n.t("generic_error"));
+ });
+ }
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/models/permalink.js.es6 b/app/assets/javascripts/admin/models/permalink.js.es6
new file mode 100644
index 00000000000..761cd4ab51f
--- /dev/null
+++ b/app/assets/javascripts/admin/models/permalink.js.es6
@@ -0,0 +1,22 @@
+const Permalink = Discourse.Model.extend({
+ save: function() {
+ return Discourse.ajax("/admin/permalinks.json", {
+ type: 'POST',
+ data: {url: this.get('url'), permalink_type: this.get('permalink_type'), permalink_type_value: this.get('permalink_type_value')}
+ });
+ },
+
+ destroy: function() {
+ return Discourse.ajax("/admin/permalinks/" + this.get('id') + ".json", {type: 'DELETE'});
+ }
+});
+
+Permalink.reopenClass({
+ findAll: function(filter) {
+ return Discourse.ajax("/admin/permalinks.json", { data: { filter: filter } }).then(function(permalinks) {
+ return permalinks.map(p => Discourse.Permalink.create(p));
+ });
+ }
+});
+
+export default Permalink;
diff --git a/app/assets/javascripts/admin/routes/admin-permalinks.js.es6 b/app/assets/javascripts/admin/routes/admin-permalinks.js.es6
new file mode 100644
index 00000000000..72b7f444d2f
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-permalinks.js.es6
@@ -0,0 +1,9 @@
+export default Discourse.Route.extend({
+ model() {
+ return Discourse.Permalink.findAll();
+ },
+
+ setupController(controller, model) {
+ controller.set('model', model);
+ }
+});
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 ab5f2e16c93..daf3b1ced6f 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -22,6 +22,7 @@ export default {
});
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
+ this.resource('adminPermalinks', { path: '/permalinks' });
});
this.route('api');
diff --git a/app/assets/javascripts/admin/templates/components/permalink-form.hbs b/app/assets/javascripts/admin/templates/components/permalink-form.hbs
new file mode 100644
index 00000000000..f5155c27c2e
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/permalink-form.hbs
@@ -0,0 +1,5 @@
+{{i18n 'admin.permalink.form.label'}}
+{{text-field value=url disabled=formSubmitted class="permalink-url" placeholderKey="admin.permalink.url" autocorrect="off" autocapitalize="off"}}
+{{combo-box content=permalinkTypes value=permalinkType}}
+{{text-field value=permalink_type_value disabled=formSubmitted class="external-url" placeholderKey=permalinkTypePlaceholder autocorrect="off" autocapitalize="off"}}
+
diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs
index 1622539bbb3..c009909e1a7 100644
--- a/app/assets/javascripts/admin/templates/customize.hbs
+++ b/app/assets/javascripts/admin/templates/customize.hbs
@@ -4,6 +4,7 @@
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
+ {{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{/admin-nav}}
diff --git a/app/assets/javascripts/admin/templates/permalinks.hbs b/app/assets/javascripts/admin/templates/permalinks.hbs
new file mode 100644
index 00000000000..47717627b6a
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/permalinks.hbs
@@ -0,0 +1,25 @@
+
{{i18n 'admin.permalink.title'}}
+
+ {{text-field value=filter class="url-input" placeholderKey="admin.permalink.form.filter" autocorrect="off" autocapitalize="off"}}
+
+{{permalink-form action="recordAdded"}}
+
+
+{{#conditional-loading-spinner condition=loading}}
+ {{#if model.length}}
+
+
+
{{i18n 'admin.permalink.url'}}
+
{{i18n 'admin.permalink.topic_id'}}
+
{{i18n 'admin.permalink.post_id'}}
+
{{i18n 'admin.permalink.category_id'}}
+
{{i18n 'admin.permalink.external_url'}}
+
+
+
+ {{view 'permalinks-list' content=controller}}
+
+ {{else}}
+ {{i18n 'search.no_results'}}
+ {{/if}}
+{{/conditional-loading-spinner}}
diff --git a/app/assets/javascripts/admin/templates/permalinks_list_item.hbs b/app/assets/javascripts/admin/templates/permalinks_list_item.hbs
new file mode 100644
index 00000000000..4fbf2db55e2
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/permalinks_list_item.hbs
@@ -0,0 +1,7 @@
+
{{url}}
+
{{topic_id}}
+
{{post_id}}
+
{{category_id}}
+
{{external_url}}
+
+
diff --git a/app/assets/javascripts/admin/views/permalinks-list.js.es6 b/app/assets/javascripts/admin/views/permalinks-list.js.es6
new file mode 100644
index 00000000000..c4254376862
--- /dev/null
+++ b/app/assets/javascripts/admin/views/permalinks-list.js.es6
@@ -0,0 +1,8 @@
+import ListView from 'ember-addons/list-view';
+import ListItemView from 'ember-addons/list-item-view';
+
+export default ListView.extend({
+ height: 700,
+ rowHeight: 32,
+ itemViewClass: ListItemView.extend({templateName: "admin/templates/permalinks_list_item"})
+});
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index 1aeabb40106..05891ad3446 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -1199,7 +1199,7 @@ table.api-keys {
position: absolute;
}
-.staff-actions, .screened-emails, .screened-urls, .screened-ip-addresses {
+.staff-actions, .screened-emails, .screened-urls, .screened-ip-addresses, .permalinks {
border-bottom: dotted 1px scale-color($primary, $lightness: 75%);
@@ -1469,3 +1469,19 @@ table#user-badges {
width: 90%;
}
}
+
+// Permalinks
+
+.permalinks {
+ .url, .external_url {
+ width: 300px;
+ }
+ .action, .topic_id, .post_id, .category_id {
+ text-align: center;
+ width: 9.9099%;
+ }
+}
+
+.permalink-title {
+ margin-bottom: 10px;
+}
diff --git a/app/controllers/admin/permalinks_controller.rb b/app/controllers/admin/permalinks_controller.rb
new file mode 100644
index 00000000000..9c2eb47fb2e
--- /dev/null
+++ b/app/controllers/admin/permalinks_controller.rb
@@ -0,0 +1,39 @@
+class Admin::PermalinksController < Admin::AdminController
+
+ before_filter :fetch_permalink, only: [:destroy]
+
+ def index
+ filter = params[:filter]
+
+ permalinks = Permalink
+ permalinks = permalinks.where('url ILIKE :filter OR external_url ILIKE :filter', filter: "%#{params[:filter]}%") if filter.present?
+ permalinks = permalinks.limit(100).order('created_at desc').to_a
+
+ render_serialized(permalinks, PermalinkSerializer)
+ end
+
+ def create
+ params.require(:url)
+ params.require(:permalink_type)
+ params.require(:permalink_type_value)
+
+ permalink = Permalink.new(:url => params[:url], params[:permalink_type] => params[:permalink_type_value])
+ if permalink.save
+ render_serialized(permalink, PermalinkSerializer)
+ else
+ render_json_error(permalink)
+ end
+ end
+
+ def destroy
+ @permalink.destroy
+ render json: success_json
+ end
+
+ private
+
+ def fetch_permalink
+ @permalink = Permalink.find(params[:id])
+ end
+
+end
diff --git a/app/serializers/permalink_serializer.rb b/app/serializers/permalink_serializer.rb
new file mode 100644
index 00000000000..2d621ed72de
--- /dev/null
+++ b/app/serializers/permalink_serializer.rb
@@ -0,0 +1,3 @@
+class PermalinkSerializer < ApplicationSerializer
+ attributes :id, :url, :topic_id, :post_id, :category_id, :external_url
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 52e0ca0250b..4b6020abf9a 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2443,6 +2443,19 @@ en:
image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"
+ permalink:
+ title: "Permalinks"
+ url: "URL"
+ topic_id: "Topic ID"
+ post_id: "Post ID"
+ category_id: "Category ID"
+ external_url: "External URL"
+ delete_confirm: Are you sure you want to delete this permalink?
+ form:
+ label: "New:"
+ add: "Add"
+ filter: "Search (URL or External URL)"
+
lightbox:
download: "download"
diff --git a/config/routes.rb b/config/routes.rb
index 0553fd322bb..f691a8c2ea0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -133,6 +133,7 @@ Discourse::Application.routes.draw do
get "customize" => "color_schemes#index", constraints: AdminConstraint.new
get "customize/css_html" => "site_customizations#index", constraints: AdminConstraint.new
get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new
+ get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new
get "flags" => "flags#index"
get "flags/:filter" => "flags#index"
post "flags/agree/:id" => "flags#agree"
@@ -148,6 +149,8 @@ Discourse::Application.routes.draw do
resources :color_schemes, constraints: AdminConstraint.new
+ resources :permalinks, constraints: AdminConstraint.new
+
get "version_check" => "versions#show"
resources :dashboard, only: [:index] do
diff --git a/spec/controllers/admin/permalinks_controller_spec.rb b/spec/controllers/admin/permalinks_controller_spec.rb
new file mode 100644
index 00000000000..c885363bfd0
--- /dev/null
+++ b/spec/controllers/admin/permalinks_controller_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Admin::PermalinksController do
+
+ it "is a subclass of AdminController" do
+ expect(Admin::PermalinksController < Admin::AdminController).to eq(true)
+ end
+
+ let!(:user) { log_in(:admin) }
+
+ describe 'index' do
+ it 'filters url' do
+ Fabricate(:permalink, url: "/forum/23")
+ Fabricate(:permalink, url: "/forum/98")
+ Fabricate(:permalink, url: "/discuss/topic/45")
+ Fabricate(:permalink, url: "/discuss/topic/76")
+
+ xhr :get, :index, filter: "topic"
+
+ expect(response).to be_success
+ result = JSON.parse(response.body)
+ expect(result.length).to eq(2)
+ end
+
+ it 'filters external url' do
+ Fabricate(:permalink, external_url: "http://google.com")
+ Fabricate(:permalink, external_url: "http://wikipedia.org")
+ Fabricate(:permalink, external_url: "http://www.discourse.org")
+ Fabricate(:permalink, external_url: "http://try.discourse.org")
+
+ xhr :get, :index, filter: "discourse"
+
+ expect(response).to be_success
+ result = JSON.parse(response.body)
+ expect(result.length).to eq(2)
+ end
+
+ it 'filters url and external url both' do
+ Fabricate(:permalink, url: "/forum/23", external_url: "http://google.com")
+ Fabricate(:permalink, url: "/discourse/98", external_url: "http://wikipedia.org")
+ Fabricate(:permalink, url: "/discuss/topic/45", external_url: "http://discourse.org")
+ Fabricate(:permalink, url: "/discuss/topic/76", external_url: "http://try.discourse.org")
+
+ xhr :get, :index, filter: "discourse"
+
+ expect(response).to be_success
+ result = JSON.parse(response.body)
+ expect(result.length).to eq(3)
+ end
+ end
+end