Merge branch 'main' into stable
Dieser Commit ist enthalten in:
Commit
52fe6e2baa
60 geänderte Dateien mit 6199 neuen und 436 gelöschten Zeilen
|
@ -1 +1,2 @@
|
||||||
|
2.7.99: e07a57e398b6b1676ab42a7e34467556fca5416b
|
||||||
2.5.1: bb85b3a0d2c0ab6b59bcb405731c39089ec6731c
|
2.5.1: bb85b3a0d2c0ab6b59bcb405731c39089ec6731c
|
2
.github/workflows/plugin-linting.yml
gevendort
2
.github/workflows/plugin-linting.yml
gevendort
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 14
|
||||||
|
|
||||||
- name: Set up ruby
|
- name: Set up ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|
44
.github/workflows/plugin-metadata.yml
gevendort
Normale Datei
44
.github/workflows/plugin-metadata.yml
gevendort
Normale Datei
|
@ -0,0 +1,44 @@
|
||||||
|
name: Metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout head repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Store head version
|
||||||
|
run: |
|
||||||
|
sed -n -e 's/^.*version: /head_version=/p' plugin.rb >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout base repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: "${{ github.base_ref }}"
|
||||||
|
|
||||||
|
- name: Store base version
|
||||||
|
run: |
|
||||||
|
sed -n -e 's/^.*version: /base_version=/p' plugin.rb >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
|
||||||
|
- name: Install semver
|
||||||
|
run: npm install --include=dev
|
||||||
|
|
||||||
|
- name: Check version
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const semver = require('semver');
|
||||||
|
const { head_version, base_version } = process.env;
|
||||||
|
|
||||||
|
if (semver.lte(head_version, base_version)) {
|
||||||
|
core.setFailed("Head version is equal to or lower than base version.");
|
||||||
|
}
|
|
@ -81,7 +81,8 @@ export default Component.extend({
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
index = items.length;
|
let last_item = items[items.length - 1];
|
||||||
|
index = Number(last_item.id.split("_").pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
params.index = index;
|
params.index = index;
|
||||||
|
|
|
@ -24,6 +24,8 @@ const customFieldActionMap = {
|
||||||
user: ["update_profile"],
|
user: ["update_profile"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const values = ["present", "true", "false"];
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
classNameBindings: [":mapper-selector", "activeType"],
|
classNameBindings: [":mapper-selector", "activeType"],
|
||||||
|
|
||||||
|
@ -60,6 +62,9 @@ export default Component.extend({
|
||||||
showCustomField: computed("activeType", function () {
|
showCustomField: computed("activeType", function () {
|
||||||
return this.showInput("customField");
|
return this.showInput("customField");
|
||||||
}),
|
}),
|
||||||
|
showValue: computed("activeType", function () {
|
||||||
|
return this.showInput("value");
|
||||||
|
}),
|
||||||
textEnabled: computed("options.textSelection", "inputType", function () {
|
textEnabled: computed("options.textSelection", "inputType", function () {
|
||||||
return this.optionEnabled("textSelection");
|
return this.optionEnabled("textSelection");
|
||||||
}),
|
}),
|
||||||
|
@ -117,6 +122,9 @@ export default Component.extend({
|
||||||
listEnabled: computed("options.listSelection", "inputType", function () {
|
listEnabled: computed("options.listSelection", "inputType", function () {
|
||||||
return this.optionEnabled("listSelection");
|
return this.optionEnabled("listSelection");
|
||||||
}),
|
}),
|
||||||
|
valueEnabled: computed("connector", function () {
|
||||||
|
return this.connector === "is";
|
||||||
|
}),
|
||||||
|
|
||||||
groups: alias("site.groups"),
|
groups: alias("site.groups"),
|
||||||
categories: alias("site.categories"),
|
categories: alias("site.categories"),
|
||||||
|
@ -125,7 +133,8 @@ export default Component.extend({
|
||||||
"showWizardAction",
|
"showWizardAction",
|
||||||
"showUserField",
|
"showUserField",
|
||||||
"showUserFieldOptions",
|
"showUserFieldOptions",
|
||||||
"showCustomField"
|
"showCustomField",
|
||||||
|
"showValue"
|
||||||
),
|
),
|
||||||
showMultiSelect: or("showCategory", "showGroup"),
|
showMultiSelect: or("showCategory", "showGroup"),
|
||||||
hasTypes: gt("selectorTypes.length", 1),
|
hasTypes: gt("selectorTypes.length", 1),
|
||||||
|
@ -157,7 +166,7 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed("connector")
|
||||||
selectorTypes() {
|
selectorTypes() {
|
||||||
return selectionTypes
|
return selectionTypes
|
||||||
.filter((type) => this[`${type}Enabled`])
|
.filter((type) => this[`${type}Enabled`])
|
||||||
|
@ -268,6 +277,13 @@ export default Component.extend({
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeType === "value") {
|
||||||
|
content = values.map((value) => ({
|
||||||
|
id: value,
|
||||||
|
name: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -337,7 +353,7 @@ export default Component.extend({
|
||||||
resetActiveType() {
|
resetActiveType() {
|
||||||
this.set(
|
this.set(
|
||||||
"activeType",
|
"activeType",
|
||||||
defaultSelectionType(this.selectorType, this.options)
|
defaultSelectionType(this.selectorType, this.options, this.connector)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -58,6 +58,21 @@ export default Controller.extend({
|
||||||
}
|
}
|
||||||
return wizardFieldList(steps);
|
return wizardFieldList(steps);
|
||||||
},
|
},
|
||||||
|
getErrorMessage(result) {
|
||||||
|
if (result.backend_validation_error) {
|
||||||
|
return result.backend_validation_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorType = "failed";
|
||||||
|
let errorParams = {};
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
errorType = result.error.type;
|
||||||
|
errorParams = result.error.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return I18n.t(`admin.wizard.error.${errorType}`, errorParams);
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
|
@ -80,18 +95,7 @@ export default Controller.extend({
|
||||||
this.send("afterSave", result.wizard_id);
|
this.send("afterSave", result.wizard_id);
|
||||||
})
|
})
|
||||||
.catch((result) => {
|
.catch((result) => {
|
||||||
let errorType = "failed";
|
this.set("error", this.getErrorMessage(result));
|
||||||
let errorParams = {};
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
errorType = result.error.type;
|
|
||||||
errorParams = result.error.params;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set(
|
|
||||||
"error",
|
|
||||||
I18n.t(`admin.wizard.error.${errorType}`, errorParams)
|
|
||||||
);
|
|
||||||
|
|
||||||
later(() => this.set("error", null), 10000);
|
later(() => this.set("error", null), 10000);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import getUrl from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "custom-wizard-edits",
|
name: "custom-wizard-edits",
|
||||||
|
@ -16,5 +18,23 @@ export default {
|
||||||
}
|
}
|
||||||
return existing.apply(this, [path, opts]);
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -105,13 +105,18 @@ const selectionTypes = [
|
||||||
"tag",
|
"tag",
|
||||||
"user",
|
"user",
|
||||||
"customField",
|
"customField",
|
||||||
|
"value",
|
||||||
];
|
];
|
||||||
|
|
||||||
function defaultSelectionType(inputType, options = {}) {
|
function defaultSelectionType(inputType, options = {}, connector = null) {
|
||||||
if (options[`${inputType}DefaultSelection`]) {
|
if (options[`${inputType}DefaultSelection`]) {
|
||||||
return options[`${inputType}DefaultSelection`];
|
return options[`${inputType}DefaultSelection`];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connector === "is") {
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
|
||||||
let type = selectionTypes[0];
|
let type = selectionTypes[0];
|
||||||
|
|
||||||
for (let t of selectionTypes) {
|
for (let t of selectionTypes) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
const CustomWizardManager = EmberObject.extend();
|
const CustomWizardManager = EmberObject.extend();
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ CustomWizardManager.reopenClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
export(wizardIds) {
|
export(wizardIds) {
|
||||||
let url = `${Discourse.BaseUrl}/${basePath}/export?`;
|
let url = `${getURL()}/${basePath}/export?`;
|
||||||
|
|
||||||
wizardIds.forEach((wizardId, index) => {
|
wizardIds.forEach((wizardId, index) => {
|
||||||
let step = "wizard_ids[]=" + wizardId;
|
let step = "wizard_ids[]=" + wizardId;
|
||||||
|
|
|
@ -28,7 +28,7 @@ const CustomWizard = EmberObject.extend({
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.error) {
|
if (result.backend_validation_error) {
|
||||||
reject(result);
|
reject(result);
|
||||||
} else {
|
} else {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
|
|
@ -40,12 +40,13 @@
|
||||||
<label>{{i18n "admin.wizard.field.image"}}</label>
|
<label>{{i18n "admin.wizard.field.image"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-value">
|
<div class="setting-value">
|
||||||
{{image-uploader
|
{{uppy-image-uploader
|
||||||
imageUrl=field.image
|
imageUrl=field.image
|
||||||
onUploadDone=(action "imageUploadDone")
|
onUploadDone=(action "imageUploadDone")
|
||||||
onUploadDeleted=(action "imageUploadDeleted")
|
onUploadDeleted=(action "imageUploadDeleted")
|
||||||
type="wizard-step"
|
type="wizard-step"
|
||||||
class="no-repeat contain-image"}}
|
class="no-repeat contain-image"
|
||||||
|
id=(concat "wizard-field-" field.id "-image-upload")}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -127,13 +128,13 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if isComposerPreview}}
|
{{#if isComposerPreview}}
|
||||||
<div class="setting">
|
<div class="setting full">
|
||||||
<div class="setting-label">
|
<div class="setting-label">
|
||||||
<label>{{i18n "admin.wizard.field.preview_template"}}</label>
|
<label>{{i18n "admin.wizard.field.preview_template"}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-value">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -14,12 +14,13 @@
|
||||||
<label>{{i18n "admin.wizard.step.banner"}}</label>
|
<label>{{i18n "admin.wizard.step.banner"}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-value">
|
<div class="setting-value">
|
||||||
{{image-uploader
|
{{uppy-image-uploader
|
||||||
imageUrl=step.banner
|
imageUrl=step.banner
|
||||||
onUploadDone=(action "bannerUploadDone")
|
onUploadDone=(action "bannerUploadDone")
|
||||||
onUploadDeleted=(action "bannerUploadDeleted")
|
onUploadDeleted=(action "bannerUploadDeleted")
|
||||||
type="wizard-banner"
|
type="wizard-banner"
|
||||||
class="no-repeat contain-image"}}
|
class="no-repeat contain-image"
|
||||||
|
id=(concat "wizard-step-" step.id "-banner-upload")}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
value=pair.value
|
value=pair.value
|
||||||
activeType=pair.value_type
|
activeType=pair.value_type
|
||||||
options=options
|
options=options
|
||||||
onUpdate=onUpdate}}
|
onUpdate=onUpdate
|
||||||
|
connector=pair.connector}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if showJoin}}
|
{{#if showJoin}}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
//= require_tree_discourse discourse/app/lib
|
//= require_tree_discourse discourse/app/lib
|
||||||
//= require_tree_discourse discourse/app/mixins
|
//= require_tree_discourse discourse/app/mixins
|
||||||
|
|
||||||
//= require discourse/app/mixins/singleton
|
|
||||||
//= require discourse/app/mixins/upload
|
|
||||||
|
|
||||||
//= require discourse/app/adapters/rest
|
//= require discourse/app/adapters/rest
|
||||||
|
|
||||||
//= require message-bus
|
//= require message-bus
|
||||||
|
@ -22,6 +19,7 @@
|
||||||
|
|
||||||
//= require discourse/app/services/app-events
|
//= require discourse/app/services/app-events
|
||||||
//= require discourse/app/services/emoji-store
|
//= require discourse/app/services/emoji-store
|
||||||
|
//= require discourse/app/services/store
|
||||||
|
|
||||||
//= require discourse/app/components/user-selector
|
//= require discourse/app/components/user-selector
|
||||||
//= require discourse/app/components/conditional-loading-spinner
|
//= require discourse/app/components/conditional-loading-spinner
|
||||||
|
@ -37,6 +35,7 @@
|
||||||
//= require discourse/app/components/date-time-input
|
//= require discourse/app/components/date-time-input
|
||||||
//= require discourse/app/components/text-field
|
//= require discourse/app/components/text-field
|
||||||
//= require discourse/app/components/d-textarea
|
//= 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/conditional-loading-spinner
|
||||||
//= require discourse/app/templates/components/d-button
|
//= require discourse/app/templates/components/d-button
|
||||||
|
@ -61,10 +60,11 @@
|
||||||
|
|
||||||
//= require markdown-it-bundle
|
//= require markdown-it-bundle
|
||||||
//= require lodash.js
|
//= require lodash.js
|
||||||
//= require mousetrap.js
|
|
||||||
//= require template_include.js
|
//= require template_include.js
|
||||||
|
//= require itsatrap.js
|
||||||
//= require caret_position.js
|
//= require caret_position.js
|
||||||
//= require popper.js
|
//= require popper.js
|
||||||
|
//= require uppy.js
|
||||||
//= require bootstrap-modal.js
|
//= require bootstrap-modal.js
|
||||||
//= require bootbox.js
|
//= require bootbox.js
|
||||||
//= require discourse-shims
|
//= require discourse-shims
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { computed } from "@ember/object";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
|
||||||
export default CategorySelector.extend({
|
export default CategorySelector.extend({
|
||||||
|
classNames: ["category-selector", "wizard-category-selector"],
|
||||||
content: computed(
|
content: computed(
|
||||||
"categories.[]",
|
"categories.[]",
|
||||||
"blacklist.[]",
|
"blacklist.[]",
|
||||||
|
|
|
@ -4,24 +4,14 @@ import {
|
||||||
on,
|
on,
|
||||||
} from "discourse-common/utils/decorators";
|
} from "discourse-common/utils/decorators";
|
||||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
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 { caretPosition, inCodeBlock } from "discourse/lib/utilities";
|
||||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
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 { alias } from "@ember/object/computed";
|
||||||
import WizardI18n from "../lib/wizard-i18n";
|
|
||||||
import Site from "../models/site";
|
import Site from "../models/site";
|
||||||
|
import { uploadIcon } from "discourse/lib/uploads";
|
||||||
|
import { dasherize } from "@ember/string";
|
||||||
|
|
||||||
const uploadMarkdownResolvers = [];
|
|
||||||
|
|
||||||
const uploadHandlers = [];
|
|
||||||
export default ComposerEditor.extend({
|
export default ComposerEditor.extend({
|
||||||
classNameBindings: ["fieldClass"],
|
classNameBindings: ["fieldClass"],
|
||||||
allowUpload: true,
|
allowUpload: true,
|
||||||
|
@ -35,11 +25,11 @@ export default ComposerEditor.extend({
|
||||||
popupMenuOptions: [],
|
popupMenuOptions: [],
|
||||||
draftStatus: "null",
|
draftStatus: "null",
|
||||||
replyPlaceholder: alias("field.placeholder"),
|
replyPlaceholder: alias("field.placeholder"),
|
||||||
|
uploadingFieldId: null,
|
||||||
|
|
||||||
@on("didInsertElement")
|
@on("didInsertElement")
|
||||||
_composerEditorInit() {
|
_composerEditorInit() {
|
||||||
const $input = $(this.element.querySelector(".d-editor-input"));
|
const $input = $(this.element.querySelector(".d-editor-input"));
|
||||||
const $preview = $(this.element.querySelector(".d-editor-preview-wrapper"));
|
|
||||||
|
|
||||||
if (this.siteSettings.enable_mentions) {
|
if (this.siteSettings.enable_mentions) {
|
||||||
$input.autocomplete({
|
$input.autocomplete({
|
||||||
|
@ -81,15 +71,52 @@ export default ComposerEditor.extend({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._enableAdvancedEditorPreviewSync()) {
|
$input.on("scroll", this._throttledSyncEditorAndPreviewScroll);
|
||||||
this._initInputPreviewSync($input, $preview);
|
|
||||||
} else {
|
|
||||||
$input.on("scroll", () =>
|
|
||||||
throttle(this, this._syncEditorAndPreviewScroll, $input, $preview, 20)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._bindUploadTarget();
|
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
|
@discourseComputed
|
||||||
|
@ -105,192 +132,6 @@ export default ComposerEditor.extend({
|
||||||
return uploadIcon(false, this.siteSettings);
|
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) {
|
click(e) {
|
||||||
if ($(e.target).hasClass("wizard-composer-hyperlink")) {
|
if ($(e.target).hasClass("wizard-composer-hyperlink")) {
|
||||||
this.set("showHyperlinkBox", false);
|
this.set("showHyperlinkBox", false);
|
||||||
|
@ -372,7 +213,8 @@ export default ComposerEditor.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
showUploadModal() {
|
showUploadModal() {
|
||||||
$(this.element.querySelector(".wizard-composer-upload")).trigger("click");
|
this.session.set("uploadingFieldId", this.field.id);
|
||||||
|
document.getElementById(this.fileUploadElementId).click();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import DateInput from "discourse/components/date-input";
|
import DateInput from "discourse/components/date-input";
|
||||||
import loadScript from "discourse/lib/load-script";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import I18n from "I18n";
|
|
||||||
/* global Pikaday:true */
|
|
||||||
|
|
||||||
export default DateInput.extend({
|
export default DateInput.extend({
|
||||||
useNativePicker: false,
|
useNativePicker: false,
|
||||||
|
@ -11,32 +8,9 @@ export default DateInput.extend({
|
||||||
placeholder() {
|
placeholder() {
|
||||||
return this.format;
|
return this.format;
|
||||||
},
|
},
|
||||||
|
_opts() {
|
||||||
_loadPikadayPicker(container) {
|
return {
|
||||||
return loadScript("/javascripts/pikaday.js").then(() => {
|
format: this.format || "LL",
|
||||||
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()));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default Ember.Component.extend({
|
||||||
"composer",
|
"composer",
|
||||||
EmberObject.create({
|
EmberObject.create({
|
||||||
loading: false,
|
loading: false,
|
||||||
reply: this.get("field.value"),
|
reply: this.get("field.value") || "",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,63 +1,24 @@
|
||||||
import getUrl from "discourse-common/lib/get-url";
|
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
||||||
import { getToken } from "wizard/lib/ajax";
|
import Component from "@ember/component";
|
||||||
import WizardI18n from "../lib/wizard-i18n";
|
import { computed } from "@ember/object";
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Component.extend(UppyUploadMixin, {
|
||||||
classNames: ["wizard-field-upload"],
|
classNames: ["wizard-field-upload"],
|
||||||
classNameBindings: ["isImage"],
|
classNameBindings: ["isImage"],
|
||||||
uploading: false,
|
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() {
|
uploadDone(upload) {
|
||||||
this._super();
|
this.set("field.value", upload);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
||||||
const Router = requirejs("wizard/router").default;
|
const Router = requirejs("wizard/router").default;
|
||||||
const ApplicationRoute = requirejs("wizard/routes/application").default;
|
const ApplicationRoute = requirejs("wizard/routes/application").default;
|
||||||
const getUrl = requirejs("discourse-common/lib/get-url").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(
|
const registerRawHelpers = requirejs(
|
||||||
"discourse-common/lib/raw-handlebars-helpers"
|
"discourse-common/lib/raw-handlebars-helpers"
|
||||||
).registerRawHelpers;
|
).registerRawHelpers;
|
||||||
|
@ -111,8 +111,16 @@ export default {
|
||||||
model() {},
|
model() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
$.ajaxPrefilter(function (_, __, jqXHR) {
|
// Add a CSRF token to all AJAX requests
|
||||||
jqXHR.setRequestHeader("X-CSRF-Token", getToken());
|
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"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
export function cook(text, options) {
|
export function cook(text, options) {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
options = buildOptions({
|
options = buildOptions({
|
||||||
getURL: getURL,
|
getURL,
|
||||||
siteSettings: getOwner(this).lookup("site-settings:main"),
|
siteSettings: getOwner(this).lookup("site-settings:main"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,12 +27,12 @@ function performSearch(
|
||||||
// need to be able to cancel this
|
// need to be able to cancel this
|
||||||
oldSearch = $.ajax(getUrl("/u/search/users"), {
|
oldSearch = $.ajax(getUrl("/u/search/users"), {
|
||||||
data: {
|
data: {
|
||||||
term: term,
|
term,
|
||||||
topic_id: topicId,
|
topic_id: topicId,
|
||||||
include_groups: includeGroups,
|
include_groups: includeGroups,
|
||||||
include_mentionable_groups: includeMentionableGroups,
|
include_mentionable_groups: includeMentionableGroups,
|
||||||
include_messageable_groups: includeMessageableGroups,
|
include_messageable_groups: includeMessageableGroups,
|
||||||
group: group,
|
group,
|
||||||
topic_allowed_users: allowedUsers,
|
topic_allowed_users: allowedUsers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { default as computed } from "discourse-common/utils/decorators";
|
||||||
import getUrl from "discourse-common/lib/get-url";
|
import getUrl from "discourse-common/lib/get-url";
|
||||||
import WizardField from "wizard/models/wizard-field";
|
import WizardField from "wizard/models/wizard-field";
|
||||||
import { ajax } from "wizard/lib/ajax";
|
import { ajax } from "wizard/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import Step from "wizard/models/step";
|
import Step from "wizard/models/step";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import Site from "./site";
|
import Site from "./site";
|
||||||
|
@ -24,9 +25,19 @@ const CustomWizard = EmberObject.extend({
|
||||||
|
|
||||||
CustomWizard.reopenClass({
|
CustomWizard.reopenClass({
|
||||||
skip(wizardId) {
|
skip(wizardId) {
|
||||||
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" }).then((result) => {
|
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" })
|
||||||
CustomWizard.finished(result);
|
.then((result) => {
|
||||||
});
|
CustomWizard.finished(result);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
restart(wizardId) {
|
||||||
|
ajax({ url: `/w/${wizardId}/skip`, type: "PUT" })
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = `/w/${wizardId}`;
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
restart(wizardId) {
|
restart(wizardId) {
|
||||||
|
|
|
@ -16,24 +16,10 @@
|
||||||
disabled=disableTextarea
|
disabled=disableTextarea
|
||||||
outletArgs=(hash composer=composer editorType="composer")}}
|
outletArgs=(hash composer=composer editorType="composer")}}
|
||||||
|
|
||||||
{{input
|
<input
|
||||||
class="wizard-composer-upload hidden-upload-field"
|
|
||||||
disabled=isUploading
|
|
||||||
type="file"
|
type="file"
|
||||||
accept=allowedFileTypes
|
id={{fileUploadElementId}}
|
||||||
multiple=true}}
|
class="wizard-composer-upload"
|
||||||
|
accept={{allowedFileTypes}}
|
||||||
{{#if showHyperlinkBox}}
|
multiple
|
||||||
{{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}}
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{{d-icon "upload"}}
|
{{d-icon "upload"}}
|
||||||
{{/if}}
|
{{/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>
|
</label>
|
||||||
|
|
||||||
{{#if field.value}}
|
{{#if field.value}}
|
||||||
|
|
|
@ -317,6 +317,10 @@
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-template {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.full,
|
&.full,
|
||||||
|
|
|
@ -72,6 +72,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
margin-top: unset;
|
||||||
|
padding-top: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-editor-button-bar {
|
.d-editor-button-bar {
|
||||||
|
@ -107,11 +111,6 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-editor-preview-wrapper {
|
|
||||||
overflow: auto;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-editor-input,
|
.d-editor-input,
|
||||||
.d-editor-preview {
|
.d-editor-preview {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -173,4 +173,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wizard-category-selector {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,6 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: 1.7;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@extend img.emoji;
|
@extend img.emoji;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
@import "common/components/buttons";
|
@import "common/components/buttons";
|
||||||
@import "common/d-editor";
|
@import "common/d-editor";
|
||||||
@import "desktop/modal";
|
@import "desktop/modal";
|
||||||
|
@import "common/input_tip";
|
||||||
|
|
||||||
@import "custom/base";
|
@import "custom/base";
|
||||||
@import "custom/wizard";
|
@import "custom/wizard";
|
||||||
|
|
|
@ -60,6 +60,10 @@ en:
|
||||||
select_type: "Select a type"
|
select_type: "Select a type"
|
||||||
condition: "Condition"
|
condition: "Condition"
|
||||||
index: "Index"
|
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:
|
message:
|
||||||
wizard:
|
wizard:
|
||||||
|
@ -124,6 +128,7 @@ en:
|
||||||
group: "group"
|
group: "group"
|
||||||
list: "list"
|
list: "list"
|
||||||
custom_field: "custom field"
|
custom_field: "custom field"
|
||||||
|
value: "value"
|
||||||
|
|
||||||
placeholder:
|
placeholder:
|
||||||
text: "Enter text"
|
text: "Enter text"
|
||||||
|
@ -138,6 +143,7 @@ en:
|
||||||
group: "Select group"
|
group: "Select group"
|
||||||
list: "Enter item"
|
list: "Enter item"
|
||||||
custom_field: "Select field"
|
custom_field: "Select field"
|
||||||
|
value: "Select value"
|
||||||
|
|
||||||
error:
|
error:
|
||||||
failed: "failed to save wizard"
|
failed: "failed to save wizard"
|
||||||
|
@ -177,7 +183,7 @@ en:
|
||||||
char_counter_placeholder: "Display Character Counter"
|
char_counter_placeholder: "Display Character Counter"
|
||||||
field_placeholder: "Field Placeholder"
|
field_placeholder: "Field Placeholder"
|
||||||
file_types: "File Types"
|
file_types: "File Types"
|
||||||
preview_template: "Preview Template"
|
preview_template: "Template"
|
||||||
limit: "Limit"
|
limit: "Limit"
|
||||||
property: "Property"
|
property: "Property"
|
||||||
prefill: "Prefill"
|
prefill: "Prefill"
|
||||||
|
|
|
@ -48,7 +48,10 @@ en:
|
||||||
validation:
|
validation:
|
||||||
required: "%{property} is required"
|
required: "%{property} is required"
|
||||||
conflict: "Wizard with id '%{wizard_id}' already exists"
|
conflict: "Wizard with id '%{wizard_id}' already exists"
|
||||||
after_time: "After time setting is invalid"
|
after_signup: "You can only have one 'after signup' wizard at a time. %{wizard_id} has 'after signup' enabled."
|
||||||
|
after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard."
|
||||||
|
after_time: "After time setting is invalid."
|
||||||
|
liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}"
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
custom_wizard_enabled: "Enable custom wizards."
|
custom_wizard_enabled: "Enable custom wizards."
|
||||||
|
|
|
@ -37,7 +37,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
|
||||||
wizard_id = template.save(create: params[:create])
|
wizard_id = template.save(create: params[:create])
|
||||||
|
|
||||||
if template.errors.any?
|
if template.errors.any?
|
||||||
render json: failed_json.merge(errors: template.errors.full_messages)
|
render json: failed_json.merge(backend_validation_error: template.errors.full_messages.join("\n\n"))
|
||||||
else
|
else
|
||||||
render json: success_json.merge(wizard_id: wizard_id)
|
render json: success_json.merge(wizard_id: wizard_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,7 +54,7 @@ class CustomWizard::StepsController < ::ApplicationController
|
||||||
updater.result[:redirect_on_complete] = redirect
|
updater.result[:redirect_on_complete] = redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
@wizard.final_cleanup!
|
@wizard.cleanup_on_complete!
|
||||||
|
|
||||||
result[:final] = true
|
result[:final] = true
|
||||||
else
|
else
|
||||||
|
|
|
@ -5,6 +5,7 @@ class CustomWizard::WizardController < ::ApplicationController
|
||||||
layout 'wizard'
|
layout 'wizard'
|
||||||
|
|
||||||
before_action :ensure_plugin_enabled
|
before_action :ensure_plugin_enabled
|
||||||
|
before_action :ensure_logged_in, only: [:skip]
|
||||||
helper_method :wizard_page_title
|
helper_method :wizard_page_title
|
||||||
helper_method :wizard_theme_id
|
helper_method :wizard_theme_id
|
||||||
helper_method :wizard_theme_lookup
|
helper_method :wizard_theme_lookup
|
||||||
|
@ -59,17 +60,13 @@ class CustomWizard::WizardController < ::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
result = success_json
|
result = success_json
|
||||||
user = current_user
|
|
||||||
|
|
||||||
if user && wizard.can_access?
|
if current_user && wizard.can_access?
|
||||||
submission = wizard.current_submission
|
if redirect_to = wizard.current_submission&.redirect_to
|
||||||
|
result.merge!(redirect_to: redirect_to)
|
||||||
if submission.present? && submission.redirect_to
|
|
||||||
result.merge!(redirect_to: submission.redirect_to)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
submission.remove if submission.present?
|
wizard.cleanup_on_skip!
|
||||||
wizard.reset
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: result
|
render json: result
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"result": {
|
"result": {
|
||||||
"line": 92.09
|
"line": 92.52
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
crowdin.yml
Normale Datei
6
crowdin.yml
Normale Datei
|
@ -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
|
17
extensions/guardian.rb
Normale Datei
17
extensions/guardian.rb
Normale Datei
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module CustomWizardGuardian
|
||||||
|
def can_edit_topic?(topic)
|
||||||
|
wizard_can_edit_topic?(topic) || super
|
||||||
|
end
|
||||||
|
|
||||||
|
def wizard_can_edit_topic?(topic)
|
||||||
|
created_by_wizard = !!topic.wizard_submission_id
|
||||||
|
(
|
||||||
|
is_my_own?(topic) &&
|
||||||
|
created_by_wizard &&
|
||||||
|
can_see_topic?(topic) &&
|
||||||
|
can_create_post_on_topic?(topic)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
module InvitesControllerCustomWizard
|
module InvitesControllerCustomWizard
|
||||||
def path(url)
|
def path(url)
|
||||||
if ::Wizard.user_requires_completion?(@user)
|
if ::Wizard.user_requires_completion?(@user)
|
||||||
wizard_id = @user.custom_fields['redirect_to_wizard']
|
wizard_id = @user.redirect_to_wizard
|
||||||
|
|
||||||
if wizard_id && url != '/'
|
if wizard_id && url != '/'
|
||||||
CustomWizard::Wizard.set_wizard_redirect(@user, wizard_id, url)
|
CustomWizard::Wizard.set_wizard_redirect(@user, wizard_id, url)
|
||||||
|
|
|
@ -14,6 +14,8 @@ module Jobs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
CustomWizard::Template.clear_cache_keys
|
||||||
|
|
||||||
MessageBus.publish "/redirect_to_wizard", wizard.id, user_ids: user_ids
|
MessageBus.publish "/redirect_to_wizard", wizard.id, user_ids: user_ids
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -514,7 +514,12 @@ class CustomWizard::Action
|
||||||
|
|
||||||
def basic_topic_params
|
def basic_topic_params
|
||||||
params = {
|
params = {
|
||||||
skip_validations: true
|
skip_validations: true,
|
||||||
|
topic_opts: {
|
||||||
|
custom_fields: {
|
||||||
|
wizard_submission_id: @wizard.current_submission.id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params[:title] = CustomWizard::Mapper.new(
|
params[:title] = CustomWizard::Mapper.new(
|
||||||
|
|
|
@ -143,7 +143,7 @@ class CustomWizard::Mapper
|
||||||
if value == "present"
|
if value == "present"
|
||||||
result = key.public_send(operator)
|
result = key.public_send(operator)
|
||||||
elsif ["true", "false"].include?(value)
|
elsif ["true", "false"].include?(value)
|
||||||
result = key.public_send(operator, ActiveRecord::Type::Boolean.new.cast(value))
|
result = bool(key).public_send(operator, bool(value))
|
||||||
end
|
end
|
||||||
elsif [key, value, operator].all? { |i| !i.nil? }
|
elsif [key, value, operator].all? { |i| !i.nil? }
|
||||||
result = key.public_send(operator, value)
|
result = key.public_send(operator, value)
|
||||||
|
@ -265,4 +265,8 @@ class CustomWizard::Mapper
|
||||||
result = data[k]
|
result = data[k]
|
||||||
keys.empty? ? result : self.recurse(result, keys)
|
keys.empty? ? result : self.recurse(result, keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bool(value)
|
||||||
|
ActiveRecord::Type::Boolean.new.cast(value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
class CustomWizard::Template
|
class CustomWizard::Template
|
||||||
include HasErrors
|
include HasErrors
|
||||||
|
|
||||||
|
AFTER_SIGNUP_CACHE_KEY ||= "after_signup_wizard_ids"
|
||||||
|
AFTER_TIME_CACHE_KEY ||= "after_time_wizard_ids"
|
||||||
|
|
||||||
attr_reader :data,
|
attr_reader :data,
|
||||||
:opts,
|
:opts,
|
||||||
:steps,
|
:steps,
|
||||||
|
@ -28,6 +31,8 @@ class CustomWizard::Template
|
||||||
PluginStore.set(CustomWizard::PLUGIN_NAME, @data[:id], @data)
|
PluginStore.set(CustomWizard::PLUGIN_NAME, @data[:id], @data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.class.clear_cache_keys
|
||||||
|
|
||||||
@data[:id]
|
@data[:id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,10 +58,10 @@ class CustomWizard::Template
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
PluginStore.remove(CustomWizard::PLUGIN_NAME, wizard.id)
|
PluginStore.remove(CustomWizard::PLUGIN_NAME, wizard.id)
|
||||||
clear_user_wizard_redirect(wizard_id)
|
clear_user_wizard_redirect(wizard_id, after_time: !!wizard.after_time)
|
||||||
end
|
end
|
||||||
|
|
||||||
Jobs.cancel_scheduled_job(:set_after_time_wizard) if wizard.after_time
|
clear_cache_keys
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -65,9 +70,10 @@ class CustomWizard::Template
|
||||||
PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: wizard_id)
|
PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: wizard_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.list(setting: nil, order: :id)
|
def self.list(setting: nil, query_str: nil, order: :id)
|
||||||
query = "plugin_name = 'custom_wizard'"
|
query = "plugin_name = 'custom_wizard'"
|
||||||
query += "AND (value::json ->> '#{setting}')::boolean IS TRUE" if setting
|
query += " AND (value::json ->> '#{setting}')::boolean IS TRUE" if setting
|
||||||
|
query += " #{query_str}" if query_str
|
||||||
|
|
||||||
PluginStoreRow.where(query).order(order)
|
PluginStoreRow.where(query).order(order)
|
||||||
.reduce([]) do |result, record|
|
.reduce([]) do |result, record|
|
||||||
|
@ -85,8 +91,36 @@ class CustomWizard::Template
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.clear_user_wizard_redirect(wizard_id)
|
def self.clear_user_wizard_redirect(wizard_id, after_time: false)
|
||||||
UserCustomField.where(name: 'redirect_to_wizard', value: wizard_id).destroy_all
|
UserCustomField.where(name: 'redirect_to_wizard', value: wizard_id).destroy_all
|
||||||
|
|
||||||
|
if after_time
|
||||||
|
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.after_signup_ids
|
||||||
|
::CustomWizard::Cache.wrap(AFTER_SIGNUP_CACHE_KEY) do
|
||||||
|
list(setting: 'after_signup').map { |t| t['id'] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.after_time_ids
|
||||||
|
::CustomWizard::Cache.wrap(AFTER_TIME_CACHE_KEY) do
|
||||||
|
list(
|
||||||
|
setting: 'after_time',
|
||||||
|
query_str: "AND (value::json ->> 'after_time_scheduled')::timestamp < CURRENT_TIMESTAMP"
|
||||||
|
).map { |t| t['id'] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.can_redirect_users?(wizard_id)
|
||||||
|
after_signup_ids.include?(wizard_id) || after_time_ids.include?(wizard_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.clear_cache_keys
|
||||||
|
CustomWizard::Cache.new(AFTER_SIGNUP_CACHE_KEY).delete
|
||||||
|
CustomWizard::Cache.new(AFTER_TIME_CACHE_KEY).delete
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -132,8 +166,7 @@ class CustomWizard::Template
|
||||||
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id)
|
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id)
|
||||||
Jobs.enqueue_at(enqueue_wizard_at, :set_after_time_wizard, wizard_id: wizard_id)
|
Jobs.enqueue_at(enqueue_wizard_at, :set_after_time_wizard, wizard_id: wizard_id)
|
||||||
elsif old_data && old_data[:after_time]
|
elsif old_data && old_data[:after_time]
|
||||||
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id)
|
clear_user_wizard_redirect(wizard_id, after_time: true)
|
||||||
self.class.clear_user_wizard_redirect(wizard_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,14 +13,19 @@ class CustomWizard::TemplateValidator
|
||||||
|
|
||||||
check_id(data, :wizard)
|
check_id(data, :wizard)
|
||||||
check_required(data, :wizard)
|
check_required(data, :wizard)
|
||||||
|
validate_after_signup
|
||||||
validate_after_time
|
validate_after_time
|
||||||
|
|
||||||
|
return false if errors.any?
|
||||||
|
|
||||||
data[:steps].each do |step|
|
data[:steps].each do |step|
|
||||||
check_required(step, :step)
|
check_required(step, :step)
|
||||||
|
validate_liquid_template(step, :step)
|
||||||
|
|
||||||
if data[:fields].present?
|
if step[:fields].present?
|
||||||
data[:fields].each do |field|
|
step[:fields].each do |field|
|
||||||
check_required(field, :field)
|
check_required(field, :field)
|
||||||
|
validate_liquid_template(field, :field)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -28,14 +33,11 @@ class CustomWizard::TemplateValidator
|
||||||
if data[:actions].present?
|
if data[:actions].present?
|
||||||
data[:actions].each do |action|
|
data[:actions].each do |action|
|
||||||
check_required(action, :action)
|
check_required(action, :action)
|
||||||
|
validate_liquid_template(action, :action)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if errors.any?
|
!errors.any?
|
||||||
false
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.required
|
def self.required
|
||||||
|
@ -63,8 +65,24 @@ class CustomWizard::TemplateValidator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_after_signup
|
||||||
|
return unless ActiveRecord::Type::Boolean.new.cast(@data[:after_signup])
|
||||||
|
|
||||||
|
other_after_signup = CustomWizard::Template.list(setting: 'after_signup')
|
||||||
|
.select { |template| template['id'] != @data[:id] }
|
||||||
|
|
||||||
|
if other_after_signup.any?
|
||||||
|
errors.add :base, I18n.t("wizard.validation.after_signup", wizard_id: other_after_signup.first['id'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate_after_time
|
def validate_after_time
|
||||||
return unless @data[:after_time]
|
return unless ActiveRecord::Type::Boolean.new.cast(@data[:after_time])
|
||||||
|
|
||||||
|
if ActiveRecord::Type::Boolean.new.cast(@data[:after_signup])
|
||||||
|
errors.add :base, I18n.t("wizard.validation.after_signup_after_time")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
wizard = CustomWizard::Wizard.create(@data[:id]) if !@opts[:create]
|
wizard = CustomWizard::Wizard.create(@data[:id]) if !@opts[:create]
|
||||||
current_time = wizard.present? ? wizard.after_time_scheduled : nil
|
current_time = wizard.present? ? wizard.after_time_scheduled : nil
|
||||||
|
@ -80,4 +98,35 @@ class CustomWizard::TemplateValidator
|
||||||
errors.add :base, I18n.t("wizard.validation.after_time")
|
errors.add :base, I18n.t("wizard.validation.after_time")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_liquid_template(object, type)
|
||||||
|
%w[
|
||||||
|
description
|
||||||
|
raw_description
|
||||||
|
placeholder
|
||||||
|
preview_template
|
||||||
|
post_template
|
||||||
|
].each do |field|
|
||||||
|
if template = object[field]
|
||||||
|
result = is_liquid_template_valid?(template)
|
||||||
|
|
||||||
|
unless "valid" == result
|
||||||
|
error = I18n.t("wizard.validation.liquid_syntax_error",
|
||||||
|
attribute: "#{object[:id]}.#{field}",
|
||||||
|
message: result
|
||||||
|
)
|
||||||
|
errors.add :base, error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_liquid_template_valid?(template)
|
||||||
|
begin
|
||||||
|
Liquid::Template.parse(template)
|
||||||
|
'valid'
|
||||||
|
rescue Liquid::SyntaxError => error
|
||||||
|
error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -288,11 +288,8 @@ class CustomWizard::Wizard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def final_cleanup!
|
def cleanup_on_complete!
|
||||||
if id == user.custom_fields['redirect_to_wizard']
|
remove_user_redirect
|
||||||
user.custom_fields.delete('redirect_to_wizard')
|
|
||||||
user.save_custom_fields(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
if current_submission.present?
|
if current_submission.present?
|
||||||
current_submission.submitted_at = Time.now.iso8601
|
current_submission.submitted_at = Time.now.iso8601
|
||||||
|
@ -302,6 +299,23 @@ class CustomWizard::Wizard
|
||||||
update!
|
update!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cleanup_on_skip!
|
||||||
|
remove_user_redirect
|
||||||
|
|
||||||
|
if current_submission.present?
|
||||||
|
current_submission.remove
|
||||||
|
end
|
||||||
|
|
||||||
|
reset
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_user_redirect
|
||||||
|
if id == user.redirect_to_wizard
|
||||||
|
user.custom_fields.delete('redirect_to_wizard')
|
||||||
|
user.save_custom_fields(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.create(wizard_id, user = nil)
|
def self.create(wizard_id, user = nil)
|
||||||
if template = CustomWizard::Template.find(wizard_id)
|
if template = CustomWizard::Template.find(wizard_id)
|
||||||
new(template.to_h, user)
|
new(template.to_h, user)
|
||||||
|
|
5363
package-lock.json
generiert
Normale Datei
5363
package-lock.json
generiert
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
|
@ -5,6 +5,7 @@
|
||||||
"author": "Pavilion",
|
"author": "Pavilion",
|
||||||
"license": "GPL V2",
|
"license": "GPL V2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-discourse": "^1.1.8"
|
"eslint-config-discourse": "^1.1.8",
|
||||||
|
"semver": "^7.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
45
plugin.rb
45
plugin.rb
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
# name: discourse-custom-wizard
|
# name: discourse-custom-wizard
|
||||||
# about: Create custom wizards
|
# about: Create custom wizards
|
||||||
# version: 1.15.0.stable
|
# version: 1.17.2.stable
|
||||||
# authors: Angus McLeod
|
# authors: Angus McLeod
|
||||||
# url: https://github.com/paviliondev/discourse-custom-wizard
|
# url: https://github.com/paviliondev/discourse-custom-wizard
|
||||||
# contact emails: angus@thepavilion.io
|
# contact emails: angus@thepavilion.io
|
||||||
|
@ -108,6 +108,7 @@ after_initialize do
|
||||||
../serializers/custom_wizard/realtime_validation/similar_topics_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/guardian.rb
|
||||||
../extensions/users_controller.rb
|
../extensions/users_controller.rb
|
||||||
../extensions/custom_field/preloader.rb
|
../extensions/custom_field/preloader.rb
|
||||||
../extensions/custom_field/serializer.rb
|
../extensions/custom_field/serializer.rb
|
||||||
|
@ -116,8 +117,21 @@ after_initialize do
|
||||||
load File.expand_path(path, __FILE__)
|
load File.expand_path(path, __FILE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Liquid::Template.error_mode = :strict
|
||||||
|
|
||||||
|
# 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)
|
Liquid::Template.register_filter(::CustomWizard::LiquidFilter::FirstNonEmpty)
|
||||||
|
|
||||||
|
add_to_class(:topic, :wizard_submission_id) do
|
||||||
|
custom_fields['wizard_submission_id']
|
||||||
|
end
|
||||||
|
|
||||||
add_class_method(:wizard, :user_requires_completion?) do |user|
|
add_class_method(:wizard, :user_requires_completion?) do |user|
|
||||||
wizard_result = self.new(user).requires_completion?
|
wizard_result = self.new(user).requires_completion?
|
||||||
return wizard_result if wizard_result
|
return wizard_result if wizard_result
|
||||||
|
@ -137,8 +151,16 @@ after_initialize do
|
||||||
!!custom_redirect
|
!!custom_redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_to_class(:user, :redirect_to_wizard) do
|
||||||
|
if custom_fields['redirect_to_wizard'].present?
|
||||||
|
custom_fields['redirect_to_wizard']
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
add_to_class(:users_controller, :wizard_path) do
|
add_to_class(:users_controller, :wizard_path) do
|
||||||
if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard']
|
if custom_wizard_redirect = current_user.redirect_to_wizard
|
||||||
"#{Discourse.base_url}/w/#{custom_wizard_redirect.dasherize}"
|
"#{Discourse.base_url}/w/#{custom_wizard_redirect.dasherize}"
|
||||||
else
|
else
|
||||||
"#{Discourse.base_url}/wizard"
|
"#{Discourse.base_url}/wizard"
|
||||||
|
@ -146,7 +168,7 @@ after_initialize do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_to_serializer(:current_user, :redirect_to_wizard) do
|
add_to_serializer(:current_user, :redirect_to_wizard) do
|
||||||
object.custom_fields['redirect_to_wizard']
|
object.redirect_to_wizard
|
||||||
end
|
end
|
||||||
|
|
||||||
on(:user_approved) do |user|
|
on(:user_approved) do |user|
|
||||||
|
@ -156,15 +178,19 @@ after_initialize do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_to_class(:application_controller, :redirect_to_wizard_if_required) do
|
add_to_class(:application_controller, :redirect_to_wizard_if_required) do
|
||||||
wizard_id = current_user.custom_fields['redirect_to_wizard']
|
|
||||||
@excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/']
|
@excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/']
|
||||||
url = request.referer || request.original_url
|
url = request.referer || request.original_url
|
||||||
|
excluded_route = @excluded_routes.any? { |str| /#{str}/ =~ url }
|
||||||
|
not_api = request.format === 'text/html'
|
||||||
|
|
||||||
|
if not_api && !excluded_route
|
||||||
|
wizard_id = current_user.redirect_to_wizard
|
||||||
|
|
||||||
|
if CustomWizard::Template.can_redirect_users?(wizard_id)
|
||||||
|
if url !~ /\/w\// && url !~ /\/invites\//
|
||||||
|
CustomWizard::Wizard.set_wizard_redirect(current_user, wizard_id, url)
|
||||||
|
end
|
||||||
|
|
||||||
if request.format === 'text/html' && !@excluded_routes.any? { |str| /#{str}/ =~ url } && wizard_id
|
|
||||||
if request.referer !~ /\/w\// && request.referer !~ /\/invites\//
|
|
||||||
CustomWizard::Wizard.set_wizard_redirect(current_user, wizard_id, request.referer)
|
|
||||||
end
|
|
||||||
if CustomWizard::Template.exists?(wizard_id)
|
|
||||||
redirect_to "/w/#{wizard_id.dasherize}"
|
redirect_to "/w/#{wizard_id.dasherize}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -191,6 +217,7 @@ after_initialize do
|
||||||
::ExtraLocalesController.prepend ExtraLocalesControllerCustomWizard
|
::ExtraLocalesController.prepend ExtraLocalesControllerCustomWizard
|
||||||
::InvitesController.prepend InvitesControllerCustomWizard
|
::InvitesController.prepend InvitesControllerCustomWizard
|
||||||
::UsersController.prepend CustomWizardUsersController
|
::UsersController.prepend CustomWizardUsersController
|
||||||
|
::Guardian.prepend CustomWizardGuardian
|
||||||
|
|
||||||
full_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets/stylesheets/wizard/wizard_custom.scss"
|
full_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets/stylesheets/wizard/wizard_custom.scss"
|
||||||
if Stylesheet::Importer.respond_to?(:plugin_assets)
|
if Stylesheet::Importer.respond_to?(:plugin_assets)
|
||||||
|
|
|
@ -47,6 +47,14 @@ describe CustomWizard::Builder do
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let(:boolean_field_condition_json) {
|
||||||
|
JSON.parse(
|
||||||
|
File.open(
|
||||||
|
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/condition/boolean_field_condition.json"
|
||||||
|
).read
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Group.refresh_automatic_group!(:trust_level_3)
|
Group.refresh_automatic_group!(:trust_level_3)
|
||||||
CustomWizard::Template.save(
|
CustomWizard::Template.save(
|
||||||
|
@ -316,12 +324,13 @@ describe CustomWizard::Builder do
|
||||||
.build
|
.build
|
||||||
.steps.first
|
.steps.first
|
||||||
.fields.length
|
.fields.length
|
||||||
).to eq(4)
|
).to eq(@template[:steps][0][:fields].length)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with condition" do
|
context "with condition" do
|
||||||
before do
|
before do
|
||||||
@template[:steps][0][:fields][0][:condition] = user_condition_json['condition']
|
@template[:steps][0][:fields][0][:condition] = user_condition_json['condition']
|
||||||
|
@template[:steps][2][:fields][5][:condition] = boolean_field_condition_json['condition']
|
||||||
CustomWizard::Template.save(@template.as_json)
|
CustomWizard::Template.save(@template.as_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -334,6 +343,16 @@ describe CustomWizard::Builder do
|
||||||
wizard = CustomWizard::Builder.new(@template[:id], user).build
|
wizard = CustomWizard::Builder.new(@template[:id], user).build
|
||||||
expect(wizard.steps.first.fields.first.id).to eq(@template[:steps][0][:fields][1]['id'])
|
expect(wizard.steps.first.fields.first.id).to eq(@template[:steps][0][:fields][1]['id'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "works if a field condition uses 'is true/false'" do
|
||||||
|
builder = CustomWizard::Builder.new(@template[:id], user)
|
||||||
|
wizard = builder.build
|
||||||
|
wizard.create_updater('step_2', step_2_field_5: 'true').update
|
||||||
|
|
||||||
|
builder = CustomWizard::Builder.new(@template[:id], user)
|
||||||
|
wizard = builder.build
|
||||||
|
expect(wizard.steps.last.fields.last.id).to eq(@template[:steps][2][:fields][5]['id'])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,33 @@ describe CustomWizard::TemplateValidator do
|
||||||
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
|
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
|
||||||
).read).with_indifferent_access
|
).read).with_indifferent_access
|
||||||
}
|
}
|
||||||
|
let(:valid_liquid_template) {
|
||||||
|
<<-LIQUID.strip
|
||||||
|
{%- assign hello = "Topic Form 1" %}
|
||||||
|
LIQUID
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:invalid_liquid_template) {
|
||||||
|
<<-LIQUID.strip
|
||||||
|
{%- assign hello = "Topic Form 1" %
|
||||||
|
LIQUID
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:liquid_syntax_error) {
|
||||||
|
"Liquid syntax error: Tag '{%' was not properly terminated with regexp: /\\%\\}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
def expect_validation_success
|
||||||
|
expect(
|
||||||
|
CustomWizard::TemplateValidator.new(template).perform
|
||||||
|
).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_validation_failure(object_id, message)
|
||||||
|
validator = CustomWizard::TemplateValidator.new(template)
|
||||||
|
expect(validator.perform).to eq(false)
|
||||||
|
expect(validator.errors.first.message).to eq("Liquid syntax error in #{object_id}: #{message}")
|
||||||
|
end
|
||||||
|
|
||||||
it "validates valid templates" do
|
it "validates valid templates" do
|
||||||
expect(
|
expect(
|
||||||
|
@ -30,6 +57,38 @@ describe CustomWizard::TemplateValidator do
|
||||||
).to eq(false)
|
).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "only allows one after signup wizard at a time" do
|
||||||
|
wizard_id = template[:id]
|
||||||
|
template[:after_signup] = true
|
||||||
|
CustomWizard::Template.save(template)
|
||||||
|
|
||||||
|
template[:id] = "wizard_2"
|
||||||
|
template[:after_signup] = true
|
||||||
|
|
||||||
|
validator = CustomWizard::TemplateValidator.new(template)
|
||||||
|
expect(validator.perform).to eq(false)
|
||||||
|
expect(validator.errors.first.type).to eq(
|
||||||
|
I18n.t("wizard.validation.after_signup", wizard_id: wizard_id)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "only allows a wizard with after signup to be validated twice" do
|
||||||
|
template[:after_signup] = true
|
||||||
|
CustomWizard::Template.save(template)
|
||||||
|
expect(CustomWizard::TemplateValidator.new(template).perform).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "only allows one after _ setting per wizard" do
|
||||||
|
template[:after_signup] = true
|
||||||
|
template[:after_time] = true
|
||||||
|
|
||||||
|
validator = CustomWizard::TemplateValidator.new(template)
|
||||||
|
expect(validator.perform).to eq(false)
|
||||||
|
expect(validator.errors.first.type).to eq(
|
||||||
|
I18n.t("wizard.validation.after_signup_after_time")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it "validates after time settings" do
|
it "validates after time settings" do
|
||||||
template[:after_time] = true
|
template[:after_time] = true
|
||||||
template[:after_time_scheduled] = (Time.now + 3.hours).iso8601
|
template[:after_time_scheduled] = (Time.now + 3.hours).iso8601
|
||||||
|
@ -45,4 +104,97 @@ describe CustomWizard::TemplateValidator do
|
||||||
CustomWizard::TemplateValidator.new(template).perform
|
CustomWizard::TemplateValidator.new(template).perform
|
||||||
).to eq(false)
|
).to eq(false)
|
||||||
end
|
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
|
||||||
|
|
||||||
|
context "liquid templates" do
|
||||||
|
it "validates if no liquid syntax in use" do
|
||||||
|
expect_validation_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "validates if liquid syntax in use is correct" do
|
||||||
|
template[:steps][0][:raw_description] = valid_liquid_template
|
||||||
|
expect_validation_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't validate if liquid syntax in use is incorrect" do
|
||||||
|
template[:steps][0][:raw_description] = invalid_liquid_template
|
||||||
|
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "validation targets" do
|
||||||
|
context "fields" do
|
||||||
|
it "validates descriptions" do
|
||||||
|
template[:steps][0][:fields][0][:description] = invalid_liquid_template
|
||||||
|
expect_validation_failure("step_1_field_1.description", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "validates placeholders" do
|
||||||
|
template[:steps][0][:fields][0][:placeholder] = invalid_liquid_template
|
||||||
|
expect_validation_failure("step_1_field_1.placeholder", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "validates preview templates" do
|
||||||
|
template[:steps][0][:fields][4][:preview_template] = invalid_liquid_template
|
||||||
|
expect_validation_failure("step_1_field_5.preview_template", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "steps" do
|
||||||
|
it "validates descriptions" do
|
||||||
|
template[:steps][0][:raw_description] = invalid_liquid_template
|
||||||
|
expect_validation_failure("step_1.raw_description", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "actions" do
|
||||||
|
it "validates post builder" do
|
||||||
|
action = nil
|
||||||
|
action_index = nil
|
||||||
|
|
||||||
|
template[:actions].each_with_index do |a, i|
|
||||||
|
if a["post_builder"]
|
||||||
|
action = a
|
||||||
|
action_index = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
template[:actions][action_index][:post_template] = invalid_liquid_template
|
||||||
|
|
||||||
|
expect_validation_failure("#{action[:id]}.post_template", liquid_syntax_error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
61
spec/extensions/guardian_extension_spec.rb
Normale Datei
61
spec/extensions/guardian_extension_spec.rb
Normale Datei
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../plugin_helper'
|
||||||
|
|
||||||
|
describe ::Guardian do
|
||||||
|
fab!(:user) {
|
||||||
|
Fabricate(:user, name: "Angus", username: 'angus', email: "angus@email.com")
|
||||||
|
}
|
||||||
|
fab!(:category) { Fabricate(:category, name: 'cat1', slug: 'cat-slug') }
|
||||||
|
let(:wizard_template) {
|
||||||
|
JSON.parse(
|
||||||
|
File.open(
|
||||||
|
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
|
||||||
|
).read
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_topic_by_wizard(wizard)
|
||||||
|
wizard.create_updater(
|
||||||
|
wizard.steps.first.id,
|
||||||
|
step_1_field_1: "Topic Title",
|
||||||
|
step_1_field_2: "topic body"
|
||||||
|
).update
|
||||||
|
wizard.create_updater(wizard.steps.second.id, {}).update
|
||||||
|
wizard.create_updater(wizard.steps.last.id,
|
||||||
|
step_3_field_3: category.id
|
||||||
|
).update
|
||||||
|
|
||||||
|
topic = Topic.where(
|
||||||
|
title: "Topic Title",
|
||||||
|
category_id: category.id
|
||||||
|
).first
|
||||||
|
|
||||||
|
topic
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
CustomWizard::Template.save(wizard_template, skip_jobs: true)
|
||||||
|
@template = CustomWizard::Template.find('super_mega_fun_wizard')
|
||||||
|
end
|
||||||
|
|
||||||
|
context "topic created by user using wizard" do
|
||||||
|
it "allows editing the topic first post" do
|
||||||
|
wizard = CustomWizard::Builder.new(@template[:id], user).build
|
||||||
|
topic = create_topic_by_wizard(wizard)
|
||||||
|
expect(user.guardian.wizard_can_edit_topic?(topic)).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "topic created by user without wizard" do
|
||||||
|
it "restricts editing the topic first post" do
|
||||||
|
topic_params = {
|
||||||
|
title: "Topic Title",
|
||||||
|
raw: "Topic body",
|
||||||
|
skip_validations: true
|
||||||
|
}
|
||||||
|
post = PostCreator.new(user, topic_params).create
|
||||||
|
expect(user.guardian.wizard_can_edit_topic?(post.topic)).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
spec/fixtures/condition/boolean_field_condition.json
gevendort
Normale Datei
17
spec/fixtures/condition/boolean_field_condition.json
gevendort
Normale Datei
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"condition": [
|
||||||
|
{
|
||||||
|
"type": "validation",
|
||||||
|
"pairs": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"key": "step_2_field_5",
|
||||||
|
"key_type": "wizard_field",
|
||||||
|
"value": "true",
|
||||||
|
"value_type": "text",
|
||||||
|
"connector": "is"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
13
spec/fixtures/wizard.json
gevendort
13
spec/fixtures/wizard.json
gevendort
|
@ -43,6 +43,13 @@
|
||||||
"label": "I'm only text",
|
"label": "I'm only text",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "text_only"
|
"type": "text_only"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "step_1_field_5",
|
||||||
|
"label": "I'm a preview",
|
||||||
|
"description": "",
|
||||||
|
"type": "composer_preview",
|
||||||
|
"preview_template": "w{step_1_field_1}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Text inputs!"
|
"description": "Text inputs!"
|
||||||
|
@ -157,6 +164,12 @@
|
||||||
"label": "User Selector",
|
"label": "User Selector",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "user_selector"
|
"type": "user_selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "step_3_field_6",
|
||||||
|
"label": "Conditional User Selector",
|
||||||
|
"description": "Shown when checkbox in step_2_field_5 is true",
|
||||||
|
"type": "user_selector"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Unfortunately not the edible type :sushi: "
|
"description": "Unfortunately not the edible type :sushi: "
|
||||||
|
|
|
@ -15,11 +15,8 @@ describe CustomWizard::AdminManagerController do
|
||||||
|
|
||||||
template_2 = template.dup
|
template_2 = template.dup
|
||||||
template_2["id"] = 'super_mega_fun_wizard_2'
|
template_2["id"] = 'super_mega_fun_wizard_2'
|
||||||
|
|
||||||
template_3 = template.dup
|
template_3 = template.dup
|
||||||
template_3["id"] = 'super_mega_fun_wizard_3'
|
template_3["id"] = 'super_mega_fun_wizard_3'
|
||||||
template_3["after_signup"] = true
|
|
||||||
|
|
||||||
@template_array = [template, template_2, template_3]
|
@template_array = [template, template_2, template_3]
|
||||||
|
|
||||||
FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder)
|
FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder)
|
||||||
|
|
|
@ -31,24 +31,67 @@ describe ApplicationController do
|
||||||
user.save_custom_fields(true)
|
user.save_custom_fields(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects if user is required to complete a wizard" do
|
it "does not redirect if wizard if no after setting is enabled" do
|
||||||
get "/"
|
|
||||||
expect(response).to redirect_to("/w/super-mega-fun-wizard")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "saves original destination of user" do
|
|
||||||
get '/', headers: { 'REFERER' => "/t/2" }
|
|
||||||
expect(
|
|
||||||
CustomWizard::Wizard.create(@template['id'], user).submissions
|
|
||||||
.first.redirect_to
|
|
||||||
).to eq("/t/2")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not redirect if wizard does not exist" do
|
|
||||||
CustomWizard::Template.remove('super_mega_fun_wizard')
|
|
||||||
get "/"
|
get "/"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "after signup enabled" do
|
||||||
|
before do
|
||||||
|
@template["after_signup"] = true
|
||||||
|
CustomWizard::Template.save(@template)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not redirect if wizard does not exist" do
|
||||||
|
CustomWizard::Template.remove(@template[:id])
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects if user is required to complete a wizard" do
|
||||||
|
get "/"
|
||||||
|
expect(response).to redirect_to("/w/super-mega-fun-wizard")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not redirect if wizard is subsequently disabled" do
|
||||||
|
get "/"
|
||||||
|
expect(response).to redirect_to("/w/super-mega-fun-wizard")
|
||||||
|
|
||||||
|
@template["after_signup"] = false
|
||||||
|
CustomWizard::Template.save(@template)
|
||||||
|
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "saves original destination of user" do
|
||||||
|
get '/', headers: { 'REFERER' => "/t/2" }
|
||||||
|
expect(
|
||||||
|
CustomWizard::Wizard.create(@template['id'], user).submissions
|
||||||
|
.first.redirect_to
|
||||||
|
).to eq("/t/2")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "after time enabled" do
|
||||||
|
before do
|
||||||
|
@template["after_time"] = true
|
||||||
|
@template["after_time_scheduled"] = (Time.now + 3.hours).iso8601
|
||||||
|
CustomWizard::Template.save(@template)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not redirect if time hasn't passed" do
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects if time has passed" do
|
||||||
|
@template["after_time_scheduled"] = (Time.now - 1.hours).iso8601
|
||||||
|
CustomWizard::Template.save(@template)
|
||||||
|
get "/"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "who is not required to complete wizard" do
|
context "who is not required to complete wizard" do
|
||||||
|
|
|
@ -79,6 +79,15 @@ describe CustomWizard::WizardController do
|
||||||
expect(response.parsed_body['redirect_to']).to eq('/t/2')
|
expect(response.parsed_body['redirect_to']).to eq('/t/2')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'deletes the users redirect_to_wizard if present' do
|
||||||
|
user.custom_fields['redirect_to_wizard'] = @template["id"]
|
||||||
|
user.save_custom_fields(true)
|
||||||
|
@wizard = CustomWizard::Wizard.create(@template["id"], user)
|
||||||
|
put '/w/super-mega-fun-wizard/skip.json'
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(user.reload.redirect_to_wizard).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
it "deletes the submission if user has filled up some data" do
|
it "deletes the submission if user has filled up some data" do
|
||||||
@wizard = CustomWizard::Wizard.create(@template["id"], user)
|
@wizard = CustomWizard::Wizard.create(@template["id"], user)
|
||||||
CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save
|
CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe CustomWizard::FieldSerializer do
|
||||||
scope: Guardian.new(user)
|
scope: Guardian.new(user)
|
||||||
).as_json
|
).as_json
|
||||||
|
|
||||||
expect(json_array.size).to eq(4)
|
expect(json_array.size).to eq(@wizard.steps.first.fields.size)
|
||||||
expect(json_array[0][:label]).to eq("<p>Text</p>")
|
expect(json_array[0][:label]).to eq("<p>Text</p>")
|
||||||
expect(json_array[0][:description]).to eq("Text field description.")
|
expect(json_array[0][:description]).to eq("Text field description.")
|
||||||
expect(json_array[3][:index]).to eq(3)
|
expect(json_array[3][:index]).to eq(3)
|
||||||
|
|
|
@ -43,7 +43,8 @@ describe CustomWizard::StepSerializer do
|
||||||
each_serializer: described_class,
|
each_serializer: described_class,
|
||||||
scope: Guardian.new(user)
|
scope: Guardian.new(user)
|
||||||
).as_json
|
).as_json
|
||||||
expect(json_array[0][:fields].length).to eq(4)
|
|
||||||
|
expect(json_array[0][:fields].length).to eq(@wizard.steps[0].fields.length)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with required data' do
|
context 'with required data' do
|
||||||
|
|
Laden …
In neuem Issue referenzieren