diff --git a/assets/javascripts/discourse/components/realtime-validation-settings.js.es6 b/assets/javascripts/discourse/components/realtime-validation-settings.js.es6
index 33796cf3..2fd306e6 100644
--- a/assets/javascripts/discourse/components/realtime-validation-settings.js.es6
+++ b/assets/javascripts/discourse/components/realtime-validation-settings.js.es6
@@ -1,4 +1,7 @@
import Component from "@ember/component";
+import EmberObject from "@ember/object";
+import { cloneJSON } from "discourse-common/lib/object";
+import Category from "discourse/models/category";
export default Component.extend({
init(){
@@ -13,5 +16,20 @@ export default Component.extend({
this.set('field.validations', EmberObject.create(validations));
}
+
+ const validationBuffer = cloneJSON(this.get('field.validations'));
+ let bufferCategories;
+ if( validationBuffer.similar_topics && (bufferCategories = validationBuffer.similar_topics.categories)) {
+ const categories = Category.findByIds(bufferCategories);
+ validationBuffer.similar_topics.categories = categories;
+ }
+ this.set('validationBuffer', validationBuffer);
+ },
+
+ actions: {
+ updateValidationCategories(name, validation, categories) {
+ this.set(`validationBuffer.${name}.categories`, categories);
+ this.set(`field.validations.${name}.categories`, categories.map(category => category.id));
+ }
}
});
diff --git a/assets/javascripts/discourse/templates/components/realtime-validation-settings.hbs b/assets/javascripts/discourse/templates/components/realtime-validation-settings.hbs
index 487c4de6..c8d35aa4 100644
--- a/assets/javascripts/discourse/templates/components/realtime-validation-settings.hbs
+++ b/assets/javascripts/discourse/templates/components/realtime-validation-settings.hbs
@@ -4,8 +4,25 @@
{{#each-in field.validations as |name props|}}
{{input type="checkbox" checked=props.status }}
- {{i18n (concat 'admin.wizard.field.validations.' name)}}
+ {{i18n (concat 'admin.wizard.field.validations.' name) }}
+
+
{{i18n 'admin.wizard.field.validations.categories'}}
+
+ {{category-selector
+ categories=(get this (concat 'validationBuffer.' name '.categories'))
+ onChange=(action 'updateValidationCategories' name props) }}
+
+
+
+
{{i18n 'admin.wizard.field.validations.date_after'}}
+
+ {{date-picker-past
+ value=(readonly props.date_after)
+ containerId="date-container"
+ onSelect=(action (mut props.date_after))}}
+
+
{{radio-button name=(concat name field.id) value="above" selection=props.position}} {{i18n 'admin.wizard.field.validations.above'}}
diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js
index 5d18328f..dd6bbf0c 100644
--- a/assets/javascripts/wizard-custom.js
+++ b/assets/javascripts/wizard-custom.js
@@ -144,3 +144,29 @@
//= require_tree ./wizard/models
//= require_tree ./wizard/routes
//= require_tree ./wizard/templates
+
+//= require discourse/app/components/mount-widget
+//= require discourse/app/widgets/widget
+//= require discourse/app/widgets/hooks
+//= require discourse/app/widgets/decorator-helper
+//= require discourse/app/widgets/connector
+//= require discourse/app/widgets/post-cooked
+//= require discourse/app/lib/highlight-html
+//= require discourse/app/lib/highlight-search
+//= require discourse/app/lib/constants
+//= require discourse/app/lib/click-track
+//= require discourse/app/helpers/loading-spinner
+//= require discourse/app/widgets/raw-html
+//= require discourse/app/lib/dirty-keys
+
+//= require discourse/app/widgets/search-menu
+//= require discourse/app/widgets/search-menu-results
+//= require discourse/app/widgets/post
+//= require discourse/app/helpers/node
+//= require discourse/app/widgets/post-stream
+
+//= require discourse/app/lib/posts-with-placeholders
+//= require discourse/app/lib/transform-post
+//= require discourse/app/helpers/category-link
+//= require discourse/app/lib/render-tags
+//= require discourse/app/helpers/topic-status-icons
\ No newline at end of file
diff --git a/assets/javascripts/wizard/components/suggested-validator.js.es6 b/assets/javascripts/wizard/components/suggested-validator.js.es6
index 953acfa1..599723e3 100644
--- a/assets/javascripts/wizard/components/suggested-validator.js.es6
+++ b/assets/javascripts/wizard/components/suggested-validator.js.es6
@@ -1,20 +1,53 @@
import WizardFieldValidator from "../../wizard/components/validator";
-import { ajax } from "discourse/lib/ajax";
-import { getToken } from "wizard/lib/ajax";
-import { getOwner } from "discourse-common/lib/get-owner";
-import discourseComputed from "discourse-common/utils/decorators";
+import { deepMerge } from "discourse-common/lib/object";
+import { observes } from "discourse-common/utils/decorators";
+import { cancel, later } from "@ember/runloop";
+import { A } from '@ember/array';
+import EmberObject from "@ember/object";
export default WizardFieldValidator.extend({
validMessageKey: 'hello',
invalidMessageKey: 'world',
+ similarTopics: [],
+
validate() {
- this.backendValidate({title: this.get("field.value")}).then(response => {
- console.log(response)
- })
+ },
+ @observes("field.value")
+ customValidate(){
+ const lastKeyUp = new Date();
+ this._lastKeyUp = lastKeyUp;
+
+ // One second from now, check to see if the last key was hit when
+ // we recorded it. If it was, the user paused typing.
+ cancel(this._lastKeyTimeout);
+ this._lastKeyTimeout = later(() => {
+ if (lastKeyUp !== this._lastKeyUp) {
+ return;
+ }
+
+ this.updateSimilarTopics();
+ }, 1000);
},
+ updateSimilarTopics(){
+ this.backendValidate({
+ title: this.get("field.value"),
+ categories: this.get('validation.categories'),
+ date_after: this.get('validation.date_after')
+ }).then((result) => {
+ const similarTopics = A(deepMerge(result['topics'], result['similar_topics']));
+ similarTopics.forEach(function(topic, index) {
+ similarTopics[index] = EmberObject.create(topic);
+ });
+
+ this.set('similarTopics', similarTopics);
+ });
+ },
init() {
this._super(...arguments);
+ },
+ actions: {
+ closeMessage(){}
}
});
\ No newline at end of file
diff --git a/assets/javascripts/wizard/components/validator.js.es6 b/assets/javascripts/wizard/components/validator.js.es6
index f46b8319..2e5c47e7 100644
--- a/assets/javascripts/wizard/components/validator.js.es6
+++ b/assets/javascripts/wizard/components/validator.js.es6
@@ -17,8 +17,7 @@ export default Component.extend({
// set a function that can be called as often as it need to
// from the derived component
this.backendValidate = (params) => {
- return ajax('/w/realtime_validation', {
- type: 'put',
+ return ajax('/realtime_validations', {
data: {
validation: this.get('name'),
authenticity_token: getToken(),
diff --git a/assets/javascripts/wizard/initializers/register-widgets.js.es6 b/assets/javascripts/wizard/initializers/register-widgets.js.es6
new file mode 100644
index 00000000..7040d150
--- /dev/null
+++ b/assets/javascripts/wizard/initializers/register-widgets.js.es6
@@ -0,0 +1,276 @@
+import { escapeExpression, formatUsername } from "discourse/lib/utilities";
+import I18n from "I18n";
+import RawHtml from "discourse/widgets/raw-html";
+import { avatarImg } from "discourse/widgets/post";
+import { createWidget } from "discourse/widgets/widget";
+import { dateNode } from "discourse/helpers/node";
+import { emojiUnescape } from "discourse/lib/text";
+import { h } from "virtual-dom";
+import highlightSearch from "discourse/lib/highlight-search";
+import { iconNode } from "discourse-common/lib/icon-library";
+import renderTag from "discourse/lib/render-tag";
+import DiscourseURL from "discourse/lib/url";
+import getURL from "discourse-common/lib/get-url";
+import { wantsNewWindow } from "discourse/lib/intercept-click";
+import { htmlSafe } from "@ember/template";
+import { registerUnbound } from "discourse-common/lib/helpers";
+import renderTags from "discourse/lib/render-tags";
+import TopicStatusIcons from "discourse/helpers/topic-status-icons";
+
+class Highlighted extends RawHtml {
+ constructor(html, term) {
+ super({ html: `${html}` });
+ this.term = term;
+ }
+
+ decorate($html) {
+ highlightSearch($html[0], this.term);
+ }
+}
+
+
+
+export default {
+ name: "wizard-register-widgets",
+ after: "custom-routes",
+ initialize(app) {
+ if (window.location.pathname.indexOf("/w/") < 0) return;
+
+ createWidget("link", {
+ tagName: "a",
+
+ href(attrs) {
+ const route = attrs.route;
+ if (route) {
+ const router = this.register.lookup("router:main");
+ if (router && router._routerMicrolib) {
+ const params = [route];
+ if (attrs.model) {
+ params.push(attrs.model);
+ }
+ return getURL(
+ router._routerMicrolib.generate.apply(router._routerMicrolib, params)
+ );
+ }
+ } else {
+ return getURL(attrs.href);
+ }
+ },
+
+ buildClasses(attrs) {
+ const result = [];
+ result.push("widget-link");
+ if (attrs.className) {
+ result.push(attrs.className);
+ }
+ return result;
+ },
+
+ buildAttributes(attrs) {
+ const ret = {
+ href: this.href(attrs),
+ title: attrs.title
+ ? I18n.t(attrs.title, attrs.titleOptions)
+ : this.label(attrs),
+ };
+ if (attrs.attributes) {
+ Object.keys(attrs.attributes).forEach(
+ (k) => (ret[k] = attrs.attributes[k])
+ );
+ }
+ return ret;
+ },
+
+ label(attrs) {
+ if (attrs.labelCount && attrs.count) {
+ return I18n.t(attrs.labelCount, { count: attrs.count });
+ }
+ return attrs.rawLabel || (attrs.label ? I18n.t(attrs.label) : "");
+ },
+
+ html(attrs) {
+ if (attrs.contents) {
+ return attrs.contents();
+ }
+
+ const result = [];
+ if (attrs.icon) {
+ if (attrs.alt) {
+ let icon = iconNode(attrs.icon);
+ icon.properties.attributes["alt"] = I18n.t(attrs.alt);
+ icon.properties.attributes["aria-hidden"] = false;
+ result.push(icon);
+ } else {
+ result.push(iconNode(attrs.icon));
+ }
+ result.push(" ");
+ }
+
+ if (!attrs.hideLabel) {
+ let label = this.label(attrs);
+
+ if (attrs.omitSpan) {
+ result.push(label);
+ } else {
+ result.push(h("span.d-label", label));
+ }
+ }
+
+ const currentUser = this.currentUser;
+ if (currentUser && attrs.badgeCount) {
+ const val = parseInt(currentUser.get(attrs.badgeCount), 10);
+ if (val > 0) {
+ const title = attrs.badgeTitle ? I18n.t(attrs.badgeTitle) : "";
+ result.push(" ");
+ result.push(
+ h(
+ "span.badge-notification",
+ {
+ className: attrs.badgeClass,
+ attributes: { title },
+ },
+ val
+ )
+ );
+ }
+ }
+ return result;
+ },
+
+ click(e) {
+ if (this.attrs.attributes && this.attrs.attributes.target === "_blank") {
+ return;
+ }
+
+ if (wantsNewWindow(e)) {
+ return;
+ }
+
+ e.preventDefault();
+
+ if (this.attrs.action) {
+ e.preventDefault();
+ return this.sendWidgetAction(this.attrs.action, this.attrs.actionParam);
+ } else {
+ this.sendWidgetEvent("linkClicked", this.attrs);
+ }
+
+ return DiscourseURL.routeToTag($(e.target).closest("a")[0]);
+ },
+ });
+ createWidget("topic-status", {
+ tagName: "div.topic-statuses",
+
+ html(attrs) {
+ const topic = attrs.topic;
+ const canAct = this.currentUser && !attrs.disableActions;
+
+ const result = [];
+ TopicStatusIcons.render(topic, function (name, key) {
+ const iconArgs = key === "unpinned" ? { class: "unpinned" } : null;
+ const icon = iconNode(name, iconArgs);
+
+ const attributes = {
+ title: escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
+ };
+ result.push(h(`${canAct ? "a" : "span"}.topic-status`, attributes, icon));
+ });
+
+ return result;
+ },
+ });
+
+ createSearchResult({
+ type: "topic",
+ linkField: "url",
+ builder(result, term) {
+ const topic = result;
+
+ const firstLine = [
+ this.attach("topic-status", { topic, disableActions: true }),
+ h(
+ "span.topic-title",
+ { attributes: { "data-topic-id": topic.id } },
+ this.siteSettings.use_pg_headlines_for_excerpt &&
+ result.topic_title_headline
+ ? new RawHtml({
+ html: `${emojiUnescape(
+ result.topic_title_headline
+ )}`,
+ })
+ : new Highlighted(topic.fancy_title, term)
+ ),
+ ];
+
+ const secondLine = [
+ // this.attach("category-link", {
+ // category: topic.category,
+ // link: false,
+ // }),
+ ];
+ // if (this.siteSettings.tagging_enabled) {
+ // secondLine.push(
+ // this.attach("discourse-tags", { topic, tagName: "span" })
+ // );
+ // }
+
+ const link = h("span.topic", [
+ h("div.first-line", firstLine),
+ h("div.second-line", secondLine),
+ ]);
+
+ return postResult.call(this, result, link, term);
+ },
+ });
+
+ }
+}
+
+function createSearchResult({ type, linkField, builder }) {
+ return createWidget(`search-result-${type}`, {
+ tagName: "ul.list",
+
+ html(attrs) {
+ return attrs.results.map((r) => {
+ let searchResultId;
+
+ if (type === "topic") {
+ searchResultId = r.topic_id;
+ } else {
+ searchResultId = r.id;
+ }
+
+ return h(
+ "li.item",
+ this.attach("link", {
+ href: r[linkField],
+ contents: () => builder.call(this, r, attrs.term),
+ className: "search-link",
+ searchResultId,
+ searchResultType: type,
+ searchContextEnabled: attrs.searchContextEnabled,
+ searchLogId: attrs.searchLogId,
+ })
+ );
+ });
+ },
+ });
+}
+
+function postResult(result, link, term) {
+ const html = [link];
+
+ if (!this.site.mobileView) {
+ html.push(
+ h("span.blurb", [
+ dateNode(result.created_at),
+ h("span", " - "),
+ this.siteSettings.use_pg_headlines_for_excerpt
+ ? new RawHtml({ html: `${result.blurb}` })
+ : new Highlighted(result.blurb, term),
+ ])
+ );
+ }
+
+ return html;
+}
\ No newline at end of file
diff --git a/assets/javascripts/wizard/templates/components/suggested-validator.hbs b/assets/javascripts/wizard/templates/components/suggested-validator.hbs
index e69de29b..87ddd03f 100644
--- a/assets/javascripts/wizard/templates/components/suggested-validator.hbs
+++ b/assets/javascripts/wizard/templates/components/suggested-validator.hbs
@@ -0,0 +1,10 @@
+{{#if similarTopics}}
+ {{d-icon "times"}}
+ {{i18n "composer.similar_topics"}}
+
+
+
+ {{mount-widget widget="search-result-topic" args=(hash results=similarTopics)}}
+
+{{/if}}
+
diff --git a/assets/javascripts/wizard/templates/components/wizard-field.hbs b/assets/javascripts/wizard/templates/components/wizard-field.hbs
index 8f342e54..b0563384 100644
--- a/assets/javascripts/wizard/templates/components/wizard-field.hbs
+++ b/assets/javascripts/wizard/templates/components/wizard-field.hbs
@@ -13,7 +13,7 @@
{{#field-validators field=field as |validators|}}
{{#if inputComponentName}}
- {{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard focusOut=validators.perform}}
+ {{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard }}
{{/if}}
{{/field-validators}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index bb1d03c0..555f8a94 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -178,9 +178,11 @@ en:
instructions: "Moment.js format"
validations:
header: "Realtime Validation Settings"
- suggested_topics: "Suggested Topics"
+ similar_topics: "Similar Topics"
above: "Above"
below: "Below"
+ categories: "Categories"
+ date_after: "Date After"
type:
text: "Text"
diff --git a/config/routes.rb b/config/routes.rb
index 7f30e311..3421d55a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,12 +4,12 @@ CustomWizard::Engine.routes.draw do
get ':wizard_id/steps' => 'wizard#index'
get ':wizard_id/steps/:step_id' => 'wizard#index'
put ':wizard_id/steps/:step_id' => 'steps#update'
- put 'realtime_validation' => 'realtime_validation#validate'
end
Discourse::Application.routes.append do
mount ::CustomWizard::Engine, at: 'w'
post 'wizard/authorization/callback' => "custom_wizard/authorization#callback"
+ get 'realtime_validations' => 'custom_wizard/realtime_validations#validate'
scope module: 'custom_wizard', constraints: AdminConstraint.new do
get 'admin/wizards' => 'admin#index'
diff --git a/controllers/custom_wizard/realtime_validation.rb b/controllers/custom_wizard/realtime_validations.rb
similarity index 78%
rename from controllers/custom_wizard/realtime_validation.rb
rename to controllers/custom_wizard/realtime_validations.rb
index a2e0dac1..38ae1a0d 100644
--- a/controllers/custom_wizard/realtime_validation.rb
+++ b/controllers/custom_wizard/realtime_validations.rb
@@ -1,4 +1,4 @@
-class CustomWizard::RealtimeValidationController < ::ApplicationController
+class CustomWizard::RealtimeValidationsController < ::ApplicationController
def validate
params.require(:validation)
params.require(::CustomWizard::RealtimeValidation.types[params[:validation].to_sym][:required_params])
diff --git a/lib/custom_wizard/realtime_validation.rb b/lib/custom_wizard/realtime_validation.rb
index f015d3fb..b5df6a9e 100644
--- a/lib/custom_wizard/realtime_validation.rb
+++ b/lib/custom_wizard/realtime_validation.rb
@@ -1,7 +1,7 @@
class CustomWizard::RealtimeValidation
cattr_accessor :types
@@types ||= {
- suggested_topics: { types: [:text], component: "suggested-validator", backend: true, required_params: [] }
+ similar_topics: { types: [:text], component: "suggested-validator", backend: true, required_params: [] }
}
class SimilarTopic
@@ -16,15 +16,19 @@ class CustomWizard::RealtimeValidation
end
end
- def self.suggested_topics(params, current_user)
+ def self.similar_topics(params, current_user)
title = params[:title]
raw = params[:raw]
+ categories = params[:categories]
+ date_after = params[:date_after]
if title.length < SiteSetting.min_title_similar_length || !Topic.count_exceeds_minimum?
return []
end
topics = Topic.similar_to(title, raw, current_user).to_a
+ topics.select! { |t| categories.include?(t.category.id.to_s) } if categories.present?
+ topics.select! { |t| t.created_at > DateTime.parse(date_after) } if date_after.present?
topics.map! { |t| SimilarTopic.new(t) }
::ActiveModel::ArraySerializer.new(topics, each_serializer: SimilarTopicSerializer, root: :similar_topics, rest_serializer: true, scope: ::Guardian.new(current_user))
end
diff --git a/plugin.rb b/plugin.rb
index 83699751..46cd4e78 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -47,7 +47,7 @@ after_initialize do
../controllers/custom_wizard/admin/custom_fields.rb
../controllers/custom_wizard/wizard.rb
../controllers/custom_wizard/steps.rb
- ../controllers/custom_wizard/realtime_validation.rb
+ ../controllers/custom_wizard/realtime_validations.rb
../jobs/clear_after_time_wizard.rb
../jobs/refresh_api_access_token.rb
../jobs/set_after_time_wizard.rb