0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-30 04:30:29 +01:00

Merge branch 'main' into stable

Dieser Commit ist enthalten in:
angusmcleod 2022-01-31 21:50:12 +08:00
Commit 52fe6e2baa
60 geänderte Dateien mit 6199 neuen und 436 gelöschten Zeilen

Datei anzeigen

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

Datei anzeigen

@ -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
Datei anzeigen

@ -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.");
}

Datei anzeigen

@ -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;

Datei anzeigen

@ -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)
); );
}, },

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

@ -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);
}) })

Datei anzeigen

@ -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();
}
},
},
});
});
}, },
}; };

Datei anzeigen

@ -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) {

Datei anzeigen

@ -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;

Datei anzeigen

@ -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);

Datei anzeigen

@ -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}}

Datei anzeigen

@ -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>

Datei anzeigen

@ -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}}

Datei anzeigen

@ -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

Datei anzeigen

@ -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.[]",

Datei anzeigen

@ -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();
}, },
}, },
}); });

Datei anzeigen

@ -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()));
});
}, },
}); });

Datei anzeigen

@ -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") || "",
}) })
); );
}, },

Datei anzeigen

@ -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}`;
didInsertElement() { }),
this._super(); isImage: computed("field.value.extension", function () {
return (
const $upload = $(this.element); this.field.value &&
this.siteSettings.wizard_recognised_image_upload_formats
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("|") .split("|")
.includes(response.result.extension) .includes(this.field.value.extension)
) { );
this.setProperties({ }),
isImage: true,
});
}
});
$upload.on("fileuploadfail", (e, response) => { uploadDone(upload) {
let message = WizardI18n("wizard.upload_error"); this.set("field.value", upload);
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);
});
}, },
}); });

Datei anzeigen

@ -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"));
}
}); });
}, },
}; };

Datei anzeigen

@ -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"),
}); });
} }

Datei anzeigen

@ -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,
}, },
}); });

Datei anzeigen

@ -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" })
.then((result) => {
CustomWizard.finished(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) {

Datei anzeigen

@ -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}}

Datei anzeigen

@ -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}}

Datei anzeigen

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

Datei anzeigen

@ -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;

Datei anzeigen

@ -173,4 +173,8 @@
} }
} }
} }
.wizard-category-selector {
width: 500px;
}
} }

Datei anzeigen

@ -28,8 +28,6 @@
margin-bottom: 0; margin-bottom: 0;
p { p {
line-height: 1.7;
img { img {
@extend img.emoji; @extend img.emoji;
} }

Datei anzeigen

@ -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";

Datei anzeigen

@ -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"

Datei anzeigen

@ -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."

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -1,5 +1,5 @@
{ {
"result": { "result": {
"line": 92.09 "line": 92.52
} }
} }

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

17
extensions/guardian.rb Normale Datei
Datei anzeigen

@ -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

Datei anzeigen

@ -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)

Datei anzeigen

@ -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

Datei anzeigen

@ -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(

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -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"
} }
} }

Datei anzeigen

@ -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 request.format === 'text/html' && !@excluded_routes.any? { |str| /#{str}/ =~ url } && wizard_id if not_api && !excluded_route
if request.referer !~ /\/w\// && request.referer !~ /\/invites\// wizard_id = current_user.redirect_to_wizard
CustomWizard::Wizard.set_wizard_redirect(current_user, wizard_id, request.referer)
if CustomWizard::Template.can_redirect_users?(wizard_id)
if url !~ /\/w\// && url !~ /\/invites\//
CustomWizard::Wizard.set_wizard_redirect(current_user, wizard_id, url)
end 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)

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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"
}
]
}
]
}

Datei anzeigen

@ -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: "

Datei anzeigen

@ -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)

Datei anzeigen

@ -31,11 +31,39 @@ describe ApplicationController do
user.save_custom_fields(true) user.save_custom_fields(true)
end end
it "does not redirect if wizard if no after setting is enabled" do
get "/"
expect(response.status).to eq(200)
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 it "redirects if user is required to complete a wizard" do
get "/" get "/"
expect(response).to redirect_to("/w/super-mega-fun-wizard") expect(response).to redirect_to("/w/super-mega-fun-wizard")
end 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 it "saves original destination of user" do
get '/', headers: { 'REFERER' => "/t/2" } get '/', headers: { 'REFERER' => "/t/2" }
expect( expect(
@ -43,12 +71,27 @@ describe ApplicationController do
.first.redirect_to .first.redirect_to
).to eq("/t/2") ).to eq("/t/2")
end end
end
it "does not redirect if wizard does not exist" do context "after time enabled" do
CustomWizard::Template.remove('super_mega_fun_wizard') 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 "/" get "/"
expect(response.status).to eq(200) expect(response.status).to eq(200)
end 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

Datei anzeigen

@ -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

Datei anzeigen

@ -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)

Datei anzeigen

@ -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