1
0
Fork 0

merged 'main' and resolved conflicts

Dieser Commit ist enthalten in:
Faizaan Gagan 2022-01-31 09:47:12 +05:30
Commit 5d882d69a2
26 geänderte Dateien mit 242 neuen und 352 gelöschten Zeilen

Datei anzeigen

@ -1,2 +1,2 @@
2.7.8: e07a57e398b6b1676ab42a7e34467556fca5416b
2.5.1: bb85b3a0d2c0ab6b59bcb405731c39089ec6731c
2.7.99: e07a57e398b6b1676ab42a7e34467556fca5416b
2.5.1: bb85b3a0d2c0ab6b59bcb405731c39089ec6731c

Datei anzeigen

@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Set up ruby
uses: ruby/setup-ruby@v1

Datei anzeigen

@ -0,0 +1,16 @@
<h3>{{i18n "admin.wizard.category_settings.custom_wizard.title"}}</h3>
<section class="field new-topic-wizard">
<label for="new-topic-wizard">
{{i18n "admin.wizard.category_settings.custom_wizard.create_topic_wizard"}}
</label>
<div class="controls">
{{combo-box
value=wizardListVal
content=wizardList
onChange=(action "changeWizard")
options=(hash
none="admin.wizard.select"
)}}
</div>
</section>

Datei anzeigen

@ -0,0 +1,24 @@
import CustomWizard from "../../models/custom-wizard";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default {
setupComponent(attrs, component) {
CustomWizard.all()
.then((result) => {
component.set("wizardList", result);
})
.catch(popupAjaxError);
component.set(
"wizardListVal",
attrs?.category?.custom_fields?.create_topic_wizard
);
},
actions: {
changeWizard(wizard) {
this.set("wizardListVal", wizard);
this.set("category.custom_fields.create_topic_wizard", wizard);
},
},
};

Datei anzeigen

@ -1,4 +1,6 @@
import DiscourseURL from "discourse/lib/url";
import { withPluginApi } from "discourse/lib/plugin-api";
import getUrl from "discourse-common/lib/get-url";
export default {
name: "custom-wizard-edits",
@ -16,5 +18,23 @@ export default {
}
return existing.apply(this, [path, opts]);
};
withPluginApi("0.8.7", (api) => {
api.modifyClass("component:d-navigation", {
pluginId: "custom-wizard",
actions: {
clickCreateTopicButton() {
let createTopicWizard = this.get(
"category.custom_fields.create_topic_wizard"
);
if (createTopicWizard) {
window.location.href = getUrl(`/w/${createTopicWizard}`);
} else {
this._super();
}
},
},
});
});
},
};

Datei anzeigen

@ -1,6 +1,7 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import EmberObject from "@ember/object";
import getURL from "discourse-common/lib/get-url";
const CustomWizardManager = EmberObject.extend();
@ -17,7 +18,7 @@ CustomWizardManager.reopenClass({
},
export(wizardIds) {
let url = `${Discourse.BaseUrl}/${basePath}/export?`;
let url = `${getURL()}/${basePath}/export?`;
wizardIds.forEach((wizardId, index) => {
let step = "wizard_ids[]=" + wizardId;

Datei anzeigen

@ -40,12 +40,13 @@
<label>{{i18n "admin.wizard.field.image"}}</label>
</div>
<div class="setting-value">
{{image-uploader
{{uppy-image-uploader
imageUrl=field.image
onUploadDone=(action "imageUploadDone")
onUploadDeleted=(action "imageUploadDeleted")
type="wizard-step"
class="no-repeat contain-image"}}
class="no-repeat contain-image"
id=(concat "wizard-field-" field.id "-image-upload")}}
</div>
</div>
@ -127,13 +128,13 @@
{{/if}}
{{#if isComposerPreview}}
<div class="setting">
<div class="setting full">
<div class="setting-label">
<label>{{i18n "admin.wizard.field.preview_template"}}</label>
</div>
<div class="setting-value">
{{textarea name="preview-template" value=field.preview_template}}
{{textarea name="preview-template" value=field.preview_template class="preview-template"}}
</div>
</div>
{{/if}}

Datei anzeigen

@ -14,12 +14,13 @@
<label>{{i18n "admin.wizard.step.banner"}}</label>
</div>
<div class="setting-value">
{{image-uploader
{{uppy-image-uploader
imageUrl=step.banner
onUploadDone=(action "bannerUploadDone")
onUploadDeleted=(action "bannerUploadDeleted")
type="wizard-banner"
class="no-repeat contain-image"}}
class="no-repeat contain-image"
id=(concat "wizard-step-" step.id "-banner-upload")}}
</div>
</div>

Datei anzeigen

@ -1,11 +1,5 @@
//= require_tree_discourse discourse/app/lib
//= require_tree_discourse discourse/app/mixins
//
//= require discourse/app/mixins/singleton
//= require discourse/app/mixins/upload
//= require discourse/app/mixins/composer-upload
//= require discourse/app/mixins/textarea-text-manipulation
//= require discourse/app/adapters/rest
@ -25,6 +19,7 @@
//= require discourse/app/services/app-events
//= require discourse/app/services/emoji-store
//= require discourse/app/services/store
//= require discourse/app/components/user-selector
//= require discourse/app/components/conditional-loading-spinner
@ -40,6 +35,7 @@
//= require discourse/app/components/date-time-input
//= require discourse/app/components/text-field
//= require discourse/app/components/d-textarea
//= require discourse/app/components/popup-input-tip
//= require discourse/app/templates/components/conditional-loading-spinner
//= require discourse/app/templates/components/d-button

Datei anzeigen

@ -4,24 +4,14 @@ import {
on,
} from "discourse-common/utils/decorators";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { next, scheduleOnce, throttle } from "@ember/runloop";
import { scheduleOnce } from "@ember/runloop";
import { caretPosition, inCodeBlock } from "discourse/lib/utilities";
import highlightSyntax from "discourse/lib/highlight-syntax";
import { getToken } from "wizard/lib/ajax";
import {
displayErrorForUpload,
getUploadMarkdown,
uploadIcon,
validateUploadedFiles,
} from "discourse/lib/uploads";
import { cacheShortUploadUrl } from "pretty-text/upload-short-url";
import { alias } from "@ember/object/computed";
import WizardI18n from "../lib/wizard-i18n";
import Site from "../models/site";
import { uploadIcon } from "discourse/lib/uploads";
import { dasherize } from "@ember/string";
const uploadMarkdownResolvers = [];
const uploadHandlers = [];
export default ComposerEditor.extend({
classNameBindings: ["fieldClass"],
allowUpload: true,
@ -35,11 +25,11 @@ export default ComposerEditor.extend({
popupMenuOptions: [],
draftStatus: "null",
replyPlaceholder: alias("field.placeholder"),
uploadingFieldId: null,
@on("didInsertElement")
_composerEditorInit() {
const $input = $(this.element.querySelector(".d-editor-input"));
const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
@ -81,15 +71,52 @@ export default ComposerEditor.extend({
});
}
if (this._enableAdvancedEditorPreviewSync()) {
this._initInputPreviewSync($input, $preview);
} else {
$input.on("scroll", () =>
throttle(this, this._syncEditorAndPreviewScroll, $input, $preview, 20)
);
}
$input.on("scroll", this._throttledSyncEditorAndPreviewScroll);
this._bindUploadTarget();
const wizardEventNames = ["insert-text", "replace-text"];
const eventPrefix = this.eventPrefix;
const session = this.get("session");
this.appEvents.reopen({
trigger(name, ...args) {
let eventParts = name.split(":");
let currentEventPrefix = eventParts[0];
let currentEventName = eventParts[1];
if (
currentEventPrefix !== "wizard-editor" &&
wizardEventNames.some((wen) => wen === currentEventName)
) {
let wizardName = name.replace(eventPrefix, "wizard-editor");
if (currentEventName === "insert-text") {
args = {
text: args[0],
};
}
if (currentEventName === "replace-text") {
args = {
oldVal: args[0],
newVal: args[1],
};
}
let wizardArgs = Object.assign(
{},
{
fieldId: session.get("uploadingFieldId"),
},
args
);
return this._super(wizardName, wizardArgs);
} else {
return this._super(name, ...args);
}
},
});
},
@discourseComputed("field.id")
fileUploadElementId(fieldId) {
return `file-uploader-${dasherize(fieldId)}`;
},
@discourseComputed
@ -105,192 +132,6 @@ export default ComposerEditor.extend({
return uploadIcon(false, this.siteSettings);
},
_setUploadPlaceholderSend() {
if (!this.composer.get("reply")) {
this.composer.set("reply", "");
}
this._super(...arguments);
},
_bindUploadTarget() {
this._super(...arguments);
const $element = $(this.element);
// adding dropZone property post initialization
$element.fileupload("option", "dropZone", $element);
$element.off("fileuploadsubmit");
$element.on("fileuploadsubmit", (e, data) => {
const max = this.siteSettings.simultaneous_uploads;
// Limit the number of simultaneous uploads
if (max > 0 && data.files.length > max) {
bootbox.alert(
WizardI18n("post.errors.too_many_dragged_and_dropped_files", { max })
);
return false;
}
// Look for a matching file upload handler contributed from a plugin
const matcher = (handler) => {
const ext = handler.extensions.join("|");
const regex = new RegExp(`\\.(${ext})$`, "i");
return regex.test(data.files[0].name);
};
const matchingHandler = uploadHandlers.find(matcher);
if (data.files.length === 1 && matchingHandler) {
if (!matchingHandler.method(data.files[0], this)) {
return false;
}
}
// If no plugin, continue as normal
const isPrivateMessage = this.get("composer.privateMessage");
data.formData = { type: "composer" };
data.formData.authenticity_token = getToken();
if (isPrivateMessage) {
data.formData.for_private_message = true;
}
if (this._pasted) {
data.formData.pasted = true;
}
const opts = {
user: this.currentUser,
siteSettings: this.siteSettings,
isPrivateMessage,
allowStaffToUploadAnyFileInPm: this.siteSettings
.allow_staff_to_upload_any_file_in_pm,
};
const isUploading = validateUploadedFiles(data.files, opts);
this.setProperties({ uploadProgress: 0, isUploading });
return isUploading;
});
$element.on("fileuploadprogressall", (e, data) => {
this.set(
"uploadProgress",
parseInt((data.loaded / data.total) * 100, 10)
);
});
$element.on("fileuploadfail", (e, data) => {
this._setUploadPlaceholderDone(data);
this._resetUpload(true);
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data, this.siteSettings);
}
});
$element.on("fileuploadsend", (e, data) => {
this._pasted = false;
this._validUploads++;
this._setUploadPlaceholderSend(data);
this.appEvents.trigger("wizard-editor:insert-text", {
fieldId: this.field.id,
text: this.uploadPlaceholder,
});
if (data.xhr && data.originalFiles.length === 1) {
this.set("isCancellable", true);
this._xhr = data.xhr();
}
});
$element.on("fileuploaddone", (e, data) => {
let upload = data.result;
this._setUploadPlaceholderDone(data);
if (!this._xhr || !this._xhr._userCancelled) {
const markdown = uploadMarkdownResolvers.reduce(
(md, resolver) => resolver(upload) || md,
getUploadMarkdown(upload)
);
cacheShortUploadUrl(upload.short_url, upload);
this.appEvents.trigger("wizard-editor:replace-text", {
fieldId: this.field.id,
oldVal: this.uploadPlaceholder.trim(),
newVal: markdown,
});
this._resetUpload(false);
} else {
this._resetUpload(true);
}
});
},
_resetUpload(removePlaceholder) {
next(() => {
if (this._validUploads > 0) {
this._validUploads--;
}
if (this._validUploads === 0) {
this.setProperties({
uploadProgress: 0,
isUploading: false,
isCancellable: false,
});
}
if (removePlaceholder) {
this.appEvents.trigger("wizard-editor:replace-text", {
fieldId: this.field.id,
oldVal: this.uploadPlaceholder,
newVal: "",
});
}
this._resetUploadFilenamePlaceholder();
});
},
_registerImageScaleButtonClick($preview) {
const imageScaleRegex = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g;
$preview.off("click", ".scale-btn").on("click", ".scale-btn", (e) => {
const index = parseInt($(e.target).parent().attr("data-image-index"), 10);
const scale = e.target.attributes["data-scale"].value;
const matchingPlaceholder = this.get("composer.reply").match(
imageScaleRegex
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
if (match) {
const replacement = match.replace(
imageScaleRegex,
`![$1|$2, ${scale}%$4]($5)`
);
this.appEvents.trigger("wizard-editor:replace-text", {
fieldId: this.field.id,
oldVal: matchingPlaceholder[index],
newVal: replacement,
options: {
regex: imageScaleRegex,
index,
},
});
}
}
e.preventDefault();
return;
});
},
click(e) {
if ($(e.target).hasClass("wizard-composer-hyperlink")) {
this.set("showHyperlinkBox", false);
@ -372,7 +213,8 @@ export default ComposerEditor.extend({
},
showUploadModal() {
$(this.element.querySelector(".wizard-composer-upload")).trigger("click");
this.session.set("uploadingFieldId", this.field.id);
document.getElementById(this.fileUploadElementId).click();
},
},
});

Datei anzeigen

@ -1,8 +1,5 @@
import DateInput from "discourse/components/date-input";
import loadScript from "discourse/lib/load-script";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
/* global Pikaday:true */
export default DateInput.extend({
useNativePicker: false,
@ -11,32 +8,9 @@ export default DateInput.extend({
placeholder() {
return this.format;
},
_loadPikadayPicker(container) {
return loadScript("/javascripts/pikaday.js").then(() => {
let defaultOptions = {
field: this.element.querySelector(".date-picker"),
container: container || this.element.querySelector(".picker-container"),
bound: container === null,
format: this.format,
firstDay: 1,
i18n: {
previousMonth: I18n.t("dates.previous_month"),
nextMonth: I18n.t("dates.next_month"),
months: moment.months(),
weekdays: moment.weekdays(),
weekdaysShort: moment.weekdaysShort(),
},
onSelect: (date) => this._handleSelection(date),
};
if (this.relativeDate) {
defaultOptions = Object.assign({}, defaultOptions, {
minDate: moment(this.relativeDate).toDate(),
});
}
return new Pikaday(Object.assign({}, defaultOptions, this._opts()));
});
_opts() {
return {
format: this.format || "LL",
};
},
});

Datei anzeigen

@ -16,7 +16,7 @@ export default Ember.Component.extend({
"composer",
EmberObject.create({
loading: false,
reply: this.get("field.value"),
reply: this.get("field.value") || "",
})
);
},

Datei anzeigen

@ -1,63 +1,24 @@
import getUrl from "discourse-common/lib/get-url";
import { getToken } from "wizard/lib/ajax";
import WizardI18n from "../lib/wizard-i18n";
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import Component from "@ember/component";
import { computed } from "@ember/object";
export default Ember.Component.extend({
export default Component.extend(UppyUploadMixin, {
classNames: ["wizard-field-upload"],
classNameBindings: ["isImage"],
uploading: false,
isImage: false,
type: computed(function () {
return `wizard_${this.field.id}`;
}),
isImage: computed("field.value.extension", function () {
return (
this.field.value &&
this.siteSettings.wizard_recognised_image_upload_formats
.split("|")
.includes(this.field.value.extension)
);
}),
didInsertElement() {
this._super();
const $upload = $(this.element);
const id = this.get("field.id");
$upload.fileupload({
url: getUrl("/uploads.json"),
formData: {
synchronous: true,
type: `wizard_${id}`,
authenticity_token: getToken(),
},
dataType: "json",
dropZone: $upload,
});
$upload.on("fileuploadsubmit", () => this.set("uploading", true));
$upload.on("fileuploaddone", (e, response) => {
this.setProperties({
"field.value": response.result,
uploading: false,
});
if (
Discourse.SiteSettings.wizard_recognised_image_upload_formats
.split("|")
.includes(response.result.extension)
) {
this.setProperties({
isImage: true,
});
}
});
$upload.on("fileuploadfail", (e, response) => {
let message = WizardI18n("wizard.upload_error");
if (response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) {
message = response.jqXHR.responseJSON.errors.join("\n");
}
window.swal({
customClass: "wizard-warning",
title: "",
text: message,
type: "warning",
confirmButtonColor: "#6699ff",
});
this.set("uploading", false);
});
uploadDone(upload) {
this.set("field.value", upload);
},
});

Datei anzeigen

@ -9,7 +9,7 @@ export default {
const Router = requirejs("wizard/router").default;
const ApplicationRoute = requirejs("wizard/routes/application").default;
const getUrl = requirejs("discourse-common/lib/get-url").default;
const Store = requirejs("discourse/models/store").default;
const Store = requirejs("discourse/services/store").default;
const registerRawHelpers = requirejs(
"discourse-common/lib/raw-handlebars-helpers"
).registerRawHelpers;
@ -111,8 +111,16 @@ export default {
model() {},
});
$.ajaxPrefilter(function (_, __, jqXHR) {
jqXHR.setRequestHeader("X-CSRF-Token", getToken());
// Add a CSRF token to all AJAX requests
let token = getToken();
session.set("csrfToken", token);
let callbacks = $.Callbacks();
$.ajaxPrefilter(callbacks.fire);
callbacks.add(function (options, originalOptions, xhr) {
if (!options.crossDomain) {
xhr.setRequestHeader("X-CSRF-Token", session.get("csrfToken"));
}
});
},
};

Datei anzeigen

@ -2,6 +2,7 @@ import { default as computed } from "discourse-common/utils/decorators";
import getUrl from "discourse-common/lib/get-url";
import WizardField from "wizard/models/wizard-field";
import { ajax } from "wizard/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Step from "wizard/models/step";
import EmberObject from "@ember/object";
import Site from "./site";
@ -24,15 +25,19 @@ const CustomWizard = EmberObject.extend({
CustomWizard.reopenClass({
skip(wizardId) {
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" }).then((result) => {
CustomWizard.finished(result);
});
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" })
.then((result) => {
CustomWizard.finished(result);
})
.catch(popupAjaxError);
},
restart(wizardId) {
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" }).then(() => {
window.location.href = `/w/${wizardId}`;
});
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" })
.then(() => {
window.location.href = `/w/${wizardId}`;
})
.catch(popupAjaxError);
},
finished(result) {

Datei anzeigen

@ -16,24 +16,10 @@
disabled=disableTextarea
outletArgs=(hash composer=composer editorType="composer")}}
{{input
class="wizard-composer-upload hidden-upload-field"
disabled=isUploading
<input
type="file"
accept=allowedFileTypes
multiple=true}}
{{#if showHyperlinkBox}}
{{wizard-composer-hyperlink
addLink=(action "addLink")
hideBox=(action "hideBox")}}
{{/if}}
{{#if isUploading}}
<div id="file-uploading">
{{loading-spinner size="small"}}<span>{{wizard-i18n "upload_selector.uploading"}} {{uploadProgress}}%</span>
{{#if isCancellable}}
<a href id="cancel-file-upload" {{action "cancelUpload"}}>{{d-icon "times"}}</a>
{{/if}}
</div>
{{/if}}
id={{fileUploadElementId}}
class="wizard-composer-upload"
accept={{allowedFileTypes}}
multiple
>

Datei anzeigen

@ -6,7 +6,7 @@
{{d-icon "upload"}}
{{/if}}
<input disabled={{uploading}} type="file" accept={{field.file_types}} style="visibility: hidden; position: absolute;" >
<input disabled={{uploading}} class="hidden-upload-field" type="file" accept={{field.file_types}} style="visibility: hidden; position: absolute;" >
</label>
{{#if field.value}}

Datei anzeigen

@ -317,6 +317,10 @@
font-size: 1em;
}
}
.preview-template {
min-height: 150px;
}
}
&.full,

Datei anzeigen

@ -72,6 +72,10 @@
display: flex;
flex-direction: column;
margin-left: 0;
overflow: auto;
cursor: default;
margin-top: unset;
padding-top: unset;
}
.d-editor-button-bar {
@ -107,11 +111,6 @@
display: inline-block;
}
.d-editor-preview-wrapper {
overflow: auto;
cursor: default;
}
.d-editor-input,
.d-editor-preview {
box-sizing: border-box;

Datei anzeigen

@ -6,6 +6,7 @@
@import "common/components/buttons";
@import "common/d-editor";
@import "desktop/modal";
@import "common/input_tip";
@import "custom/base";
@import "custom/wizard";

Datei anzeigen

@ -60,7 +60,11 @@ en:
select_type: "Select a type"
condition: "Condition"
index: "Index"
category_settings:
custom_wizard:
title: "Custom Wizard"
create_topic_wizard: "Select a wizard to replace the new topic composer in this category."
message:
wizard:
select: "Select a wizard, or create a new one"
@ -179,7 +183,7 @@ en:
char_counter_placeholder: "Display Character Counter"
field_placeholder: "Field Placeholder"
file_types: "File Types"
preview_template: "Preview Template"
preview_template: "Template"
limit: "Limit"
property: "Property"
prefill: "Prefill"

Datei anzeigen

@ -5,6 +5,7 @@ class CustomWizard::WizardController < ::ApplicationController
layout 'wizard'
before_action :ensure_plugin_enabled
before_action :ensure_logged_in, only: [:skip]
helper_method :wizard_page_title
helper_method :wizard_theme_id
helper_method :wizard_theme_lookup

6
crowdin.yml Normale Datei
Datei anzeigen

@ -0,0 +1,6 @@
pull_request_title: 'I18n: Update translations'
files:
- source: /config/locales/client.en.yml
translation: /config/locales/client.%two_letters_code%.yml
- source: /config/locales/server.en.yml
translation: /config/locales/server.%two_letters_code%.yml

Datei anzeigen

@ -18,8 +18,8 @@ class CustomWizard::TemplateValidator
data[:steps].each do |step|
check_required(step, :step)
if data[:fields].present?
data[:fields].each do |field|
if step[:fields].present?
step[:fields].each do |field|
check_required(field, :field)
end
end

Datei anzeigen

@ -1,7 +1,7 @@
# frozen_string_literal: true
# name: discourse-custom-wizard
# about: Create custom wizards
# version: 1.15.3
# version: 1.16.4
# authors: Angus McLeod
# url: https://github.com/paviliondev/discourse-custom-wizard
# contact emails: angus@thepavilion.io
@ -117,6 +117,13 @@ after_initialize do
load File.expand_path(path, __FILE__)
end
# preloaded category custom fields
%w[
create_topic_wizard
].each do |custom_field|
Site.preloaded_category_custom_fields << custom_field
end
Liquid::Template.register_filter(::CustomWizard::LiquidFilter::FirstNonEmpty)
add_to_class(:topic, :wizard_id) do

Datei anzeigen

@ -45,4 +45,37 @@ describe CustomWizard::TemplateValidator do
CustomWizard::TemplateValidator.new(template).perform
).to eq(false)
end
context "steps" do
CustomWizard::TemplateValidator.required[:step].each do |attribute|
it "invalidates if \"#{attribute.to_s}\" is not present" do
template[:steps][0][attribute] = nil
expect(
CustomWizard::TemplateValidator.new(template).perform
).to eq(false)
end
end
end
context "fields" do
CustomWizard::TemplateValidator.required[:field].each do |attribute|
it "invalidates if \"#{attribute.to_s}\" is not present" do
template[:steps][0][:fields][0][attribute] = nil
expect(
CustomWizard::TemplateValidator.new(template).perform
).to eq(false)
end
end
end
context "actions" do
CustomWizard::TemplateValidator.required[:action].each do |attribute|
it "invalidates if \"#{attribute.to_s}\" is not present" do
template[:actions][0][attribute] = nil
expect(
CustomWizard::TemplateValidator.new(template).perform
).to eq(false)
end
end
end
end