Merge pull request #69 from paviliondev/realtime-validation
FEATURE: Implemented realtime validation framework
Dieser Commit ist enthalten in:
Commit
7f021791f5
39 geänderte Dateien mit 701 neuen und 13 gelöschten Zeilen
|
@ -4,6 +4,7 @@ import { computed } from "@ember/object";
|
|||
import { selectKitContent } from '../lib/wizard';
|
||||
import UndoChanges from '../mixins/undo-changes';
|
||||
import Component from "@ember/component";
|
||||
import wizardSchema from '../lib/wizard-schema';
|
||||
|
||||
export default Component.extend(UndoChanges, {
|
||||
componentType: 'field',
|
||||
|
@ -26,6 +27,19 @@ export default Component.extend(UndoChanges, {
|
|||
showAdvanced: alias('field.type'),
|
||||
messageUrl: 'https://thepavilion.io/t/2809',
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
return applicableToField;
|
||||
},
|
||||
|
||||
@discourseComputed('field.type')
|
||||
isDateTime(type) {
|
||||
return ['date_time', 'date', 'time'].indexOf(type) > -1;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
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({
|
||||
classNames: ["realtime-validations"],
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (!this.validations) return;
|
||||
|
||||
if (!this.field.validations) {
|
||||
const validations = {};
|
||||
|
||||
this.validations.forEach((validation) => {
|
||||
validations[validation] = {};
|
||||
});
|
||||
|
||||
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(type, validation, categories) {
|
||||
this.set(`validationBuffer.${type}.categories`, categories);
|
||||
this.set(
|
||||
`field.validations.${type}.categories`,
|
||||
categories.map((category) => category.id)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -244,6 +244,10 @@ export function buildFieldTypes(types) {
|
|||
wizardSchema.field.types = types;
|
||||
}
|
||||
|
||||
export function buildFieldValidations(validations) {
|
||||
wizardSchema.field.validations = validations;
|
||||
}
|
||||
|
||||
if (Discourse.SiteSettings.wizard_apis_enabled) {
|
||||
wizardSchema.action.types.send_to_api = {
|
||||
api: null,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { buildFieldTypes } from '../lib/wizard-schema';
|
||||
import { buildFieldTypes, buildFieldValidations } from '../lib/wizard-schema';
|
||||
import EmberObject, { set } from "@ember/object";
|
||||
import { A } from "@ember/array";
|
||||
import { all } from "rsvp";
|
||||
|
@ -12,7 +12,8 @@ export default DiscourseRoute.extend({
|
|||
|
||||
afterModel(model) {
|
||||
buildFieldTypes(model.field_types);
|
||||
|
||||
buildFieldValidations(model.realtime_validations);
|
||||
|
||||
return all([
|
||||
this._getThemes(model),
|
||||
this._getApis(model),
|
||||
|
|
|
@ -219,6 +219,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#if validations}}
|
||||
{{wizard-realtime-validations field=field validations=validations}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<h3>{{i18n 'admin.wizard.field.validations.header'}}</h3>
|
||||
|
||||
<ul>
|
||||
{{#each-in field.validations as |type props|}}
|
||||
<li>
|
||||
<span class="setting-title">
|
||||
<h4>{{i18n (concat 'admin.wizard.field.validations.' type)}}</h4>
|
||||
{{input type="checkbox" checked=props.status}}
|
||||
{{i18n 'admin.wizard.field.validations.enabled'}}
|
||||
</span>
|
||||
<div class="validation-container">
|
||||
<div class="validation-section">
|
||||
<div class="setting-label">
|
||||
<label>{{i18n 'admin.wizard.field.validations.categories'}}</label>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{category-selector
|
||||
categories=(get this (concat 'validationBuffer.' type '.categories'))
|
||||
onChange=(action 'updateValidationCategories' type props)
|
||||
class="wizard"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-section">
|
||||
<div class="setting-label">
|
||||
<label>{{i18n 'admin.wizard.field.validations.date_after'}}</label>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{date-picker-past
|
||||
value=(readonly props.date_after)
|
||||
containerId="date-container"
|
||||
onSelect=(action (mut props.date_after))}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-section">
|
||||
<div class="setting-label">
|
||||
<label>{{i18n 'admin.wizard.field.validations.position'}}</label>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{radio-button name=(concat type field.id) value="above" selection=props.position}}
|
||||
{{i18n 'admin.wizard.field.validations.above'}}
|
||||
{{radio-button name=(concat type field.id) value="below" selection=props.position}}
|
||||
{{i18n 'admin.wizard.field.validations.below'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{/each-in}}
|
||||
</ul>
|
9
assets/javascripts/wizard/components/field-validators.js.es6
Normale Datei
9
assets/javascripts/wizard/components/field-validators.js.es6
Normale Datei
|
@ -0,0 +1,9 @@
|
|||
import Component from "@ember/component";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
export default Component.extend({
|
||||
actions: {
|
||||
perform() {
|
||||
this.appEvents.trigger("custom-wizard:validate");
|
||||
},
|
||||
},
|
||||
});
|
84
assets/javascripts/wizard/components/similar-topics-validator.js.es6
Normale Datei
84
assets/javascripts/wizard/components/similar-topics-validator.js.es6
Normale Datei
|
@ -0,0 +1,84 @@
|
|||
import WizardFieldValidator from "../../wizard/components/validator";
|
||||
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, { computed } from "@ember/object";
|
||||
import { notEmpty, and, equal, empty } from "@ember/object/computed";
|
||||
|
||||
export default WizardFieldValidator.extend({
|
||||
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: computed('hasNotSearched', 'hasInput', 'typing', function() {
|
||||
return this.hasInput && (this.hasNotSearched || this.typing);
|
||||
}),
|
||||
showSimilarTopics: computed('typing', 'hasSimilarTopics', function() {
|
||||
return this.hasSimilarTopics && !this.typing;
|
||||
}),
|
||||
showNoSimilarTopics: computed('typing', 'noSimilarTopics', function() {
|
||||
return this.noSimilarTopics && !this.typing;
|
||||
}),
|
||||
|
||||
validate() {},
|
||||
|
||||
@observes("field.value")
|
||||
customValidate() {
|
||||
const field = this.field;
|
||||
|
||||
if (!field.value) return;
|
||||
const value = field.value;
|
||||
|
||||
this.set("typing", true);
|
||||
|
||||
if (value && value.length < 5) {
|
||||
this.set('similarTopics', null);
|
||||
return;
|
||||
}
|
||||
|
||||
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.set("typing", false);
|
||||
|
||||
this.updateSimilarTopics();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
updateSimilarTopics() {
|
||||
this.set('updating', true);
|
||||
|
||||
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);
|
||||
}).finally(() => this.set('updating', false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
closeMessage() {
|
||||
this.set("showMessage", false);
|
||||
},
|
||||
},
|
||||
});
|
44
assets/javascripts/wizard/components/validator.js.es6
Normale Datei
44
assets/javascripts/wizard/components/validator.js.es6
Normale Datei
|
@ -0,0 +1,44 @@
|
|||
import Component from "@ember/component";
|
||||
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: equal("isValid", false),
|
||||
layoutName: "components/validator", // useful for sharing the template with extending components
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.get("validation.backend")) {
|
||||
// set a function that can be called as often as it need to
|
||||
// from the derived component
|
||||
this.backendValidate = (params) => {
|
||||
return ajax("/realtime-validations", {
|
||||
data: {
|
||||
type: this.get("type"),
|
||||
authenticity_token: getToken(),
|
||||
...params,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this.appEvents.on("custom-wizard:validate", this, this.checkIsValid);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this.appEvents.off("custom-wizard:validate", this, this.checkIsValid);
|
||||
},
|
||||
|
||||
checkIsValid() {
|
||||
this.set("isValid", this.validate());
|
||||
},
|
||||
});
|
36
assets/javascripts/wizard/components/wizard-similar-topics.js.es6
Normale Datei
36
assets/javascripts/wizard/components/wizard-similar-topics.js.es6
Normale Datei
|
@ -0,0 +1,36 @@
|
|||
import Component from "@ember/component";
|
||||
import { bind } from "@ember/runloop";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['wizard-similar-topics'],
|
||||
showTopics: true,
|
||||
|
||||
didInsertElement() {
|
||||
$(document).on("click", bind(this, this.documentClick));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
$(document).off("click", bind(this, this.documentClick));
|
||||
},
|
||||
|
||||
documentClick(e) {
|
||||
if (this._state == "destroying") return;
|
||||
let $target = $(e.target);
|
||||
|
||||
if (!$target.hasClass('show-topics')) {
|
||||
this.set('showTopics', false);
|
||||
}
|
||||
},
|
||||
|
||||
@observes('topics')
|
||||
toggleShowWhenTopicsChange() {
|
||||
this.set('showTopics', true);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleShowTopics() {
|
||||
this.set('showTopics', true);
|
||||
}
|
||||
}
|
||||
})
|
22
assets/javascripts/wizard/helpers/date-node.js.es6
Normale Datei
22
assets/javascripts/wizard/helpers/date-node.js.es6
Normale Datei
|
@ -0,0 +1,22 @@
|
|||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
import { longDate, number, relativeAge } from "discourse/lib/formatter";
|
||||
|
||||
export default registerUnbound("date-node", function (dt) {
|
||||
if (typeof dt === "string") {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
if (dt) {
|
||||
const attributes = {
|
||||
title: longDate(dt),
|
||||
"data-time": dt.getTime(),
|
||||
"data-format": "tiny",
|
||||
};
|
||||
|
||||
const finalString = `<span class="relative-date" title="${
|
||||
attributes["title"]
|
||||
}" data-time="${attributes["data-time"]}" data-format="${
|
||||
attributes["data-format"]
|
||||
}">${relativeAge(dt)}</span>`;
|
||||
return new Handlebars.SafeString(finalString);
|
||||
}
|
||||
});
|
13
assets/javascripts/wizard/templates/components/field-validators.hbs
Normale Datei
13
assets/javascripts/wizard/templates/components/field-validators.hbs
Normale Datei
|
@ -0,0 +1,13 @@
|
|||
{{#if field.validations}}
|
||||
{{#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 |type validation|}}
|
||||
{{component validation.component field=field type=type validation=validation}}
|
||||
{{/each-in}}
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
|
@ -0,0 +1,12 @@
|
|||
{{#if loading}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.loading'}}</label>
|
||||
{{else if showSimilarTopics}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.results'}}</label>
|
||||
{{wizard-similar-topics topics=similarTopics}}
|
||||
{{else if showNoSimilarTopics}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.no_results'}}</label>
|
||||
{{else if showDefault}}
|
||||
<label>{{i18n 'realtime_validations.similar_topics.default'}}</label>
|
||||
{{else}}
|
||||
<label></label>
|
||||
{{/if}}
|
5
assets/javascripts/wizard/templates/components/validator.hbs
Normale Datei
5
assets/javascripts/wizard/templates/components/validator.hbs
Normale Datei
|
@ -0,0 +1,5 @@
|
|||
{{#if isValid}}
|
||||
{{i18n validMessageKey}}
|
||||
{{else}}
|
||||
{{i18n invalidMessageKey}}
|
||||
{{/if}}
|
|
@ -10,11 +10,13 @@
|
|||
<div class='field-description'>{{cookedDescription}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if inputComponentName}}
|
||||
<div class='input-area'>
|
||||
{{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#field-validators field=field as |validators|}}
|
||||
{{#if inputComponentName}}
|
||||
<div class='input-area'>
|
||||
{{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard }}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/field-validators}}
|
||||
|
||||
{{#if field.char_counter}}
|
||||
{{#if textType}}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<a href="{{topic.url}}" target="_blank">
|
||||
<span class="title">{{html-safe topic.fancy_title}}</span>
|
||||
<div class="blurb">{{date-node topic.created_at}} - {{html-safe topic.blurb}}</div>
|
||||
</a>
|
|
@ -0,0 +1,11 @@
|
|||
{{#if showTopics}}
|
||||
<ul>
|
||||
{{#each topics as |topic|}}
|
||||
<li>{{wizard-similar-topic topic=topic}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<a class="show-topics" {{action "toggleShowTopics"}}>
|
||||
{{i18n 'realtime_validations.similar_topics.show'}}
|
||||
</a>
|
||||
{{/if}}
|
|
@ -642,4 +642,32 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.realtime-validations > ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
> li {
|
||||
background-color: var(--primary-low);
|
||||
padding: 1em;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.validation-container {
|
||||
display: flex;
|
||||
padding: 1em 0;
|
||||
|
||||
.validation-section {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard.category-selector {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
|
|
@ -155,4 +155,8 @@
|
|||
.select-kit.combo-box.group-dropdown {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.text-field input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
40
assets/stylesheets/wizard/custom/validators.scss
Normale Datei
40
assets/stylesheets/wizard/custom/validators.scss
Normale Datei
|
@ -0,0 +1,40 @@
|
|||
.similar-topics-validator {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-similar-topics {
|
||||
margin-left: 3px;
|
||||
|
||||
ul {
|
||||
background-color: var(--tertiary-low);
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 25px;
|
||||
box-shadow: shadow("dropdown");
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
|
||||
.title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.blurb {
|
||||
margin-left: 0.5em;
|
||||
color: var(--primary-high);
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
}
|
||||
|
||||
.show-topics {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -176,6 +176,15 @@ en:
|
|||
date_time_format:
|
||||
label: "Format"
|
||||
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
|
||||
validations:
|
||||
header: "Realtime Validations"
|
||||
enabled: "Enabled"
|
||||
similar_topics: "Similar Topics"
|
||||
position: "Position"
|
||||
above: "Above"
|
||||
below: "Below"
|
||||
categories: "Categories"
|
||||
date_after: "Date After"
|
||||
|
||||
type:
|
||||
text: "Text"
|
||||
|
@ -511,3 +520,11 @@ en:
|
|||
yourself_confirm:
|
||||
title: "Did you forget to add recipients?"
|
||||
body: "Right now this message is only being sent to yourself!"
|
||||
|
||||
realtime_validations:
|
||||
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..."
|
||||
show: "show"
|
||||
|
|
|
@ -9,6 +9,7 @@ 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'
|
||||
|
|
|
@ -8,6 +8,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
|
|||
each_serializer: CustomWizard::BasicWizardSerializer
|
||||
),
|
||||
field_types: CustomWizard::Field.types,
|
||||
realtime_validations: CustomWizard::RealtimeValidation.types,
|
||||
custom_fields: custom_field_list
|
||||
)
|
||||
end
|
||||
|
@ -63,7 +64,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
|
|||
output: [],
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
def save_wizard_params
|
||||
params.require(:wizard).permit(
|
||||
:id,
|
||||
|
@ -104,7 +105,8 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
|
|||
:limit,
|
||||
:property,
|
||||
prefill: mapped_params,
|
||||
content: mapped_params
|
||||
content: mapped_params,
|
||||
validations: {},
|
||||
]
|
||||
],
|
||||
actions: [
|
||||
|
|
18
controllers/custom_wizard/realtime_validations.rb
Normale Datei
18
controllers/custom_wizard/realtime_validations.rb
Normale Datei
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomWizard::RealtimeValidationsController < ::ApplicationController
|
||||
def validate
|
||||
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
|
||||
|
||||
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
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"result": {
|
||||
"covered_percent": 89.45
|
||||
"line": 89.57
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ module CustomWizardFieldExtension
|
|||
:description,
|
||||
:image,
|
||||
:key,
|
||||
:validations,
|
||||
:min_length,
|
||||
:max_length,
|
||||
:char_counter,
|
||||
|
@ -20,6 +21,7 @@ module CustomWizardFieldExtension
|
|||
@description = attrs[:description]
|
||||
@image = attrs[:image]
|
||||
@key = attrs[:key]
|
||||
@validations = attrs[:validations]
|
||||
@min_length = attrs[:min_length]
|
||||
@max_length = attrs[:max_length]
|
||||
@char_counter = attrs[:char_counter]
|
||||
|
|
|
@ -158,6 +158,7 @@ class CustomWizard::Builder
|
|||
params[:description] = field_template['description'] if field_template['description']
|
||||
params[:image] = field_template['image'] if field_template['image']
|
||||
params[:key] = field_template['key'] if field_template['key']
|
||||
params[:validations] = field_template['validations'] if field_template['validations']
|
||||
params[:min_length] = field_template['min_length'] if field_template['min_length']
|
||||
params[:max_length] = field_template['max_length'] if field_template['max_length']
|
||||
params[:char_counter] = field_template['char_counter'] if field_template['char_counter']
|
||||
|
|
|
@ -5,7 +5,8 @@ class CustomWizard::Field
|
|||
min_length: nil,
|
||||
max_length: nil,
|
||||
prefill: nil,
|
||||
char_counter: nil
|
||||
char_counter: nil,
|
||||
validations: nil
|
||||
},
|
||||
textarea: {
|
||||
min_length: nil,
|
||||
|
|
14
lib/custom_wizard/realtime_validation.rb
Normale Datei
14
lib/custom_wizard/realtime_validation.rb
Normale Datei
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomWizard::RealtimeValidation
|
||||
cattr_accessor :types
|
||||
|
||||
@@types ||= {
|
||||
similar_topics: {
|
||||
types: [:text],
|
||||
component: "similar-topics-validator",
|
||||
backend: true,
|
||||
required_params: []
|
||||
}
|
||||
}
|
||||
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
|
|
@ -47,6 +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_validations.rb
|
||||
../jobs/clear_after_time_wizard.rb
|
||||
../jobs/refresh_api_access_token.rb
|
||||
../jobs/set_after_time_wizard.rb
|
||||
|
@ -58,6 +59,9 @@ after_initialize do
|
|||
../lib/custom_wizard/cache.rb
|
||||
../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
|
||||
|
@ -79,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
|
|
@ -8,6 +8,7 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
|
|||
:limit,
|
||||
:property,
|
||||
:content,
|
||||
:validations,
|
||||
:max_length,
|
||||
:char_counter,
|
||||
:number
|
||||
|
@ -58,6 +59,17 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
|
|||
object.choices.present?
|
||||
end
|
||||
|
||||
def validations
|
||||
validations = {}
|
||||
object.validations&.each do |type, props|
|
||||
next unless props["status"]
|
||||
validations[props["position"]] ||= {}
|
||||
validations[props["position"]][type] = props.merge CustomWizard::RealtimeValidation.types[type.to_sym]
|
||||
end
|
||||
|
||||
validations
|
||||
end
|
||||
|
||||
def max_length
|
||||
object.max_length
|
||||
end
|
||||
|
|
19
spec/components/custom_wizard/realtime_validation_spec.rb
Normale Datei
19
spec/components/custom_wizard/realtime_validation_spec.rb
Normale Datei
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../plugin_helper'
|
||||
|
||||
describe CustomWizard::RealtimeValidation do
|
||||
validation_names = CustomWizard::RealtimeValidation.types.keys
|
||||
|
||||
validation_names.each do |name|
|
||||
klass_str = "CustomWizard::RealtimeValidation::#{name.to_s.camelize}"
|
||||
|
||||
it "ensure class for validation: #{name} exists" do
|
||||
expect(klass_str.safe_constantize).not_to be_nil
|
||||
end
|
||||
|
||||
it "#{klass_str} has a perform() method" do
|
||||
expect(klass_str.safe_constantize.instance_methods).to include(:perform)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../../plugin_helper'
|
||||
|
||||
describe ::CustomWizard::RealtimeValidation::SimilarTopics do
|
||||
let(:post) { create_post(title: "matching similar topic") }
|
||||
let(:topic) { post.topic }
|
||||
let(:user) { post.user }
|
||||
|
||||
let(:category) { Fabricate(:category) }
|
||||
let(:cat_post) { create_post(title: "matching similar topic slightly different", category: category) }
|
||||
let(:cat_topic) { cat_post.topic }
|
||||
let(:user) { cat_post.user }
|
||||
|
||||
before do
|
||||
SiteSetting.min_title_similar_length = 5
|
||||
Topic.stubs(:count_exceeds_minimum?).returns(true)
|
||||
SearchIndexer.enable
|
||||
end
|
||||
|
||||
it "fetches similar topics" do
|
||||
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
|
||||
result = validation.perform({ title: topic.title.chars.take(10).join })
|
||||
expect(result.items.length).to eq(2)
|
||||
end
|
||||
|
||||
it "filters topics based on category" do
|
||||
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
|
||||
result = validation.perform({ title: "matching similar", categories: [category.id.to_s] })
|
||||
expect(result.items.length).to eq(1)
|
||||
end
|
||||
|
||||
it "filters topics based on created date" do
|
||||
topic.update!(created_at: 1.day.ago)
|
||||
cat_topic.update!(created_at: 2.days.ago)
|
||||
|
||||
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
|
||||
result = validation.perform({ title: "matching similar", date_after: 1.day.ago.to_date.to_s })
|
||||
expect(result.items.length).to eq(1)
|
||||
end
|
||||
end
|
71
spec/requests/custom_wizard/realtime_validations_spec.rb
Normale Datei
71
spec/requests/custom_wizard/realtime_validations_spec.rb
Normale Datei
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../plugin_helper'
|
||||
|
||||
describe CustomWizard::RealtimeValidationsController do
|
||||
|
||||
fab!(:validation_type) { "test_stub" }
|
||||
fab!(:validation_type_stub) {
|
||||
{
|
||||
types: [:text],
|
||||
component: "similar-topics-validator",
|
||||
backend: true,
|
||||
required_params: []
|
||||
}
|
||||
}
|
||||
|
||||
before(:all) do
|
||||
sign_in(Fabricate(:user))
|
||||
CustomWizard::RealtimeValidation.types = { test_stub: validation_type_stub }
|
||||
|
||||
class CustomWizard::RealtimeValidation::TestStub
|
||||
attr_accessor :user
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def perform(params)
|
||||
result = CustomWizard::RealtimeValidation::Result.new(:test_stub)
|
||||
result.items = ["hello", "world"]
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
class ::CustomWizard::RealtimeValidation::TestStubSerializer < ApplicationSerializer
|
||||
attributes :item
|
||||
|
||||
def item
|
||||
object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gives the correct response for a given type" do
|
||||
get '/realtime-validations.json', params: { type: validation_type }
|
||||
expect(response.status).to eq(200)
|
||||
expected_response = [
|
||||
{ "item" => "hello" },
|
||||
{ "item" => "world" }
|
||||
]
|
||||
expect(JSON.parse(response.body)).to eq(expected_response)
|
||||
end
|
||||
|
||||
it "gives 400 error when no type is passed" do
|
||||
get '/realtime-validations.json'
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it "gives 400 error when a required additional param is missing" do
|
||||
CustomWizard::RealtimeValidation.types[:test_stub][:required_params] = [:test1]
|
||||
get '/realtime-validations.json', params: { type: validation_type }
|
||||
expect(response.status).to eq(400)
|
||||
# the addition is only relevant to this test, so getting rid of it
|
||||
CustomWizard::RealtimeValidation.types[:test_stub][:required_params] = []
|
||||
end
|
||||
|
||||
it "gives 500 response code when a non existant type is passed" do
|
||||
get '/realtime-validations.json', params: { type: "random_type" }
|
||||
expect(response.status).to eq(500)
|
||||
end
|
||||
end
|
|
@ -33,6 +33,6 @@ describe CustomWizard::FieldSerializer do
|
|||
).as_json
|
||||
expect(json_array[0][:format]).to eq("YYYY-MM-DD")
|
||||
expect(json_array[5][:file_types]).to eq(".jpg,.png")
|
||||
expect(json_array[4][:number]).to eq("5")
|
||||
expect(json_array[4][:number]).to eq(5)
|
||||
end
|
||||
end
|
Laden …
In neuem Issue referenzieren