1
0
Fork 0
* Code formatting
* Both "type" and "name" are used to refer to the validation type. Changed all to "type".
* Added proper abstraction of realtime validation classes on server
* UI improvements in admin and wizard
Dieser Commit ist enthalten in:
angusmcleod 2021-02-16 11:43:00 +11:00
Ursprung 8a6b240b46
Commit c45e51fcb6
19 geänderte Dateien mit 186 neuen und 82 gelöschten Zeilen

Datei anzeigen

@ -30,6 +30,7 @@ export default Component.extend(UndoChanges, {
@discourseComputed('field.type') @discourseComputed('field.type')
validations(type) { validations(type) {
const applicableToField = []; const applicableToField = [];
for(let validation in wizardSchema.field.validations) { for(let validation in wizardSchema.field.validations) {
if ((wizardSchema.field.validations[validation]["types"]).includes(type)) { if ((wizardSchema.field.validations[validation]["types"]).includes(type)) {
applicableToField.push(validation) applicableToField.push(validation)

Datei anzeigen

@ -12,6 +12,7 @@ export default Component.extend({
if (!this.field.validations) { if (!this.field.validations) {
const validations = {}; const validations = {};
this.validations.forEach((validation) => { this.validations.forEach((validation) => {
validations[validation] = {}; validations[validation] = {};
}); });
@ -32,10 +33,10 @@ export default Component.extend({
}, },
actions: { actions: {
updateValidationCategories(name, validation, categories) { updateValidationCategories(type, validation, categories) {
this.set(`validationBuffer.${name}.categories`, categories); this.set(`validationBuffer.${type}.categories`, categories);
this.set( this.set(
`field.validations.${name}.categories`, `field.validations.${type}.categories`,
categories.map((category) => category.id) categories.map((category) => category.id)
); );
}, },

Datei anzeigen

@ -1,10 +1,10 @@
<h3>{{i18n 'admin.wizard.field.validations.header'}}</h3> <h3>{{i18n 'admin.wizard.field.validations.header'}}</h3>
<ul> <ul>
{{#each-in field.validations as |name props|}} {{#each-in field.validations as |type props|}}
<li> <li>
<span class="setting-title"> <span class="setting-title">
<h4>{{i18n (concat 'admin.wizard.field.validations.' name)}}</h4> <h4>{{i18n (concat 'admin.wizard.field.validations.' type)}}</h4>
{{input type="checkbox" checked=props.status}} {{input type="checkbox" checked=props.status}}
{{i18n 'admin.wizard.field.validations.enabled'}} {{i18n 'admin.wizard.field.validations.enabled'}}
</span> </span>
@ -15,8 +15,8 @@
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{category-selector {{category-selector
categories=(get this (concat 'validationBuffer.' name '.categories')) categories=(get this (concat 'validationBuffer.' type '.categories'))
onChange=(action 'updateValidationCategories' name props) onChange=(action 'updateValidationCategories' type props)
class="wizard"}} class="wizard"}}
</div> </div>
</div> </div>
@ -36,9 +36,9 @@
<label>{{i18n 'admin.wizard.field.validations.position'}}</label> <label>{{i18n 'admin.wizard.field.validations.position'}}</label>
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{radio-button name=(concat name field.id) value="above" selection=props.position}} {{radio-button name=(concat type field.id) value="above" selection=props.position}}
{{i18n 'admin.wizard.field.validations.above'}} {{i18n 'admin.wizard.field.validations.above'}}
{{radio-button name=(concat name field.id) value="below" selection=props.position}} {{radio-button name=(concat type field.id) value="below" selection=props.position}}
{{i18n 'admin.wizard.field.validations.below'}} {{i18n 'admin.wizard.field.validations.below'}}
</div> </div>
</div> </div>

Datei anzeigen

@ -3,15 +3,34 @@ import { deepMerge } from "discourse-common/lib/object";
import { observes } from "discourse-common/utils/decorators"; import { observes } from "discourse-common/utils/decorators";
import { cancel, later } from "@ember/runloop"; import { cancel, later } from "@ember/runloop";
import { A } from "@ember/array"; import { A } from "@ember/array";
import EmberObject from "@ember/object"; import EmberObject, { computed } from "@ember/object";
import { notEmpty, and, equal, empty } from "@ember/object/computed";
export default WizardFieldValidator.extend({ export default WizardFieldValidator.extend({
similarTopics: [], classNames: ['similar-topics-validator'],
similarTopics: null,
hasInput: notEmpty('field.value'),
hasSimilarTopics: notEmpty('similarTopics'),
hasNotSearched: equal('similarTopics', null),
noSimilarTopics: computed('similarTopics', function() {
return this.similarTopics !== null && this.similarTopics.length == 0;
}),
showDefault: and('hasNotSearched', 'hasInput'),
validate() {}, validate() {},
@observes("field.value") @observes("field.value")
customValidate() { customValidate() {
const field = this.field;
if (!field.value) return;
const value = field.value;
if (value && value.length < 5) {
this.set('similarTopics', null);
return;
}
const lastKeyUp = new Date(); const lastKeyUp = new Date();
this._lastKeyUp = lastKeyUp; this._lastKeyUp = lastKeyUp;
@ -22,12 +41,14 @@ export default WizardFieldValidator.extend({
if (lastKeyUp !== this._lastKeyUp) { if (lastKeyUp !== this._lastKeyUp) {
return; return;
} }
this.updateSimilarTopics(); this.updateSimilarTopics();
}, 1000); }, 1000);
}, },
updateSimilarTopics() { updateSimilarTopics() {
this.set('updating', true);
this.backendValidate({ this.backendValidate({
title: this.get("field.value"), title: this.get("field.value"),
categories: this.get("validation.categories"), categories: this.get("validation.categories"),
@ -41,7 +62,7 @@ export default WizardFieldValidator.extend({
}); });
this.set("similarTopics", similarTopics); this.set("similarTopics", similarTopics);
}); }).finally(() => this.set('updating', false));
}, },
actions: { actions: {

Datei anzeigen

@ -1,15 +1,17 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { not } from "@ember/object/computed"; import { equal } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { getToken } from "wizard/lib/ajax"; import { getToken } from "wizard/lib/ajax";
export default Component.extend({ export default Component.extend({
classNames: ['validator'],
classNameBindings: ["isValid", "isInvalid"], classNameBindings: ["isValid", "isInvalid"],
validMessageKey: null, validMessageKey: null,
invalidMessageKey: null, invalidMessageKey: null,
isValid: null, isValid: null,
isInvalid: not("isValid"), isInvalid: equal("isValid", false),
layoutName: "components/validator", // useful for sharing the template with extending components layoutName: "components/validator", // useful for sharing the template with extending components
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -19,7 +21,7 @@ export default Component.extend({
this.backendValidate = (params) => { this.backendValidate = (params) => {
return ajax("/realtime-validations", { return ajax("/realtime-validations", {
data: { data: {
validation: this.get("name"), type: this.get("type"),
authenticity_token: getToken(), authenticity_token: getToken(),
...params, ...params,
}, },

Datei anzeigen

@ -1,12 +1,12 @@
{{#if field.validations}} {{#if field.validations}}
{{#each-in field.validations.above as |name validation|}} {{#each-in field.validations.above as |type validation|}}
{{component validation.component field=field name=name validation=validation}} {{component validation.component field=field type=type validation=validation}}
{{/each-in}} {{/each-in}}
{{yield (hash perform=(action 'perform'))}} {{yield (hash perform=(action 'perform'))}}
{{#each-in field.validations.below as |name validation|}} {{#each-in field.validations.below as |type validation|}}
{{component validation.component field=field name=name validation=validation}} {{component validation.component field=field type=type validation=validation}}
{{/each-in}} {{/each-in}}
{{else}} {{else}}
{{yield}} {{yield}}

Datei anzeigen

@ -1,8 +1,16 @@
{{#if similarTopics}} {{#if loading}}
<h3>{{i18n 'realtime_validations.similar_topics_heading'}}</h3> <label>{{i18n 'realtime_validations.similar_topics.loading'}}</label>
<div class="wizard-similar-topics"> {{else if hasSimilarTopics}}
{{#each similarTopics as |similarTopic|}} <label>{{i18n 'realtime_validations.similar_topics.results'}}</label>
{{wizard-similar-topic topic=similarTopic}} <ul class="wizard-similar-topics">
{{/each}} {{#each similarTopics as |similarTopic|}}
</div> <li>{{wizard-similar-topic topic=similarTopic}}</li>
{{/each}}
</ul>
{{else if noSimilarTopics}}
<label>{{i18n 'realtime_validations.similar_topics.no_results'}}</label>
{{else if showDefault}}
<label>{{i18n 'realtime_validations.similar_topics.default'}}</label>
{{else}}
<label></label>
{{/if}} {{/if}}

Datei anzeigen

@ -644,14 +644,18 @@
} }
} }
.realtime-validations ul { .realtime-validations > ul {
list-style: none; list-style: none;
margin: 0; margin: 0;
li { > li {
background-color: var(--tertiary-low); background-color: var(--primary-low);
padding: 0 1em; padding: 1em;
margin: 0 0 1em 0; margin: 0 0 1em 0;
input {
margin-bottom: 0;
}
} }
} }

Datei anzeigen

@ -155,18 +155,8 @@
.select-kit.combo-box.group-dropdown { .select-kit.combo-box.group-dropdown {
min-width: 220px; min-width: 220px;
} }
}
.wizard-similar-topics {
background-color: var(--tertiary-low);
padding: 5px;
.title {
color: var(--primary);
}
.blurb { .text-field input {
margin-left: 0.5em; margin-bottom: 0;
color: var(--primary-high);
font-size: $font-down-1;
} }
} }

Datei anzeigen

@ -0,0 +1,30 @@
.similar-topics-validator {
position: relative;
label {
min-height: 20px;
display: inline-block;
}
}
.wizard-similar-topics {
background-color: var(--tertiary-low);
padding: 5px;
margin: 0;
list-style: none;
position: absolute;
top: 25px;
box-shadow: shadow("dropdown");
width: 100%;
box-sizing: border-box;
.title {
color: var(--primary);
}
.blurb {
margin-left: 0.5em;
color: var(--primary-high);
font-size: $font-down-1;
}
}

Datei anzeigen

@ -10,6 +10,7 @@
@import "custom/wizard"; @import "custom/wizard";
@import "custom/step"; @import "custom/step";
@import "custom/field"; @import "custom/field";
@import "custom/validators";
@import "custom/mobile"; @import "custom/mobile";
@import "custom/autocomplete"; @import "custom/autocomplete";
@import "custom/composer"; @import "custom/composer";

Datei anzeigen

@ -178,7 +178,7 @@ en:
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>" instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
validations: validations:
header: "Realtime Validations" header: "Realtime Validations"
enabled: "Enable this validation" enabled: "Enabled"
similar_topics: "Similar Topics" similar_topics: "Similar Topics"
position: "Position" position: "Position"
above: "Above" above: "Above"
@ -521,4 +521,8 @@ en:
title: "Did you forget to add recipients?" title: "Did you forget to add recipients?"
body: "Right now this message is only being sent to yourself!" body: "Right now this message is only being sent to yourself!"
realtime_validations: realtime_validations:
similar_topics_heading: "Your topic is similar to..." similar_topics:
default: "When you stop typing we'll look for similar topics."
results: "Your topic is similar to..."
no_results: "No similar topics."
loading: "Looking for similar topics..."

Datei anzeigen

@ -2,10 +2,17 @@
class CustomWizard::RealtimeValidationsController < ::ApplicationController class CustomWizard::RealtimeValidationsController < ::ApplicationController
def validate def validate
params.require(:validation) klass_str = "CustomWizard::RealtimeValidation::#{validation_params[:type].camelize}"
params.require(::CustomWizard::RealtimeValidation.types[params[:validation].to_sym][:required_params]) result = klass_str.constantize.new(current_user).perform(validation_params)
render_serialized(result.items, "#{klass_str}Serializer".constantize, result.serializer_opts)
result = ::CustomWizard::RealtimeValidation.send(params[:validation], params, current_user) end
render_serialized(result[:items], result[:serializer], result[:opts])
private
def validation_params
params.require(:type)
settings = ::CustomWizard::RealtimeValidation.types[params[:type].to_sym]
params.require(settings[:required_params]) if settings[:required_params].present?
params
end end
end end

Datei anzeigen

@ -2,36 +2,13 @@
class CustomWizard::RealtimeValidation class CustomWizard::RealtimeValidation
cattr_accessor :types cattr_accessor :types
@@types ||= { @@types ||= {
similar_topics: { types: [:text], component: "similar-topics-validator", backend: true, required_params: [] } similar_topics: {
types: [:text],
component: "similar-topics-validator",
backend: true,
required_params: []
}
} }
class SimilarTopic
def initialize(topic)
@topic = topic
end
attr_reader :topic
def blurb
Search::GroupedSearchResults.blurb_for(cooked: @topic.try(:blurb))
end
end
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) }
{ items: topics, serializer: SimilarTopicSerializer, opts: { root: :similar_topics } }
end
end end

Datei anzeigen

@ -0,0 +1,11 @@
class CustomWizard::RealtimeValidation::Result
attr_accessor :type,
:items,
:serializer_opts
def initialize(type)
@type = type
@items = []
@serializer_opts = {}
end
end

Datei anzeigen

@ -0,0 +1,42 @@
class CustomWizard::RealtimeValidation::SimilarTopics
attr_accessor :user
def initialize(user)
@user = user
end
class SimilarTopic
def initialize(topic)
@topic = topic
end
attr_reader :topic
def blurb
Search::GroupedSearchResults.blurb_for(cooked: @topic.try(:blurb))
end
end
def perform(params)
title = params[:title]
raw = params[:raw]
categories = params[:categories]
date_after = params[:date_after]
result = CustomWizard::RealtimeValidation::Result.new(:similar_topic)
if title.length < SiteSetting.min_title_similar_length || !Topic.count_exceeds_minimum?
return result
end
topics = Topic.similar_to(title, raw, 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) }
result.items = topics
result.serializer_opts = { root: :similar_topics }
result
end
end

Datei anzeigen

@ -60,6 +60,8 @@ after_initialize do
../lib/custom_wizard/custom_field.rb ../lib/custom_wizard/custom_field.rb
../lib/custom_wizard/field.rb ../lib/custom_wizard/field.rb
../lib/custom_wizard/realtime_validation.rb ../lib/custom_wizard/realtime_validation.rb
../lib/custom_wizard/realtime_validations/result.rb
../lib/custom_wizard/realtime_validations/similar_topics.rb
../lib/custom_wizard/mapper.rb ../lib/custom_wizard/mapper.rb
../lib/custom_wizard/log.rb ../lib/custom_wizard/log.rb
../lib/custom_wizard/step_updater.rb ../lib/custom_wizard/step_updater.rb
@ -81,6 +83,7 @@ after_initialize do
../serializers/custom_wizard/wizard_step_serializer.rb ../serializers/custom_wizard/wizard_step_serializer.rb
../serializers/custom_wizard/wizard_serializer.rb ../serializers/custom_wizard/wizard_serializer.rb
../serializers/custom_wizard/log_serializer.rb ../serializers/custom_wizard/log_serializer.rb
../serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb
../extensions/extra_locales_controller.rb ../extensions/extra_locales_controller.rb
../extensions/invites_controller.rb ../extensions/invites_controller.rb
../extensions/users_controller.rb ../extensions/users_controller.rb

Datei anzeigen

@ -0,0 +1,2 @@
class ::CustomWizard::RealtimeValidation::SimilarTopicsSerializer < ::SimilarTopicSerializer
end

Datei anzeigen

@ -61,10 +61,10 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
def validations def validations
validations = {} validations = {}
object.validations&.each do |name, props| object.validations&.each do |type, props|
next unless props["status"] next unless props["status"]
validations[props["position"]] ||= {} validations[props["position"]] ||= {}
validations[props["position"]][name] = props.merge CustomWizard::RealtimeValidation.types[name.to_sym] validations[props["position"]][type] = props.merge CustomWizard::RealtimeValidation.types[type.to_sym]
end end
validations validations