Various fixes
* 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:
Ursprung
8a6b240b46
Commit
c45e51fcb6
19 geänderte Dateien mit 186 neuen und 82 gelöschten Zeilen
|
@ -30,6 +30,7 @@ export default Component.extend(UndoChanges, {
|
|||
@discourseComputed('field.type')
|
||||
validations(type) {
|
||||
const applicableToField = [];
|
||||
|
||||
for(let validation in wizardSchema.field.validations) {
|
||||
if ((wizardSchema.field.validations[validation]["types"]).includes(type)) {
|
||||
applicableToField.push(validation)
|
||||
|
|
|
@ -12,6 +12,7 @@ export default Component.extend({
|
|||
|
||||
if (!this.field.validations) {
|
||||
const validations = {};
|
||||
|
||||
this.validations.forEach((validation) => {
|
||||
validations[validation] = {};
|
||||
});
|
||||
|
@ -32,10 +33,10 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
updateValidationCategories(name, validation, categories) {
|
||||
this.set(`validationBuffer.${name}.categories`, categories);
|
||||
updateValidationCategories(type, validation, categories) {
|
||||
this.set(`validationBuffer.${type}.categories`, categories);
|
||||
this.set(
|
||||
`field.validations.${name}.categories`,
|
||||
`field.validations.${type}.categories`,
|
||||
categories.map((category) => category.id)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<h3>{{i18n 'admin.wizard.field.validations.header'}}</h3>
|
||||
|
||||
<ul>
|
||||
{{#each-in field.validations as |name props|}}
|
||||
{{#each-in field.validations as |type props|}}
|
||||
<li>
|
||||
<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}}
|
||||
{{i18n 'admin.wizard.field.validations.enabled'}}
|
||||
</span>
|
||||
|
@ -15,8 +15,8 @@
|
|||
</div>
|
||||
<div class="setting-value">
|
||||
{{category-selector
|
||||
categories=(get this (concat 'validationBuffer.' name '.categories'))
|
||||
onChange=(action 'updateValidationCategories' name props)
|
||||
categories=(get this (concat 'validationBuffer.' type '.categories'))
|
||||
onChange=(action 'updateValidationCategories' type props)
|
||||
class="wizard"}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,9 +36,9 @@
|
|||
<label>{{i18n 'admin.wizard.field.validations.position'}}</label>
|
||||
</div>
|
||||
<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'}}
|
||||
{{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'}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,15 +3,34 @@ 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";
|
||||
import EmberObject, { computed } from "@ember/object";
|
||||
import { notEmpty, and, equal, empty } from "@ember/object/computed";
|
||||
|
||||
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() {},
|
||||
|
||||
@observes("field.value")
|
||||
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();
|
||||
this._lastKeyUp = lastKeyUp;
|
||||
|
||||
|
@ -28,6 +47,8 @@ export default WizardFieldValidator.extend({
|
|||
},
|
||||
|
||||
updateSimilarTopics() {
|
||||
this.set('updating', true);
|
||||
|
||||
this.backendValidate({
|
||||
title: this.get("field.value"),
|
||||
categories: this.get("validation.categories"),
|
||||
|
@ -41,7 +62,7 @@ export default WizardFieldValidator.extend({
|
|||
});
|
||||
|
||||
this.set("similarTopics", similarTopics);
|
||||
});
|
||||
}).finally(() => this.set('updating', false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import Component from "@ember/component";
|
||||
import { not } from "@ember/object/computed";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { getToken } from "wizard/lib/ajax";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['validator'],
|
||||
classNameBindings: ["isValid", "isInvalid"],
|
||||
validMessageKey: null,
|
||||
invalidMessageKey: null,
|
||||
isValid: null,
|
||||
isInvalid: not("isValid"),
|
||||
isInvalid: equal("isValid", false),
|
||||
layoutName: "components/validator", // useful for sharing the template with extending components
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
|
@ -19,7 +21,7 @@ export default Component.extend({
|
|||
this.backendValidate = (params) => {
|
||||
return ajax("/realtime-validations", {
|
||||
data: {
|
||||
validation: this.get("name"),
|
||||
type: this.get("type"),
|
||||
authenticity_token: getToken(),
|
||||
...params,
|
||||
},
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{{#if field.validations}}
|
||||
{{#each-in field.validations.above as |name validation|}}
|
||||
{{component validation.component field=field name=name validation=validation}}
|
||||
{{#each-in field.validations.above as |type validation|}}
|
||||
{{component validation.component field=field type=type validation=validation}}
|
||||
{{/each-in}}
|
||||
|
||||
{{yield (hash perform=(action 'perform'))}}
|
||||
|
||||
{{#each-in field.validations.below as |name validation|}}
|
||||
{{component validation.component field=field name=name validation=validation}}
|
||||
{{#each-in field.validations.below as |type validation|}}
|
||||
{{component validation.component field=field type=type validation=validation}}
|
||||
{{/each-in}}
|
||||
{{else}}
|
||||
{{yield}}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
{{#if similarTopics}}
|
||||
<h3>{{i18n 'realtime_validations.similar_topics_heading'}}</h3>
|
||||
<div class="wizard-similar-topics">
|
||||
{{#if loading}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.loading'}}</label>
|
||||
{{else if hasSimilarTopics}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.results'}}</label>
|
||||
<ul class="wizard-similar-topics">
|
||||
{{#each similarTopics as |similarTopic|}}
|
||||
{{wizard-similar-topic topic=similarTopic}}
|
||||
<li>{{wizard-similar-topic topic=similarTopic}}</li>
|
||||
{{/each}}
|
||||
</div>
|
||||
</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}}
|
||||
|
|
|
@ -644,14 +644,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.realtime-validations ul {
|
||||
.realtime-validations > ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
background-color: var(--tertiary-low);
|
||||
padding: 0 1em;
|
||||
> li {
|
||||
background-color: var(--primary-low);
|
||||
padding: 1em;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -155,18 +155,8 @@
|
|||
.select-kit.combo-box.group-dropdown {
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-similar-topics {
|
||||
background-color: var(--tertiary-low);
|
||||
padding: 5px;
|
||||
.title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.blurb {
|
||||
margin-left: 0.5em;
|
||||
color: var(--primary-high);
|
||||
font-size: $font-down-1;
|
||||
.text-field input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
30
assets/stylesheets/wizard/custom/validators.scss
Normale Datei
30
assets/stylesheets/wizard/custom/validators.scss
Normale Datei
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
@import "custom/wizard";
|
||||
@import "custom/step";
|
||||
@import "custom/field";
|
||||
@import "custom/validators";
|
||||
@import "custom/mobile";
|
||||
@import "custom/autocomplete";
|
||||
@import "custom/composer";
|
||||
|
|
|
@ -178,7 +178,7 @@ en:
|
|||
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
|
||||
validations:
|
||||
header: "Realtime Validations"
|
||||
enabled: "Enable this validation"
|
||||
enabled: "Enabled"
|
||||
similar_topics: "Similar Topics"
|
||||
position: "Position"
|
||||
above: "Above"
|
||||
|
@ -521,4 +521,8 @@ en:
|
|||
title: "Did you forget to add recipients?"
|
||||
body: "Right now this message is only being sent to yourself!"
|
||||
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..."
|
||||
|
|
|
@ -2,10 +2,17 @@
|
|||
|
||||
class CustomWizard::RealtimeValidationsController < ::ApplicationController
|
||||
def validate
|
||||
params.require(:validation)
|
||||
params.require(::CustomWizard::RealtimeValidation.types[params[:validation].to_sym][:required_params])
|
||||
klass_str = "CustomWizard::RealtimeValidation::#{validation_params[:type].camelize}"
|
||||
result = klass_str.constantize.new(current_user).perform(validation_params)
|
||||
render_serialized(result.items, "#{klass_str}Serializer".constantize, result.serializer_opts)
|
||||
end
|
||||
|
||||
result = ::CustomWizard::RealtimeValidation.send(params[:validation], params, current_user)
|
||||
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
|
||||
|
|
|
@ -2,36 +2,13 @@
|
|||
|
||||
class CustomWizard::RealtimeValidation
|
||||
cattr_accessor :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
|
||||
|
|
11
lib/custom_wizard/realtime_validations/result.rb
Normale Datei
11
lib/custom_wizard/realtime_validations/result.rb
Normale Datei
|
@ -0,0 +1,11 @@
|
|||
class CustomWizard::RealtimeValidation::Result
|
||||
attr_accessor :type,
|
||||
:items,
|
||||
:serializer_opts
|
||||
|
||||
def initialize(type)
|
||||
@type = type
|
||||
@items = []
|
||||
@serializer_opts = {}
|
||||
end
|
||||
end
|
42
lib/custom_wizard/realtime_validations/similar_topics.rb
Normale Datei
42
lib/custom_wizard/realtime_validations/similar_topics.rb
Normale Datei
|
@ -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
|
|
@ -60,6 +60,8 @@ after_initialize do
|
|||
../lib/custom_wizard/custom_field.rb
|
||||
../lib/custom_wizard/field.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/log.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_serializer.rb
|
||||
../serializers/custom_wizard/log_serializer.rb
|
||||
../serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb
|
||||
../extensions/extra_locales_controller.rb
|
||||
../extensions/invites_controller.rb
|
||||
../extensions/users_controller.rb
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
class ::CustomWizard::RealtimeValidation::SimilarTopicsSerializer < ::SimilarTopicSerializer
|
||||
end
|
|
@ -61,10 +61,10 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
|
|||
|
||||
def validations
|
||||
validations = {}
|
||||
object.validations&.each do |name, props|
|
||||
object.validations&.each do |type, props|
|
||||
next unless props["status"]
|
||||
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
|
||||
|
||||
validations
|
||||
|
|
Laden …
In neuem Issue referenzieren