diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml index a121658d..a79c5462 100644 --- a/.github/workflows/plugin-linting.yml +++ b/.github/workflows/plugin-linting.yml @@ -6,6 +6,8 @@ on: - master - main pull_request: + schedule: + - cron: '0 0 * * *' jobs: build: diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index ce6112af..fcf1a1d0 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -6,6 +6,8 @@ on: - master - main pull_request: + schedule: + - cron: '0 */12 * * *' jobs: build: @@ -51,23 +53,27 @@ jobs: repository: discourse/discourse fetch-depth: 1 + - name: Fetch Repo Name + id: repo-name + run: echo "::set-output name=value::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" + - name: Install plugin uses: actions/checkout@v2 with: - path: plugins/${{ github.event.repository.name }} + path: plugins/${{ steps.repo-name.outputs.value }} fetch-depth: 1 - name: Check spec existence id: check_spec uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ github.event.repository.name }}/spec" + files: "plugins/${{ steps.repo-name.outputs.value }}/spec" - name: Check qunit existence id: check_qunit uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ github.event.repository.name }}/test/javascripts" + files: "plugins/${{ steps.repo-name.outputs.value }}/test/javascripts" - name: Setup Git run: | @@ -100,7 +106,7 @@ jobs: - name: Lint English locale if: matrix.build_type == 'backend' - run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml" + run: bundle exec ruby script/i18n_lint.rb "plugins/${{ steps.repo-name.outputs.value }}/locales/{client,server}.en.yml" - name: Get yarn cache directory id: yarn-cache-dir @@ -123,15 +129,11 @@ jobs: bin/rake db:create bin/rake db:migrate - - name: Plugin RSpec + - name: Plugin RSpec with Coverage if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exists == 'true' - run: bin/rake plugin:spec[${{ github.event.repository.name }}] + run: SIMPLECOV=1 bin/rake plugin:spec[${{ steps.repo-name.outputs.value }}] - name: Plugin QUnit if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exists == 'true' - run: bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000'] + run: bundle exec rake plugin:qunit['${{ steps.repo-name.outputs.value }}','1200000'] timeout-minutes: 30 - - - name: Simplecov Report - if: matrix.build_type == 'backend' - run: COVERAGE=1 bin/rake plugin:spec[${{ github.event.repository.name }}] diff --git a/assets/javascripts/discourse/components/custom-field-input.js.es6 b/assets/javascripts/discourse/components/custom-field-input.js.es6 index f2dca4c7..e49c6f1d 100644 --- a/assets/javascripts/discourse/components/custom-field-input.js.es6 +++ b/assets/javascripts/discourse/components/custom-field-input.js.es6 @@ -1,6 +1,6 @@ import Component from "@ember/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { alias, or } from "@ember/object/computed"; +import { alias, equal, or } from "@ember/object/computed"; import I18n from "I18n"; const generateContent = function (array, type) { @@ -29,6 +29,7 @@ export default Component.extend({ loading: or("saving", "destroying"), destroyDisabled: alias("loading"), closeDisabled: alias("loading"), + isExternal: equal("field.id", "external"), didInsertElement() { this.set("originalField", JSON.parse(JSON.stringify(this.field))); @@ -61,13 +62,14 @@ export default Component.extend({ @discourseComputed( "saving", + "isExternal", "field.name", "field.klass", "field.type", "field.serializers" ) - saveDisabled(saving) { - if (saving) { + saveDisabled(saving, isExternal) { + if (saving || isExternal) { return true; } diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index c8309f10..feb83754 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -62,6 +62,11 @@ export default Component.extend(UndoChanges, { return key; }, + @discourseComputed("action.type") + customFieldsContext(type) { + return `action.${type}`; + }, + @discourseComputed("wizard.steps") runAfterContent(steps) { let content = steps.map(function (step) { diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index b5f6b0ee..b5c10c65 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -25,6 +25,7 @@ export default Component.extend(UndoChanges, { showContent: or("isCategory", "isTag", "isGroup", "isDropdown"), showLimit: or("isCategory", "isTag"), isTextType: or("isText", "isTextarea", "isComposer"), + isComposerPreview: equal("field.type", "composer_preview"), categoryPropertyTypes: selectKitContent(["id", "slug"]), showAdvanced: alias("field.type"), messageUrl: "https://thepavilion.io/t/2809", diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index 6d65d782..7d9b0bbd 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -6,11 +6,24 @@ import { } from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; import { defaultSelectionType, selectionTypes } from "../lib/wizard-mapper"; -import { generateName, snakeCase, userProperties } from "../lib/wizard"; +import { + generateName, + sentenceCase, + snakeCase, + userProperties, +} from "../lib/wizard"; import Component from "@ember/component"; import { bind, later } from "@ember/runloop"; import I18n from "I18n"; +const customFieldActionMap = { + topic: ["create_topic", "send_message"], + post: ["create_topic", "send_message"], + category: ["create_category"], + group: ["create_group"], + user: ["update_profile"], +}; + export default Component.extend({ classNameBindings: [":mapper-selector", "activeType"], @@ -188,11 +201,19 @@ export default Component.extend({ customFields ) { let content; + let context; + let contextType; + + if (this.options.context) { + let contextAttrs = this.options.context.split("."); + context = contextAttrs[0]; + contextType = contextAttrs[1]; + } if (activeType === "wizardField") { content = wizardFields; - if (this.options.context === "field") { + if (context === "field") { content = content.filter((field) => field.id !== currentFieldId); } } @@ -204,7 +225,7 @@ export default Component.extend({ type: a.type, })); - if (this.options.context === "action") { + if (context === "action") { content = content.filter((a) => a.id !== currentActionId); } } @@ -218,7 +239,7 @@ export default Component.extend({ .concat(userFields || []); if ( - this.options.context === "action" && + context === "action" && this.inputType === "association" && this.selectorType === "key" ) { @@ -234,7 +255,17 @@ export default Component.extend({ } if (activeType === "customField") { - content = customFields; + content = customFields + .filter((f) => { + return ( + f.type !== "json" && + customFieldActionMap[f.klass].includes(contextType) + ); + }) + .map((f) => ({ + id: f.name, + name: `${sentenceCase(f.klass)} ${f.name} (${f.type})`, + })); } return content; diff --git a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 index 2081cfe3..404c6afd 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 @@ -3,12 +3,12 @@ import CustomWizardCustomField from "../models/custom-wizard-custom-field"; export default Controller.extend({ messageKey: "create", - fieldKeys: ["klass", "type", "serializers", "name"], + fieldKeys: ["klass", "type", "name", "serializers"], documentationUrl: "https://thepavilion.io/t/3572", actions: { addField() { - this.get("customFields").pushObject( + this.get("customFields").unshiftObject( CustomWizardCustomField.create({ edit: true }) ); }, diff --git a/assets/javascripts/discourse/lib/wizard.js.es6 b/assets/javascripts/discourse/lib/wizard.js.es6 index 1896b1fe..98bdbfdd 100644 --- a/assets/javascripts/discourse/lib/wizard.js.es6 +++ b/assets/javascripts/discourse/lib/wizard.js.es6 @@ -120,4 +120,5 @@ export { listProperties, notificationLevels, wizardFieldList, + sentenceCase, }; diff --git a/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 index abcaeb9e..73168ff3 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 @@ -1,17 +1,19 @@ import CustomWizard from "../models/custom-wizard"; import DiscourseRoute from "discourse/routes/discourse"; +const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"]; + export default DiscourseRoute.extend({ model(params) { return CustomWizard.submissions(params.wizardId); }, setupController(controller, model) { - if (model.submissions) { - let fields = []; + if (model && model.submissions) { + let fields = ["username"]; model.submissions.forEach((s) => { - Object.keys(s).forEach((k) => { - if (fields.indexOf(k) < 0) { + Object.keys(s.fields).forEach((k) => { + if (!excludedMetaFields.includes(k) && fields.indexOf(k) < 0) { fields.push(k); } }); @@ -19,9 +21,13 @@ export default DiscourseRoute.extend({ let submissions = []; model.submissions.forEach((s) => { - let submission = {}; - fields.forEach((f) => { - submission[f] = s[f]; + let submission = { + username: s.username, + }; + Object.keys(s.fields).forEach((f) => { + if (fields.includes(f)) { + submission[f] = s.fields[f]; + } }); submissions.push(submission); }); diff --git a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 index eaa6591c..cb2d54c3 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 @@ -2,7 +2,6 @@ import CustomWizard from "../models/custom-wizard"; import { ajax } from "discourse/lib/ajax"; import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; -import { selectKitContent } from "../lib/wizard"; export default DiscourseRoute.extend({ model(params) { @@ -33,9 +32,7 @@ export default DiscourseRoute.extend({ wizardList: parentModel.wizard_list, fieldTypes, userFields: parentModel.userFields, - customFields: selectKitContent( - parentModel.custom_fields.map((f) => f.name) - ), + customFields: parentModel.custom_fields, apis: parentModel.apis, themes: parentModel.themes, wizard, diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs index 18fd3fdb..b0dd3de6 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs @@ -3,7 +3,7 @@ {{d-button label="refresh" - icon="refresh" + icon="sync" action="refresh" class="refresh"}} diff --git a/assets/javascripts/discourse/templates/components/custom-field-input.hbs b/assets/javascripts/discourse/templates/components/custom-field-input.hbs index 205b1644..43a97be8 100644 --- a/assets/javascripts/discourse/templates/components/custom-field-input.hbs +++ b/assets/javascripts/discourse/templates/components/custom-field-input.hbs @@ -13,6 +13,11 @@ none="admin.wizard.custom_field.type.select" onChange=(action (mut field.type))}} + + {{input + value=field.name + placeholder=(i18n "admin.wizard.custom_field.name.select")}} + {{multi-select value=field.serializers @@ -20,11 +25,6 @@ none="admin.wizard.custom_field.serializers.select" onChange=(action (mut field.serializers))}} - - {{input - value=field.name - placeholder=(i18n "admin.wizard.custom_field.name.select")}} - {{#if loading}} {{loading-spinner size="small"}} @@ -51,13 +51,25 @@ {{else}} - - {{#each field.serializers as |serializer|}} - - {{/each}} - - - {{d-button action="edit" icon="pencil-alt"}} + + {{#if isExternal}} + — + {{else}} + {{#each field.serializers as |serializer|}} + + {{/each}} + {{/if}} + {{#if isExternal}} + + + + {{else}} + + {{d-button action="edit" icon="pencil-alt"}} + + {{/if}} {{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index f06e0d89..4c645cf7 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -738,7 +738,7 @@ wizardActionSelection="value" userFieldSelection="value" keyPlaceholder="admin.wizard.action.custom_fields.key" - context="action" + context=customFieldsContext )}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index 2677c9dd..db68170a 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -127,6 +127,31 @@ checked=field.char_counter}} + +
+
+ +
+ +
+ {{textarea + name="field_placeholder" + class="medium" + value=field.placeholder}} +
+
+{{/if}} + +{{#if isComposerPreview}} +
+
+ +
+ +
+ {{textarea name="preview-template" value=field.preview_template}} +
+
{{/if}} {{#if isUpload}} diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index 5d18328f..8b30ad94 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -1,43 +1,4 @@ -//= require discourse/app/lib/autocomplete -//= require discourse/app/lib/utilities -//= require discourse/app/lib/offset-calculator -//= require discourse/app/lib/lock-on -//= require discourse/app/lib/text-direction -//= require discourse/app/lib/to-markdown -//= require discourse/app/lib/load-script -//= require discourse/app/lib/url -//= require discourse/app/lib/ajax -//= require discourse/app/lib/ajax-error -//= require discourse/app/lib/page-visible -//= require discourse/app/lib/logout -//= require discourse/app/lib/render-tag -//= require discourse/app/lib/notification-levels -//= require discourse/app/lib/computed -//= require discourse/app/lib/user-search -//= require discourse/app/lib/text -//= require discourse/app/lib/formatter -//= require discourse/app/lib/quote -//= require discourse/app/lib/link-mentions -//= require discourse/app/lib/link-hashtags -//= require discourse/app/lib/category-hashtags -//= require discourse/app/lib/tag-hashtags -//= require discourse/app/lib/uploads -//= require discourse/app/lib/category-tag-search -//= require discourse/app/lib/intercept-click -//= require discourse/app/lib/show-modal -//= require discourse/app/lib/key-value-store -//= require discourse/app/lib/settings -//= require discourse/app/lib/user-presence -//= require discourse/app/lib/hash -//= require discourse/app/lib/bookmark -//= require discourse/app/lib/put-cursor-at-end -//= require discourse/app/lib/safari-hacks -//= require discourse/app/lib/preload-store -//= require discourse/app/lib/topic-fancy-title -//= require discourse/app/lib/cookie -//= require discourse/app/lib/public-js-versions -//= require discourse/app/lib/load-oneboxes -//= require discourse/app/lib/highlight-syntax +//= require_tree_discourse discourse/app/lib //= require discourse/app/mixins/singleton //= require discourse/app/mixins/upload @@ -46,35 +7,7 @@ //= require message-bus -//= require discourse/app/models/login-method -//= require discourse/app/models/permission-type -//= require discourse/app/models/archetype -//= require discourse/app/models/rest -//= require discourse/app/models/site -//= require discourse/app/models/category -//= require discourse/app/models/session -//= require discourse/app/models/post-action-type -//= require discourse/app/models/trust-level -//= require discourse/app/models/store -//= require discourse/app/models/result-set -//= require discourse/app/models/bookmark -//= require discourse/app/models/user -//= require discourse/app/models/user-stream -//= require discourse/app/models/user-action -//= require discourse/app/models/user-action-group -//= require discourse/app/models/user-posts-stream -//= require discourse/app/models/badge -//= require discourse/app/models/badge-grouping -//= require discourse/app/models/user-badge -//= require discourse/app/models/topic -//= require discourse/app/models/action-summary -//= require discourse/app/models/user-action-stat -//= require discourse/app/models/user-drafts-stream -//= require discourse/app/models/user-draft -//= require discourse/app/models/composer -//= require discourse/app/models/draft -//= require discourse/app/models/group -//= require discourse/app/models/group-history +//= require_tree_discourse discourse/app/models //= require discourse/app/helpers/category-link //= require discourse/app/helpers/user-avatar diff --git a/assets/javascripts/wizard/components/wizard-date-input.js.es6 b/assets/javascripts/wizard/components/wizard-date-input.js.es6 index 93c7ed2d..bb11b655 100644 --- a/assets/javascripts/wizard/components/wizard-date-input.js.es6 +++ b/assets/javascripts/wizard/components/wizard-date-input.js.es6 @@ -1,3 +1,42 @@ import DateInput from "discourse/components/date-input"; +import loadScript from "discourse/lib/load-script"; +import discourseComputed from "discourse-common/utils/decorators"; +import I18n from "I18n"; +/* global Pikaday:true */ -export default DateInput.extend(); +export default DateInput.extend({ + useNativePicker: false, + + @discourseComputed() + placeholder() { + return this.format; + }, + + _loadPikadayPicker(container) { + return loadScript("/javascripts/pikaday.js").then(() => { + let defaultOptions = { + field: this.element.querySelector(".date-picker"), + container: container || this.element.querySelector(".picker-container"), + bound: container === null, + format: this.format, + firstDay: 1, + i18n: { + previousMonth: I18n.t("dates.previous_month"), + nextMonth: I18n.t("dates.next_month"), + months: moment.months(), + weekdays: moment.weekdays(), + weekdaysShort: moment.weekdaysShort(), + }, + onSelect: (date) => this._handleSelection(date), + }; + + if (this.relativeDate) { + defaultOptions = Object.assign({}, defaultOptions, { + minDate: moment(this.relativeDate).toDate(), + }); + } + + return new Pikaday(Object.assign({}, defaultOptions, this._opts())); + }); + }, +}); diff --git a/assets/javascripts/wizard/components/wizard-field-composer-preview.js.es6 b/assets/javascripts/wizard/components/wizard-field-composer-preview.js.es6 new file mode 100644 index 00000000..b49233f2 --- /dev/null +++ b/assets/javascripts/wizard/components/wizard-field-composer-preview.js.es6 @@ -0,0 +1,49 @@ +import Component from "@ember/component"; +import { loadOneboxes } from "discourse/lib/load-oneboxes"; +import { schedule } from "@ember/runloop"; +import discourseDebounce from "discourse-common/lib/debounce"; +import { resolveAllShortUrls } from "pretty-text/upload-short-url"; +import { ajax } from "discourse/lib/ajax"; +import { on } from "discourse-common/utils/decorators"; + +export default Component.extend({ + @on("init") + updatePreview() { + if (this.isDestroyed) { + return; + } + + schedule("afterRender", () => { + if (this._state !== "inDOM" || !this.element) { + return; + } + + const $preview = $(this.element); + + if ($preview.length === 0) { + return; + } + + this.previewUpdated($preview); + }); + }, + + previewUpdated($preview) { + // Paint oneboxes + const paintFunc = () => { + loadOneboxes( + $preview[0], + ajax, + null, + null, + this.siteSettings.max_oneboxes_per_post, + true // refresh on every load + ); + }; + + discourseDebounce(this, paintFunc, 450); + + // Short upload urls need resolution + resolveAllShortUrls(ajax, this.siteSettings, $preview[0]); + }, +}); diff --git a/assets/javascripts/wizard/custom-wizard.js.es6 b/assets/javascripts/wizard/custom-wizard.js.es6 index 63a9ea10..8c0a473c 100644 --- a/assets/javascripts/wizard/custom-wizard.js.es6 +++ b/assets/javascripts/wizard/custom-wizard.js.es6 @@ -4,6 +4,10 @@ export default Ember.Application.extend({ rootElement: "#custom-wizard-main", Resolver: buildResolver("wizard"), + customEvents: { + paste: "paste", + }, + start() { Object.keys(requirejs._eak_seen).forEach((key) => { if (/\/pre\-initializers\//.test(key)) { diff --git a/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 index ef45e949..0e9e60f4 100644 --- a/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 +++ b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 @@ -15,7 +15,7 @@ export default { ); const DEditor = requirejs("discourse/components/d-editor").default; const { clipboardHelpers } = requirejs("discourse/lib/utilities"); - const { toMarkdown } = requirejs("discourse/lib/to-markdown"); + const toMarkdown = requirejs("discourse/lib/to-markdown").default; /* * unit: custom_wizard:templates_and_builder @@ -192,7 +192,7 @@ export default { markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown; } - this.appEvents.trigger("composer:insert-text", { + this.appEvents.trigger("wizard-editor:insert-text", { fieldId: this.fieldId, text: markdown, }); diff --git a/assets/javascripts/wizard/initializers/custom-wizard.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard.js.es6 index ab7c9146..ca396081 100644 --- a/assets/javascripts/wizard/initializers/custom-wizard.js.es6 +++ b/assets/javascripts/wizard/initializers/custom-wizard.js.es6 @@ -26,7 +26,9 @@ export default { const setDefaultOwner = requirejs("discourse-common/lib/get-owner") .setDefaultOwner; const messageBus = requirejs("message-bus-client").default; - + const getToken = requirejs("wizard/lib/ajax").getToken; + const setEnvironment = requirejs("discourse-common/config/environment") + .setEnvironment; const container = app.__container__; Discourse.Model = EmberObject.extend(); Discourse.__container__ = container; @@ -89,6 +91,7 @@ export default { const session = container.lookup("session:main"); const setupData = document.getElementById("data-discourse-setup").dataset; session.set("highlightJsPath", setupData.highlightJsPath); + setEnvironment(setupData.environment); Router.reopen({ rootURL: getUrl("/w/"), @@ -107,5 +110,9 @@ export default { }, model() {}, }); + + $.ajaxPrefilter(function (_, __, jqXHR) { + jqXHR.setRequestHeader("X-CSRF-Token", getToken()); + }); }, }; diff --git a/assets/javascripts/wizard/lib/text-lite.js.es6 b/assets/javascripts/wizard/lib/text-lite.js.es6 index 4f9064a5..c93f6708 100644 --- a/assets/javascripts/wizard/lib/text-lite.js.es6 +++ b/assets/javascripts/wizard/lib/text-lite.js.es6 @@ -1,8 +1,17 @@ import loadScript from "./load-script"; -import { default as PrettyText } from "pretty-text/pretty-text"; +import { default as PrettyText, buildOptions } from "pretty-text/pretty-text"; import Handlebars from "handlebars"; +import getURL from "discourse-common/lib/get-url"; +import { getOwner } from "discourse-common/lib/get-owner"; export function cook(text, options) { + if (!options) { + options = buildOptions({ + getURL: getURL, + siteSettings: getOwner(this).lookup("site-settings:main"), + }); + } + return new Handlebars.SafeString(new PrettyText(options).cook(text)); } diff --git a/assets/javascripts/wizard/lib/wizard-i18n.js.es6 b/assets/javascripts/wizard/lib/wizard-i18n.js.es6 index 17242e58..fdefab77 100644 --- a/assets/javascripts/wizard/lib/wizard-i18n.js.es6 +++ b/assets/javascripts/wizard/lib/wizard-i18n.js.es6 @@ -1,7 +1,10 @@ import I18n from "I18n"; const getThemeId = () => { - let themeId = parseInt($("meta[name=discourse_theme_ids]")[0].content, 10); + let themeId = parseInt( + document.querySelector("meta[name=discourse_theme_id]").content, + 10 + ); if (!isNaN(themeId)) { return themeId.toString(); diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs index 1e7c27df..c4bf1c74 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -1,7 +1,7 @@ {{d-editor tabindex=field.tabindex value=composer.reply - placeholder=replyPlaceholder + placeholderTranslated=replyPlaceholder previewUpdated=(action "previewUpdated") markdownOptions=markdownOptions extraButtons=(action "extraButtons") diff --git a/assets/javascripts/wizard/templates/components/wizard-field-composer-preview.hbs b/assets/javascripts/wizard/templates/components/wizard-field-composer-preview.hbs new file mode 100644 index 00000000..508cf31d --- /dev/null +++ b/assets/javascripts/wizard/templates/components/wizard-field-composer-preview.hbs @@ -0,0 +1,5 @@ +
+
+ {{html-safe field.preview_template}} +
+
diff --git a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs index 4ac6571b..ed4d14e3 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs @@ -2,4 +2,5 @@ date=date onChange=(action "onChange") tabindex=field.tabindex + format=field.format }} diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index 3c4b78da..66cc6b43 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -15,7 +15,7 @@ } .wizard-message { - background-color: $primary-low; + background-color: var(--primary-low); width: 100%; padding: 10px; box-sizing: border-box; @@ -37,7 +37,7 @@ } a + a { - border-left: 1px solid $primary-medium; + border-left: 1px solid var(--primary-medium); padding-left: 5px; margin-left: 5px; } @@ -89,7 +89,7 @@ .wizard-settings-parent { padding: 20px; - border: 1px solid $primary-low; + border: 1px solid var(--primary-low); } .wizard-settings-group { @@ -115,7 +115,7 @@ .wizard-custom-field { background: transparent; - background-color: dark-light-diff($primary, $secondary, 96%, -65%); + background-color: var(--primary-very-low); padding: 20px; } @@ -182,7 +182,7 @@ a { padding: 6px 12px; font-size: 1rem; - background-color: $primary-low; + background-color: var(--primary-low); } button { @@ -256,6 +256,10 @@ width: 100%; box-sizing: border-box; margin-bottom: 0; + + &.medium { + width: 40%; + } } input[type="number"] { @@ -263,7 +267,7 @@ } input[disabled] { - background-color: $primary-low; + background-color: var(--primary-low); cursor: not-allowed; } @@ -434,8 +438,8 @@ display: none; margin: 0 0 10px 0; padding: 10px; - background-color: $secondary; - border: 1px solid $primary-medium; + background-color: var(--secondary); + border: 1px solid var(--primary-medium); max-width: 100%; &.force-preview { @@ -454,17 +458,17 @@ .btn { margin-right: 10px; - color: $primary; + color: var(--primary); &:hover { - color: $secondary; + color: var(--secondary); } } .wizard-editor-gutter-popover { position: absolute; padding: 10px; - background-color: $secondary; + background-color: var(--secondary); box-shadow: shadow("card"); z-index: 200; top: 40px; @@ -502,7 +506,7 @@ width: 100px; display: inline-block; vertical-align: top; - background-color: $primary-low; + background-color: var(--primary-low); margin-right: 10px; margin-left: 3px; } @@ -587,8 +591,8 @@ .add-mapper-input .btn, .btn-after-time, .wizard-editor-gutter .btn { - background-color: $secondary; - border: 1px solid $primary-medium; + background-color: var(--secondary); + border: 1px solid var(--primary-medium); } .admin-wizards-custom-fields { @@ -667,6 +671,10 @@ margin-left: 5px !important; } } + + td.external { + font-style: italic; + } } } diff --git a/assets/stylesheets/common/wizard-api.scss b/assets/stylesheets/common/wizard-api.scss index 4fdd0fc2..9d0ad261 100644 --- a/assets/stylesheets/common/wizard-api.scss +++ b/assets/stylesheets/common/wizard-api.scss @@ -48,7 +48,7 @@ .wizard-api-authentication { display: flex; - background-color: $primary-very-low; + background-color: var(--primary-very-low); padding: 20px; margin-bottom: 20px; @@ -68,7 +68,7 @@ } .status { - border-left: 1px solid $primary; + border-left: 1px solid var(--primary); margin-left: 20px; padding-left: 20px; width: 50%; @@ -89,7 +89,7 @@ } .wizard-api-endpoints { - background-color: $primary-very-low; + background-color: var(--primary-very-low); padding: 20px; margin-bottom: 20px; diff --git a/assets/stylesheets/common/wizard-manager.scss b/assets/stylesheets/common/wizard-manager.scss index 135ddb8c..a3b30c98 100644 --- a/assets/stylesheets/common/wizard-manager.scss +++ b/assets/stylesheets/common/wizard-manager.scss @@ -16,13 +16,13 @@ #import-button:enabled, #export-button:enabled { - background-color: $tertiary; - color: $secondary; + background-color: var(--tertiary); + color: var(--secondary); } #destroy-button:enabled { - background-color: $danger; - color: $secondary; + background-color: var(--danger); + color: var(--secondary); } } @@ -32,13 +32,13 @@ .filename { padding: 0 10px; - border: 1px solid $primary; + border: 1px solid var(--primary); display: inline-flex; height: 28px; line-height: 28px; a { - color: $primary; + color: var(--primary); margin-right: 5px; display: inline-flex; align-items: center; diff --git a/assets/stylesheets/common/wizard-mapper.scss b/assets/stylesheets/common/wizard-mapper.scss index 69e9c88e..f56a2cab 100644 --- a/assets/stylesheets/common/wizard-mapper.scss +++ b/assets/stylesheets/common/wizard-mapper.scss @@ -22,7 +22,7 @@ width: min-content; margin-bottom: 10px; height: 20px; - border: 2px solid $primary-low; + border: 2px solid var(--primary-low); } } @@ -33,8 +33,8 @@ position: relative; padding: 25px 7px 7px 7px; margin-bottom: 10px; - background: rgba($secondary, 0.5); - border: 2px solid $primary-low; + background: rgba(var(--secondary-rgb), 0.5); + border: 2px solid var(--primary-low); .d-icon { text-align: center; @@ -45,7 +45,7 @@ } input[disabled] { - background-color: $primary-low; + background-color: var(--primary-low); border-color: #ddd; } @@ -62,10 +62,10 @@ align-items: center; justify-content: center; transform: translateY(-50%); - background: $secondary; + background: var(--secondary); border-radius: 50%; font-size: 0.8em; - border: 2px solid $primary-low; + border: 2px solid var(--primary-low); } &.association, @@ -89,8 +89,8 @@ &.single { height: 28px; - background: $secondary; - border: 1px solid $primary-medium; + background: var(--secondary); + border: 1px solid var(--primary-medium); display: flex; align-items: center; justify-content: center; @@ -126,7 +126,7 @@ } .type-selector a { - color: $primary; + color: var(--primary); margin-right: 4px; display: flex; align-items: center; @@ -150,11 +150,11 @@ box-shadow: shadow("dropdown"); position: absolute; display: flex; - background: $secondary; + background: var(--secondary); z-index: 200; padding: 5px 7px; flex-direction: column; - border: 1px solid $primary-low; + border: 1px solid var(--primary-low); } .value-list .remove-value-btn { @@ -162,7 +162,7 @@ border: none; .d-icon { - color: $primary; + color: var(--primary); } } diff --git a/assets/stylesheets/wizard/custom/badges.scss b/assets/stylesheets/wizard/custom/badges.scss index ca9c0450..53a869c0 100644 --- a/assets/stylesheets/wizard/custom/badges.scss +++ b/assets/stylesheets/wizard/custom/badges.scss @@ -31,7 +31,7 @@ overflow: hidden; text-overflow: ellipsis; .extra-info-wrapper & { - color: $header-primary; + color: var(--header_primary); } } @@ -108,7 +108,7 @@ text-overflow: ellipsis; .extra-info-wrapper & { - color: $header-primary; + color: var(--header_primary); } } diff --git a/assets/stylesheets/wizard/custom/composer.scss b/assets/stylesheets/wizard/custom/composer.scss index 4f5ff442..24cad2a4 100644 --- a/assets/stylesheets/wizard/custom/composer.scss +++ b/assets/stylesheets/wizard/custom/composer.scss @@ -199,11 +199,11 @@ left: 0; width: 100%; height: 100%; - background-color: rgba($primary, 0.8); + background-color: rgba(var(--primary-rgb), 0.8); } .wizard-composer-hyperlink-contents { - background-color: $secondary; + background-color: var(--secondary); padding: 20px; h3 { @@ -227,7 +227,7 @@ bottom: 1px; right: 1px; padding: 10px; - background-color: $secondary; + background-color: var(--secondary); } .bottom-bar { @@ -239,31 +239,43 @@ // Markdown table styles for wizard composer preview -.cooked table, -.d-editor-preview table { - border-collapse: collapse; +.cooked, +.d-editor-preview { + a.mention { + display: inline-block; // https://bugzilla.mozilla.org/show_bug.cgi?id=1656119 + font-weight: bold; + font-size: 0.93em; + color: var(--primary-high-or-secondary-low); + padding: 0 4px 1px; + background: var(--primary-low); + border-radius: 8px; + } - tr { - border-bottom: 1px solid var(--primary-low); - &.highlighted { - animation: background-fade-highlight 2.5s ease-out; + table { + border-collapse: collapse; + + tr { + border-bottom: 1px solid var(--primary-low); + &.highlighted { + animation: background-fade-highlight 2.5s ease-out; + } } - } - thead { - th { - text-align: left; - padding: 0.5em; - font-weight: bold; - color: var(--primary); + thead { + th { + text-align: left; + padding: 0.5em; + font-weight: bold; + color: var(--primary); + } } - } - tbody { - border-top: 3px solid var(--primary-low); - } + tbody { + border-top: 3px solid var(--primary-low); + } - td { - padding: 3px 3px 3px 0.5em; + td { + padding: 3px 3px 3px 0.5em; + } } } diff --git a/assets/stylesheets/wizard/custom/field.scss b/assets/stylesheets/wizard/custom/field.scss index e5a7f5e2..f2bb3aa0 100644 --- a/assets/stylesheets/wizard/custom/field.scss +++ b/assets/stylesheets/wizard/custom/field.scss @@ -162,4 +162,15 @@ .text-field input { margin-bottom: 0; } + + .text-field, + .textarea-field, + .composer-field { + input[type="text"], + textarea { + &:focus::placeholder { + color: transparent; + } + } + } } diff --git a/assets/stylesheets/wizard/wizard_custom.scss b/assets/stylesheets/wizard/wizard_custom.scss index aef346fc..6290ce6e 100644 --- a/assets/stylesheets/wizard/wizard_custom.scss +++ b/assets/stylesheets/wizard/wizard_custom.scss @@ -2,6 +2,7 @@ @import "common/foundation/variables"; @import "common/base/code_highlighting"; @import "common/base/modal"; +@import "common/base/onebox"; @import "common/components/buttons"; @import "common/d-editor"; @import "desktop/modal"; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0c364853..ef826cab 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -75,7 +75,7 @@ en: edit: "You're editing an action" documentation: "Check out the action documentation" custom_fields: - create: "Create, edit or destroy a custom field record" + create: "View, create, edit and destroy custom fields" saved: "Saved custom field" error: "Failed to save: {{messages}}" documentation: Check out the custom field documentation @@ -173,7 +173,9 @@ en: max_length_placeholder: "Maximum length in characters" char_counter: "Character Counter" char_counter_placeholder: "Display Character Counter" + field_placeholder: "Field Placeholder" file_types: "File Types" + preview_template: "Preview Template" limit: "Limit" property: "Property" prefill: "Prefill" @@ -200,6 +202,7 @@ en: text: "Text" textarea: Textarea composer: Composer + composer_preview: Composer Preview text_only: Text Only number: Number checkbox: Checkbox @@ -322,6 +325,9 @@ en: custom_field: nav_label: "Custom Fields" add: "Add" + external: + label: "from another plugin" + title: "This custom field has been added by another plugin. You can use it in your wizards but you can't edit the field here." name: label: "Name" select: "underscored_name" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4bda825a..7e507450 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1,8 +1,8 @@ en: admin: wizard: - submissions: - no_user: "deleted (id: %{id})" + submission: + no_user: "deleted (user_id: %{user_id})" wizard: custom_title: "Wizard" diff --git a/controllers/custom_wizard/admin/admin.rb b/controllers/custom_wizard/admin/admin.rb index 8d5e3cad..c99954d6 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/controllers/custom_wizard/admin/admin.rb @@ -14,7 +14,7 @@ class CustomWizard::AdminController < ::Admin::AdminController end def custom_field_list - serialize_data(CustomWizard::CustomField.list, CustomWizard::CustomFieldSerializer) + serialize_data(CustomWizard::CustomField.full_list, CustomWizard::CustomFieldSerializer) end def render_error(message) diff --git a/controllers/custom_wizard/admin/submissions.rb b/controllers/custom_wizard/admin/submissions.rb index 5467587e..4cb2a0e4 100644 --- a/controllers/custom_wizard/admin/submissions.rb +++ b/controllers/custom_wizard/admin/submissions.rb @@ -13,34 +13,20 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController def show render_json_dump( wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), - submissions: build_submissions.as_json + submissions: ActiveModel::ArraySerializer.new(ordered_submissions, each_serializer: CustomWizard::SubmissionSerializer) ) end def download - send_data build_submissions.to_json, + send_data ordered_submissions.to_json, filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json", content_type: "application/json", disposition: "attachment" end - private + protected - def build_submissions - PluginStoreRow.where(plugin_name: "#{@wizard.id}_submissions") - .order('id DESC') - .map do |row| - value = ::JSON.parse(row.value) - - if user = User.find_by(id: row.key) - username = user.username - else - username = I18n.t('admin.wizard.submissions.no_user', id: row.key) - end - - value.map do |v| - { username: username }.merge!(v.except("redirect_to")) - end - end.flatten + def ordered_submissions + CustomWizard::Submission.list(@wizard, order_by: 'id') end end diff --git a/controllers/custom_wizard/steps.rb b/controllers/custom_wizard/steps.rb index 277b94b2..66ec2da9 100644 --- a/controllers/custom_wizard/steps.rb +++ b/controllers/custom_wizard/steps.rb @@ -8,7 +8,7 @@ class CustomWizard::StepsController < ::ApplicationController update[:fields] = {} if params[:fields] - field_ids = @step_template['fields'].map { |f| f['id'] } + field_ids = @builder.wizard.field_ids params[:fields].each do |k, v| update[:fields][k] = v if field_ids.include? k end @@ -23,7 +23,7 @@ class CustomWizard::StepsController < ::ApplicationController if updater.success? wizard_id = update_params[:wizard_id] builder = CustomWizard::Builder.new(wizard_id, current_user) - @wizard = builder.build + @wizard = builder.build(force: true) current_step = @wizard.find_step(update[:step_id]) current_submission = @wizard.current_submission @@ -36,15 +36,19 @@ class CustomWizard::StepsController < ::ApplicationController if current_step.final? builder.template.actions.each do |action_template| if action_template['run_after'] === 'wizard_completion' - CustomWizard::Action.new( + action_result = CustomWizard::Action.new( action: action_template, wizard: @wizard, - data: current_submission + submission: current_submission ).perform + + if action_result.success? + current_submission = action_result.submission + end end end - @wizard.save_submission(current_submission) + current_submission.save if redirect = get_redirect updater.result[:redirect_on_complete] = redirect @@ -54,6 +58,8 @@ class CustomWizard::StepsController < ::ApplicationController result[:final] = true else + current_submission.save + result[:final] = false result[:next_step_id] = current_step.next.id end @@ -101,9 +107,9 @@ class CustomWizard::StepsController < ::ApplicationController def get_redirect return @result[:redirect_on_next] if @result[:redirect_on_next].present? - current_submission = @wizard.current_submission - return nil unless current_submission.present? + submission = @wizard.current_submission + return nil unless submission.present? ## route_to set by actions, redirect_on_complete set by actions, redirect_to set at wizard entry - current_submission[:route_to] || current_submission[:redirect_on_complete] || current_submission[:redirect_to] + submission.route_to || submission.redirect_on_complete || submission.redirect_to end end diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index 37728ecb..e0cf669d 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -6,7 +6,7 @@ class CustomWizard::WizardController < ::ApplicationController before_action :ensure_plugin_enabled helper_method :wizard_page_title - helper_method :wizard_theme_ids + helper_method :wizard_theme_id helper_method :wizard_theme_lookup helper_method :wizard_theme_translations_lookup @@ -20,16 +20,16 @@ class CustomWizard::WizardController < ::ApplicationController wizard ? (wizard.name || wizard.id) : I18n.t('wizard.custom_title') end - def wizard_theme_ids - wizard ? [wizard.theme_id] : nil + def wizard_theme_id + wizard ? wizard.theme_id : nil end def wizard_theme_lookup(name) - Theme.lookup_field(wizard_theme_ids, mobile_view? ? :mobile : :desktop, name) + Theme.lookup_field(wizard_theme_id, mobile_view? ? :mobile : :desktop, name) end def wizard_theme_translations_lookup - Theme.lookup_field(wizard_theme_ids, :translations, I18n.locale) + Theme.lookup_field(wizard_theme_id, :translations, I18n.locale) end def index @@ -61,10 +61,11 @@ class CustomWizard::WizardController < ::ApplicationController result = success_json user = current_user - if user + if user && wizard.can_access? submission = wizard.current_submission - if submission && submission['redirect_to'] - result.merge!(redirect_to: submission['redirect_to']) + + if submission.present? && submission.redirect_to + result.merge!(redirect_to: submission.redirect_to) end wizard.final_cleanup! diff --git a/coverage/.last_run.json b/coverage/.last_run.json index 3e7f27f6..2d4d0378 100644 --- a/coverage/.last_run.json +++ b/coverage/.last_run.json @@ -1,5 +1,5 @@ { "result": { - "line": 90.52 + "line": 91.83 } } diff --git a/extensions/custom_field/extension.rb b/extensions/custom_field/extension.rb new file mode 100644 index 00000000..876f56d4 --- /dev/null +++ b/extensions/custom_field/extension.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module CustomWizardCustomFieldExtension + def custom_field_types + @custom_field_types + end +end diff --git a/extensions/extra_locales_controller.rb b/extensions/extra_locales_controller.rb index e7c5a02e..6242f7ca 100644 --- a/extensions/extra_locales_controller.rb +++ b/extensions/extra_locales_controller.rb @@ -4,7 +4,8 @@ module ExtraLocalesControllerCustomWizard super || begin return false unless bundle =~ /wizard/ && request.referer =~ /\/w\// path = URI(request.referer).path - wizard_id = path.split('/w/').last + wizard_path = path.split('/w/').last + wizard_id = wizard_path.split('/').first CustomWizard::Template.exists?(wizard_id.underscore) end end diff --git a/extensions/invites_controller.rb b/extensions/invites_controller.rb index cafb15bd..5e0094da 100644 --- a/extensions/invites_controller.rb +++ b/extensions/invites_controller.rb @@ -5,7 +5,7 @@ module InvitesControllerCustomWizard wizard_id = @user.custom_fields['redirect_to_wizard'] if wizard_id && url != '/' - CustomWizard::Wizard.set_submission_redirect(@user, wizard_id, url) + CustomWizard::Wizard.set_wizard_redirect(@user, wizard_id, url) url = "/w/#{wizard_id.dasherize}" end end diff --git a/jobs/clear_after_time_wizard.rb b/jobs/clear_after_time_wizard.rb deleted file mode 100644 index 37d997db..00000000 --- a/jobs/clear_after_time_wizard.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true -module Jobs - class ClearAfterTimeWizard < ::Jobs::Base - sidekiq_options queue: 'critical' - - def execute(args) - User.human_users.each do |u| - if u.custom_fields['redirect_to_wizard'] == args[:wizard_id] - u.custom_fields.delete('redirect_to_wizard') - u.save_custom_fields(true) - end - end - end - end -end diff --git a/jobs/set_after_time_wizard.rb b/jobs/set_after_time_wizard.rb index 3b2e9e11..7a5b86c6 100644 --- a/jobs/set_after_time_wizard.rb +++ b/jobs/set_after_time_wizard.rb @@ -9,7 +9,7 @@ module Jobs user_ids = [] User.human_users.each do |user| - if CustomWizard::Wizard.set_wizard_redirect(wizard.id, user) + if CustomWizard::Wizard.set_user_redirect(wizard.id, user) user_ids.push(user.id) end end diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index d68e978b..1b5770d7 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class CustomWizard::Action - attr_accessor :data, + attr_accessor :submission, :action, :user, :guardian, @@ -11,7 +11,7 @@ class CustomWizard::Action @action = opts[:action] @user = @wizard.user @guardian = Guardian.new(@user) - @data = opts[:data] + @submission = opts[:submission] @log = [] @result = CustomWizard::ActionResult.new end @@ -26,14 +26,21 @@ class CustomWizard::Action end if @result.success? && @result.output.present? - data[action['id']] = @result.output + @submission.fields[action['id']] = @result.output end save_log + + @result.submission = @submission + @result + end + + def mapper_data + @mapper_data ||= @submission&.fields_and_meta || {} end def mapper - @mapper ||= CustomWizard::Mapper.new(user: user, data: data) + @mapper ||= CustomWizard::Mapper.new(user: user, data: mapper_data) end def create_topic @@ -47,7 +54,7 @@ class CustomWizard::Action messages = creator.errors.full_messages.join(" ") log_error("failed to create", messages) elsif action['skip_redirect'].blank? - data['redirect_on_complete'] = post.topic.url + @submission.redirect_on_complete = post.topic.url end if creator.errors.blank? @@ -65,7 +72,7 @@ class CustomWizard::Action if action['required'].present? required = CustomWizard::Mapper.new( inputs: action['required'], - data: data, + data: mapper_data, user: user ).perform @@ -79,7 +86,7 @@ class CustomWizard::Action targets = CustomWizard::Mapper.new( inputs: action['recipient'], - data: data, + data: mapper_data, user: user, multiple: true ).perform @@ -115,7 +122,7 @@ class CustomWizard::Action messages = creator.errors.full_messages.join(" ") log_error("failed to create message", messages) elsif action['skip_redirect'].blank? - data['redirect_on_complete'] = post.topic.url + @submission.redirect_on_complete = post.topic.url end if creator.errors.blank? @@ -178,7 +185,7 @@ class CustomWizard::Action def watch_categories watched_categories = CustomWizard::Mapper.new( inputs: action['categories'], - data: data, + data: mapper_data, user: user ).perform @@ -193,7 +200,7 @@ class CustomWizard::Action mute_remainder = CustomWizard::Mapper.new( inputs: action['mute_remainder'], - data: data, + data: mapper_data, user: user ).perform @@ -202,7 +209,7 @@ class CustomWizard::Action if action['usernames'] mapped_users = CustomWizard::Mapper.new( inputs: action['usernames'], - data: data, + data: mapper_data, user: user ).perform @@ -284,7 +291,7 @@ class CustomWizard::Action end route_to = Discourse.base_uri + url - @result.output = data['route_to'] = route_to + @result.output = @submission.route_to = route_to log_success("route: #{route_to}") else @@ -295,7 +302,7 @@ class CustomWizard::Action def add_to_group group_map = CustomWizard::Mapper.new( inputs: action['group'], - data: data, + data: mapper_data, user: user, opts: { multiple: true @@ -345,18 +352,18 @@ class CustomWizard::Action else url = CustomWizard::Mapper.new( inputs: url_input, - data: data, + data: mapper_data, user: user ).perform end if action['code'] - data[action['code']] = SecureRandom.hex(8) - url += "&#{action['code']}=#{data[action['code']]}" + @submission.fields[action['code']] = SecureRandom.hex(8) + url += "&#{action['code']}=#{@submission.fields[action['code']]}" end route_to = UrlHelper.encode(url) - data['route_to'] = route_to + @submission.route_to = route_to log_info("route: #{route_to}") end @@ -416,7 +423,7 @@ class CustomWizard::Action def action_category output = CustomWizard::Mapper.new( inputs: action['category'], - data: data, + data: mapper_data, user: user ).perform @@ -434,7 +441,7 @@ class CustomWizard::Action def action_tags output = CustomWizard::Mapper.new( inputs: action['tags'], - data: data, + data: mapper_data, user: user, ).perform @@ -451,35 +458,54 @@ class CustomWizard::Action if (custom_fields = action['custom_fields']).present? field_map = CustomWizard::Mapper.new( inputs: custom_fields, - data: data, + data: mapper_data, user: user ).perform - - registered_fields = CustomWizard::CustomField.cached_list + registered_fields = CustomWizard::CustomField.full_list field_map.each do |field| keyArr = field[:key].split('.') value = field[:value] if keyArr.length > 1 - klass = keyArr.first - name = keyArr.last + klass = keyArr.first.to_sym + name = keyArr.second + + if keyArr.length === 3 && name.include?("{}") + name = name.gsub("{}", "") + json_attr = keyArr.last + type = :json + end else name = keyArr.first end - registered = registered_fields.select { |f| f[:name] == name } - if registered.first.present? - klass = registered.first[:klass] + registered = registered_fields.select { |f| f.name == name }.first + if registered.present? + klass = registered.klass + type = registered.type end - if klass === 'topic' + next if type === :json && json_attr.blank? + + if klass === :topic params[:topic_opts] ||= {} params[:topic_opts][:custom_fields] ||= {} - params[:topic_opts][:custom_fields][name] = value + + if type === :json + params[:topic_opts][:custom_fields][name] ||= {} + params[:topic_opts][:custom_fields][name][json_attr] = value + else + params[:topic_opts][:custom_fields][name] = value + end else - params[:custom_fields] ||= {} - params[:custom_fields][name] = value + if type === :json + params[:custom_fields][name] ||= {} + params[:custom_fields][name][json_attr] = value + else + params[:custom_fields] ||= {} + params[:custom_fields][name] = value + end end end end @@ -494,7 +520,7 @@ class CustomWizard::Action params[:title] = CustomWizard::Mapper.new( inputs: action['title'], - data: data, + data: mapper_data, user: user ).perform @@ -506,7 +532,7 @@ class CustomWizard::Action wizard: true, template: true ) : - data[action['post']] + @submission.fields[action['post']] params[:import_mode] = ActiveRecord::Type::Boolean.new.cast(action['suppress_notifications']) @@ -529,7 +555,7 @@ class CustomWizard::Action unless action[field].nil? || action[field] == "" params[field.to_sym] = CustomWizard::Mapper.new( inputs: action[field], - data: data, + data: mapper_data, user: user ).perform end @@ -568,7 +594,7 @@ class CustomWizard::Action if input.present? value = CustomWizard::Mapper.new( inputs: input, - data: data, + data: mapper_data, user: user ).perform @@ -598,7 +624,7 @@ class CustomWizard::Action if action[attr].present? value = CustomWizard::Mapper.new( inputs: action[attr], - data: data, + data: mapper_data, user: user ).perform diff --git a/lib/custom_wizard/action_result.rb b/lib/custom_wizard/action_result.rb index 07c81284..53484ceb 100644 --- a/lib/custom_wizard/action_result.rb +++ b/lib/custom_wizard/action_result.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class CustomWizard::ActionResult - attr_accessor :success, :handler, :output + attr_accessor :success, :handler, :output, :submission def initialize @success = false diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 6f0cb960..af70a758 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -24,13 +24,13 @@ class CustomWizard::Builder def mapper CustomWizard::Mapper.new( user: @wizard.user, - data: @wizard.current_submission + data: @wizard.current_submission&.fields_and_meta ) end def build(build_opts = {}, params = {}) return nil if !SiteSetting.custom_wizard_enabled || !@wizard - return @wizard if !@wizard.can_access? + return @wizard if !@wizard.can_access? && !build_opts[:force] build_opts[:reset] = build_opts[:reset] || @wizard.restart_on_revisit @@ -47,9 +47,8 @@ class CustomWizard::Builder step.on_update do |updater| @updater = updater - @submission = (@wizard.current_submission || {}) - .merge(@updater.submission) - .with_indifferent_access + @submission = @wizard.current_submission + @submission.fields.merge!(@updater.submission) @updater.validate next if @updater.errors.any? @@ -60,11 +59,11 @@ class CustomWizard::Builder run_step_actions if @updater.errors.empty? - if route_to = @submission['route_to'] - @submission.delete('route_to') - end + route_to = @submission.route_to + @submission.route_to = nil + @submission.save - @wizard.save_submission(@submission) + @wizard.update! @updater.result[:redirect_on_next] = route_to if route_to true @@ -75,7 +74,7 @@ class CustomWizard::Builder end end - @wizard.update_step_order! + @wizard.update! @wizard end @@ -100,8 +99,8 @@ class CustomWizard::Builder params[:value] = prefill_field(field_template, step_template) - if !build_opts[:reset] && (submission = @wizard.current_submission) - params[:value] = submission[field_template['id']] if submission[field_template['id']] + if !build_opts[:reset] && (submission = @wizard.current_submission).present? + params[:value] = submission.fields[field_template['id']] if submission.fields[field_template['id']] end if field_template['type'] === 'group' && params[:value].present? @@ -144,7 +143,7 @@ class CustomWizard::Builder content = CustomWizard::Mapper.new( inputs: content_inputs, user: @wizard.user, - data: @wizard.current_submission, + data: @wizard.current_submission&.fields_and_meta, opts: { with_type: true } @@ -179,7 +178,7 @@ class CustomWizard::Builder index = CustomWizard::Mapper.new( inputs: field_template['index'], user: @wizard.user, - data: @wizard.current_submission + data: @wizard.current_submission&.fields_and_meta ).perform params[:index] = index.to_i unless index.nil? @@ -195,6 +194,28 @@ class CustomWizard::Builder ) end + if field_template['preview_template'].present? + preview_template = mapper.interpolate( + field_template['preview_template'], + user: true, + value: true, + wizard: true, + template: true + ) + + params[:preview_template] = PrettyText.cook(preview_template) + end + + if field_template['placeholder'].present? + params[:placeholder] = mapper.interpolate( + field_template['placeholder'], + user: true, + value: true, + wizard: true, + template: true + ) + end + field = step.add_field(params) end @@ -203,7 +224,7 @@ class CustomWizard::Builder CustomWizard::Mapper.new( inputs: prefill, user: @wizard.user, - data: @wizard.current_submission + data: @wizard.current_submission&.fields_and_meta ).perform end end @@ -213,7 +234,7 @@ class CustomWizard::Builder result = CustomWizard::Mapper.new( inputs: template['condition'], user: @wizard.user, - data: @wizard.current_submission, + data: @wizard.current_submission&.fields_and_meta, opts: { multiple: true } @@ -283,19 +304,20 @@ class CustomWizard::Builder permitted_data = {} submission_key = nil params_key = nil - submission = @wizard.current_submission || {} + submission = @wizard.current_submission permitted_params.each do |pp| pair = pp['pairs'].first params_key = pair['key'].to_sym submission_key = pair['value'].to_sym - if submission_key && params_key - submission[submission_key] = params[params_key] + if submission_key && params_key && params[params_key].present? + submission.permitted_param_keys << submission_key.to_s + submission.fields[submission_key] = params[params_key] end end - @wizard.save_submission(submission) + submission.save end def ensure_required_data(step, step_template) @@ -304,13 +326,13 @@ class CustomWizard::Builder pair['key'].present? && pair['value'].present? end - if pairs.any? && !@wizard.current_submission + if pairs.any? && !@wizard.current_submission.present? step.permitted = false break end pairs.each do |pair| - pair['key'] = @wizard.current_submission[pair['key']] + pair['key'] = @wizard.current_submission.fields[pair['key']] end if !mapper.validate_pairs(pairs) @@ -334,11 +356,15 @@ class CustomWizard::Builder if @template.actions.present? @template.actions.each do |action_template| if action_template['run_after'] === updater.step.id - CustomWizard::Action.new( + result = CustomWizard::Action.new( action: action_template, wizard: @wizard, - data: @submission + submission: @submission ).perform + + if result.success? + @submission = result.submission + end end end end diff --git a/lib/custom_wizard/custom_field.rb b/lib/custom_wizard/custom_field.rb index e3f01a1a..9cc185ba 100644 --- a/lib/custom_wizard/custom_field.rb +++ b/lib/custom_wizard/custom_field.rb @@ -66,10 +66,12 @@ class ::CustomWizard::CustomField value = send(attr) i18n_key = "wizard.custom_field.error" - if value.blank? - if REQUIRED.include?(attr) - add_error(I18n.t("#{i18n_key}.required_attribute", attr: attr)) - end + if value.blank? && REQUIRED.include?(attr) + add_error(I18n.t("#{i18n_key}.required_attribute", attr: attr)) + break + end + + if attr == 'serializers' && !value.is_a?(Array) next end @@ -140,7 +142,7 @@ class ::CustomWizard::CustomField fields.select do |cf| if attr == :serializers - cf[attr].include?(value) + cf[attr] && cf[attr].include?(value) else cf[attr] == value end @@ -215,4 +217,32 @@ class ::CustomWizard::CustomField def self.enabled? any? end + + def self.external_list + external = [] + + CLASSES.keys.each do |klass| + field_types = klass.to_s.classify.constantize.custom_field_types + + if field_types.present? + field_types.each do |name, type| + unless list.any? { |field| field.name === name } + field = new( + 'external', + name: name, + klass: klass, + type: type + ) + external.push(field) + end + end + end + end + + external + end + + def self.full_list + (list + external_list).uniq + end end diff --git a/lib/custom_wizard/exceptions/exceptions.rb b/lib/custom_wizard/exceptions/exceptions.rb new file mode 100644 index 00000000..b5014d27 --- /dev/null +++ b/lib/custom_wizard/exceptions/exceptions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +module CustomWizard + class SprocketsFileNotFound < StandardError; end + class SprocketsEmptyPath < StandardError; end +end diff --git a/lib/custom_wizard/field.rb b/lib/custom_wizard/field.rb index aacb163b..a7b8ef72 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -45,6 +45,8 @@ class CustomWizard::Field content: [:serializable, :permitted, :mapped], prefill: [:permitted, :mapped], condition: [:permitted, :mapped], + preview_template: [:serializable, :permitted, :mapped], + placeholder: [:serializable, :permitted, :mapped], } end @@ -108,20 +110,26 @@ class CustomWizard::Field max_length: nil, prefill: nil, char_counter: nil, - validations: nil + validations: nil, + placeholder: nil }, textarea: { min_length: nil, max_length: nil, prefill: nil, - char_counter: nil + char_counter: nil, + placeholder: nil }, composer: { min_length: nil, max_length: nil, - char_counter: nil + char_counter: nil, + placeholder: nil }, text_only: {}, + composer_preview: { + preview_template: nil, + }, date: { format: "YYYY-MM-DD" }, diff --git a/lib/custom_wizard/mapper.rb b/lib/custom_wizard/mapper.rb index c1187b0f..0c3543cf 100644 --- a/lib/custom_wizard/mapper.rb +++ b/lib/custom_wizard/mapper.rb @@ -5,20 +5,27 @@ class CustomWizard::Mapper USER_FIELDS = [ 'name', 'username', - 'email', 'date_of_birth', 'title', 'locale', 'trust_level', + 'email' + ] + + USER_OPTION_FIELDS = [ 'email_level', 'email_messages_level', 'email_digests' ] - PROFILE_FIELDS = ['location', 'website', 'bio_raw'] + PROFILE_FIELDS = [ + 'location', + 'website', + 'bio_raw' + ] def self.user_fields - USER_FIELDS + PROFILE_FIELDS + USER_FIELDS + USER_OPTION_FIELDS + PROFILE_FIELDS end OPERATORS = { @@ -197,11 +204,15 @@ class CustomWizard::Mapper def map_user_field(value) if value.include?(User::USER_FIELD_PREFIX) - UserCustomField.where(user_id: user.id, name: value).pluck(:value).first + user.custom_fields[value] elsif PROFILE_FIELDS.include?(value) - UserProfile.find_by(user_id: user.id).send(value) + user.user_profile.send(value) elsif USER_FIELDS.include?(value) - User.find(user.id).send(value) + user.send(value) + elsif USER_OPTION_FIELDS.include?(value) + user.user_option.send(value) + else + nil end end @@ -217,19 +228,11 @@ class CustomWizard::Mapper return string if string.blank? if opts[:user] - string.gsub!(/u\{(.*?)\}/) do |match| - result = '' - result = user.send($1) if USER_FIELDS.include?($1) - result = user.user_profile.send($1) if PROFILE_FIELDS.include?($1) - result - end + string.gsub!(/u\{(.*?)\}/) { |match| map_user_field($1) || '' } end if opts[:wizard] - string.gsub!(/w\{(.*?)\}/) do |match| - value = recurse(data, [*$1.split('.')]) - value.present? ? value : '' - end + string.gsub!(/w\{(.*?)\}/) { |match| recurse(data, [*$1.split('.')]) || '' } end if opts[:value] diff --git a/lib/custom_wizard/submission.rb b/lib/custom_wizard/submission.rb new file mode 100644 index 00000000..e50cb259 --- /dev/null +++ b/lib/custom_wizard/submission.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true +class CustomWizard::Submission + include ActiveModel::SerializerSupport + + KEY ||= "submissions" + META ||= %w(submitted_at route_to redirect_on_complete redirect_to) + + attr_reader :id, + :user, + :user_id, + :wizard + + attr_accessor :fields, + :permitted_param_keys + + META.each do |attr| + class_eval { attr_accessor attr } + end + + def initialize(wizard, data = {}, user_id = nil) + @wizard = wizard + @user_id = user_id + + if user_id + @user = User.find_by(id: user_id) + else + @user = wizard.user + end + + data = (data || {}).with_indifferent_access + @id = data['id'] || SecureRandom.hex(12) + non_field_keys = META + ['id'] + @fields = data.except(*non_field_keys) || {} + + META.each do |attr| + send("#{attr}=", data[attr]) if data[attr] + end + + @permitted_param_keys = data['permitted_param_keys'] || [] + end + + def save + return nil unless wizard.save_submissions + validate + + submission_list = self.class.list(wizard, user_id: user.id) + submissions = submission_list.select { |submission| submission.id != self.id } + submissions.push(self) + + submission_data = submissions.map { |submission| data_to_save(submission) } + PluginStore.set("#{wizard.id}_#{KEY}", user.id, submission_data) + end + + def validate + self.fields = fields.select { |key, value| validate_field_key(key) } + end + + def validate_field_key(key) + wizard.field_ids.include?(key) || + wizard.action_ids.include?(key) || + permitted_param_keys.include?(key) + end + + def fields_and_meta + result = fields + + META.each do |attr| + if value = self.send(attr) + result[attr] = value + end + end + + result + end + + def present? + fields_and_meta.present? + end + + def data_to_save(submission) + data = { + id: submission.id + } + + data.merge!(submission.fields_and_meta) + + if submission.permitted_param_keys.present? + data[:permitted_param_keys] = submission.permitted_param_keys + end + + data + end + + def self.get(wizard, user_id) + data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).first + new(wizard, data, user_id) + end + + def self.list(wizard, user_id: nil, order_by: nil) + params = { plugin_name: "#{wizard.id}_#{KEY}" } + params[:key] = user_id if user_id.present? + + query = PluginStoreRow.where(params) + query = query.order("#{order_by} DESC") if order_by.present? + + result = [] + + query.each do |record| + if (submission_data = ::JSON.parse(record.value)).any? + submission_data.each do |data| + result.push(new(wizard, data, record.key)) + end + end + end + + result + end +end diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index a1c0aad0..8e944dca 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -49,18 +49,15 @@ class CustomWizard::Template def self.remove(wizard_id) wizard = CustomWizard::Wizard.create(wizard_id) - return false if !wizard ActiveRecord::Base.transaction do PluginStore.remove(CustomWizard::PLUGIN_NAME, wizard.id) - - if wizard.after_time - Jobs.cancel_scheduled_job(:set_after_time_wizard) - Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard_id) - end + clear_user_wizard_redirect(wizard_id) end + Jobs.cancel_scheduled_job(:set_after_time_wizard) if wizard.after_time + true end @@ -88,6 +85,10 @@ class CustomWizard::Template end end + def self.clear_user_wizard_redirect(wizard_id) + UserCustomField.where(name: 'redirect_to_wizard', value: wizard_id).destroy_all + end + private def normalize_data @@ -132,7 +133,7 @@ class CustomWizard::Template Jobs.enqueue_at(enqueue_wizard_at, :set_after_time_wizard, wizard_id: wizard_id) elsif old_data && old_data[:after_time] Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id) - Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard_id) + self.class.clear_user_wizard_redirect(wizard_id) end end end diff --git a/lib/custom_wizard/validators/update.rb b/lib/custom_wizard/validators/update.rb index d84b448a..c722a763 100644 --- a/lib/custom_wizard/validators/update.rb +++ b/lib/custom_wizard/validators/update.rb @@ -32,11 +32,11 @@ class ::CustomWizard::UpdateValidator @updater.errors.add(field_id, I18n.t('wizard.field.required', label: label)) end - if min_length && value.is_a?(String) && value.strip.length < min_length.to_i + if min_length.present? && value.is_a?(String) && value.strip.length < min_length.to_i @updater.errors.add(field_id, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i)) end - if max_length && value.is_a?(String) && value.strip.length > max_length.to_i + if max_length.present? && value.is_a?(String) && value.strip.length > max_length.to_i @updater.errors.add(field_id, I18n.t('wizard.field.too_long', label: label, max: max_length.to_i)) end @@ -52,7 +52,7 @@ class ::CustomWizard::UpdateValidator @updater.errors.add(field_id, I18n.t('wizard.field.invalid_file', label: label, types: file_types)) end - if ['date', 'date_time'].include?(type) && value.present? && !validate_date(value) + if ['date', 'date_time'].include?(type) && value.present? && !validate_date(value, format) @updater.errors.add(field_id, I18n.t('wizard.field.invalid_date')) end @@ -88,13 +88,8 @@ class ::CustomWizard::UpdateValidator .include?(File.extname(value['original_filename'])[1..-1]) end - def validate_date(value) - begin - Date.parse(value) - true - rescue ArgumentError - false - end + def validate_date(value, format) + v8.eval("moment('#{value}', '#{format}', true).isValid()") end def validate_time(value) @@ -126,4 +121,12 @@ class ::CustomWizard::UpdateValidator def standardise_boolean(value) ActiveRecord::Type::Boolean.new.cast(value) end + + def v8 + return @ctx if @ctx + + @ctx = PrettyText.v8 + PrettyText.ctx_load(@ctx, "#{Rails.root}/vendor/assets/javascripts/moment.js") + @ctx + end end diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 5967e013..d5ba940e 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -34,10 +34,15 @@ class CustomWizard::Wizard :needs_groups, :steps, :step_ids, + :field_ids, :first_step, :start, :actions, - :user + :action_ids, + :user, + :submissions + + attr_reader :all_step_ids def initialize(attrs = {}, user = nil) @user = user @@ -66,11 +71,22 @@ class CustomWizard::Wizard @first_step = nil @steps = [] + if attrs['steps'].present? - @step_ids = attrs['steps'].map { |s| s['id'] } + @step_ids = @all_step_ids = attrs['steps'].map { |s| s['id'] } + + @field_ids = [] + attrs['steps'].each do |step| + if step['fields'].present? + step['fields'].each do |field| + @field_ids << field['id'] + end + end + end end - @actions = [] + @actions = attrs['actions'] || [] + @action_ids = @actions.map { |a| a['id'] } end def cast_bool(val) @@ -91,7 +107,19 @@ class CustomWizard::Wizard step.index = (steps.size == 1 ? 0 : steps.size) if step.index.nil? end - def update_step_order! + def update! + update_step_order + update_step_ids + update_field_ids + update_action_ids + + @submissions = nil + @current_submission = nil + + true + end + + def update_step_order steps.sort_by!(&:index) steps.each_with_index do |step, index| @@ -110,7 +138,7 @@ class CustomWizard::Wizard step.conditional_final_step = true end - if index === (step_ids.length - 1) + if index === (all_step_ids.length - 1) step.last_step = true end @@ -125,7 +153,7 @@ class CustomWizard::Wizard acting_user_id: user.id, action: ::UserHistory.actions[:custom_wizard_step], context: id, - subject: step_ids + subject: all_step_ids ).order("created_at").last last_completed_step.subject @@ -229,30 +257,41 @@ class CustomWizard::Wizard @groups ||= ::Site.new(Guardian.new(user)).groups end - def submissions - return nil unless user.present? - @submissions ||= Array.wrap(PluginStore.get("#{id}_submissions", user.id)) + def update_step_ids + @step_ids = steps.map(&:id) end - def current_submission - if submissions.present? && submissions.last.present? && !submissions.last.key?("submitted_at") - submissions.last.with_indifferent_access - else - nil + def update_field_ids + @field_ids = steps.map { |step| step.fields.map { |field| field.id } }.flatten + end + + def update_action_ids + @action_ids = [] + + @actions.each do |action| + if action['run_after'].blank? || + action['run_after'] === 'wizard_completion' || + step_ids.include?(action['run_after']) + + @action_ids << action['id'] + end end end - def set_submissions(submissions) - PluginStore.set("#{id}_submissions", user.id, Array.wrap(submissions)) - @submissions = nil + def submissions + return nil unless user.present? + @submissions ||= CustomWizard::Submission.list(self, user_id: user.id) end - def save_submission(submission) - return nil unless save_submissions - - submissions.pop(1) if unfinished? - submissions.push(submission) - set_submissions(submissions) + def current_submission + @current_submission ||= begin + if submissions.present? + unsubmitted = submissions.select { |submission| !submission.submitted_at } + unsubmitted.present? ? unsubmitted.first : CustomWizard::Submission.new(self) + else + CustomWizard::Submission.new(self) + end + end end def final_cleanup! @@ -261,18 +300,12 @@ class CustomWizard::Wizard user.save_custom_fields(true) end - if submission = current_submission - submission['submitted_at'] = Time.now.iso8601 - save_submission(submission) + if current_submission.present? + current_submission.submitted_at = Time.now.iso8601 + current_submission.save end - end - def self.submissions(wizard_id, user) - new({ id: wizard_id }, user).submissions - end - - def self.set_submissions(wizard_id, user, submissions) - new({ id: wizard_id }, user).set_submissions(submissions) + update! end def self.create(wizard_id, user = nil) @@ -327,11 +360,7 @@ class CustomWizard::Wizard end end - def self.set_submission_redirect(user, wizard_id, url) - set_submissions(wizard_id, user, [{ redirect_to: url }]) - end - - def self.set_wizard_redirect(wizard_id, user) + def self.set_user_redirect(wizard_id, user) wizard = self.create(wizard_id, user) if wizard.permitted? @@ -341,4 +370,16 @@ class CustomWizard::Wizard false end end + + def self.set_wizard_redirect(user, wizard_id, url) + wizard = self.create(wizard_id, user) + + if wizard.permitted? + submission = wizard.current_submission + submission.redirect_to = url + submission.save + else + false + end + end end diff --git a/package.json b/package.json index c6692218..f06823c8 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "discourse-custom-wizard", "version": "1.0.0", "repository": "git@github.com:paviliondev/discourse-custom-wizard.git", - "author": "Discourse", - "license": "MIT", + "author": "Pavilion", + "license": "GPL V2", "devDependencies": { "eslint-config-discourse": "^1.1.8" } -} +} \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index 43fe69d2..32abc584 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # name: discourse-custom-wizard # about: Create custom wizards -# version: 0.7.0 +# version: 0.8.0 # authors: Angus McLeod # url: https://github.com/paviliondev/discourse-custom-wizard # contact emails: angus@thepavilion.io @@ -42,6 +42,22 @@ if respond_to?(:register_svg_icon) register_svg_icon "save" end +class ::Sprockets::DirectiveProcessor + def process_require_tree_discourse_directive(path = ".") + raise CustomWizard::SprocketsEmptyPath, "path cannot be empty" if path == "." + + discourse_asset_path = "#{Rails.root}/app/assets/javascripts/" + path = File.expand_path(path, discourse_asset_path) + stat = @environment.stat(path) + + if stat && stat.directory? + require_paths(*@environment.stat_sorted_tree_with_dependencies(path)) + else + raise CustomWizard::SprocketsFileNotFound, "#{path} not found in discourse core" + end + end +end + after_initialize do %w[ ../lib/custom_wizard/engine.rb @@ -56,7 +72,6 @@ after_initialize do ../controllers/custom_wizard/wizard.rb ../controllers/custom_wizard/steps.rb ../controllers/custom_wizard/realtime_validations.rb - ../jobs/clear_after_time_wizard.rb ../jobs/refresh_api_access_token.rb ../jobs/set_after_time_wizard.rb ../lib/custom_wizard/validators/template.rb @@ -74,6 +89,7 @@ after_initialize do ../lib/custom_wizard/log.rb ../lib/custom_wizard/step_updater.rb ../lib/custom_wizard/step.rb + ../lib/custom_wizard/submission.rb ../lib/custom_wizard/template.rb ../lib/custom_wizard/wizard.rb ../lib/custom_wizard/api/api.rb @@ -81,6 +97,7 @@ after_initialize do ../lib/custom_wizard/api/endpoint.rb ../lib/custom_wizard/api/log_entry.rb ../lib/custom_wizard/liquid_extensions/first_non_empty.rb + ../lib/custom_wizard/exceptions/exceptions.rb ../serializers/custom_wizard/api/authorization_serializer.rb ../serializers/custom_wizard/api/basic_endpoint_serializer.rb ../serializers/custom_wizard/api/endpoint_serializer.rb @@ -93,12 +110,14 @@ after_initialize do ../serializers/custom_wizard/wizard_step_serializer.rb ../serializers/custom_wizard/wizard_serializer.rb ../serializers/custom_wizard/log_serializer.rb + ../serializers/custom_wizard/submission_serializer.rb ../serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb ../extensions/extra_locales_controller.rb ../extensions/invites_controller.rb ../extensions/users_controller.rb ../extensions/custom_field/preloader.rb ../extensions/custom_field/serializer.rb + ../extensions/custom_field/extension.rb ].each do |path| load File.expand_path(path, __FILE__) end @@ -117,7 +136,7 @@ after_initialize do if !wizard.completed? custom_redirect = true - CustomWizard::Wizard.set_wizard_redirect(wizard.id, user) + CustomWizard::Wizard.set_user_redirect(wizard.id, user) end end @@ -138,7 +157,7 @@ after_initialize do on(:user_approved) do |user| if wizard = CustomWizard::Wizard.after_signup(user) - CustomWizard::Wizard.set_wizard_redirect(wizard.id, user) + CustomWizard::Wizard.set_user_redirect(wizard.id, user) end end @@ -149,7 +168,7 @@ after_initialize do if request.format === 'text/html' && !@excluded_routes.any? { |str| /#{str}/ =~ url } && wizard_id if request.referer !~ /\/w\// && request.referer !~ /\/invites\// - CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer) + CustomWizard::Wizard.set_wizard_redirect(current_user, wizard_id, request.referer) end if CustomWizard::Template.exists?(wizard_id) redirect_to "/w/#{wizard_id.dasherize}" @@ -191,18 +210,18 @@ after_initialize do end CustomWizard::CustomField::CLASSES.keys.each do |klass| + class_constant = klass.to_s.classify.constantize + add_model_callback(klass, :after_initialize) do if CustomWizard::CustomField.enabled? CustomWizard::CustomField.list_by(:klass, klass.to_s).each do |field| - klass.to_s - .classify - .constantize - .register_custom_field_type(field[:name], field[:type].to_sym) + class_constant.register_custom_field_type(field[:name], field[:type].to_sym) end end end - klass.to_s.classify.constantize.singleton_class.prepend CustomWizardCustomFieldPreloader + class_constant.singleton_class.prepend CustomWizardCustomFieldPreloader + class_constant.singleton_class.prepend CustomWizardCustomFieldExtension end CustomWizard::CustomField.serializers.each do |serializer_klass| diff --git a/serializers/custom_wizard/submission_serializer.rb b/serializers/custom_wizard/submission_serializer.rb new file mode 100644 index 00000000..52f0cb32 --- /dev/null +++ b/serializers/custom_wizard/submission_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +class CustomWizard::SubmissionSerializer < ApplicationSerializer + attributes :id, + :username, + :fields, + :submitted_at, + :route_to, + :redirect_on_complete, + :redirect_to + + def username + object.user.present? ? + object.user.username : + I18n.t('admin.wizard.submission.no_user', user_id: object.user_id) + end +end diff --git a/serializers/custom_wizard/wizard_field_serializer.rb b/serializers/custom_wizard/wizard_field_serializer.rb index ffad36c8..f37476f0 100644 --- a/serializers/custom_wizard/wizard_field_serializer.rb +++ b/serializers/custom_wizard/wizard_field_serializer.rb @@ -55,6 +55,7 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer end def placeholder + return object.placeholder if object.placeholder.present? I18n.t("#{object.key || i18n_key}.placeholder", default: '') end @@ -100,4 +101,8 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer def char_counter object.char_counter end + + def preview_template + object.preview_template + end end diff --git a/serializers/custom_wizard/wizard_serializer.rb b/serializers/custom_wizard/wizard_serializer.rb index f858c195..7a162ba5 100644 --- a/serializers/custom_wizard/wizard_serializer.rb +++ b/serializers/custom_wizard/wizard_serializer.rb @@ -8,11 +8,11 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer :completed, :required, :permitted, - :uncategorized_category_id + :uncategorized_category_id, + :categories has_many :steps, serializer: ::CustomWizard::StepSerializer, embed: :objects has_one :user, serializer: ::BasicUserSerializer, embed: :objects - has_many :categories, serializer: ::BasicCategorySerializer, embed: :objects has_many :groups, serializer: ::BasicGroupSerializer, embed: :objects def completed @@ -56,4 +56,8 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer def include_uncategorized_category_id? object.needs_categories end + + def categories + object.categories.map { |c| c.to_h } + end end diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 28f2cab8..8b617c39 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -72,6 +72,42 @@ describe CustomWizard::Action do raw: "topic body" ).exists?).to eq(false) end + + it "adds custom fields" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + 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_custom_field = TopicCustomField.where( + name: "topic_field", + value: "Topic custom field value", + topic_id: topic.id + ) + topic_json_custom_field = TopicCustomField.where(" + name = 'topic_json_field' AND + (value::json->>'key_1') = 'Key 1 value' AND + (value::json->>'key_2') = 'Key 2 value' AND + topic_id = #{topic.id}" + ) + post_custom_field = PostCustomField.where( + name: "post_field", + value: "Post custom field value", + post_id: topic.first_post.id + ) + expect(topic_custom_field.exists?).to eq(true) + expect(topic_json_custom_field.exists?).to eq(true) + expect(post_custom_field.exists?).to eq(true) + end end context 'sending a message' do @@ -146,7 +182,7 @@ describe CustomWizard::Action do updater = wizard.create_updater(wizard.steps[1].id, {}) updater.update - category = Category.find_by(id: wizard.current_submission['action_8']) + category = Category.find_by(id: wizard.current_submission.fields['action_8']) expect(updater.result[:redirect_on_next]).to eq( "/new-topic?title=Title%20of%20the%20composer%20topic&body=I%20am%20interpolating%20some%20user%20fields%20Angus%20angus%20angus%40email.com&category_id=#{category.id}&tags=tag1" @@ -158,11 +194,10 @@ describe CustomWizard::Action do open_composer['post_template'] = "Body & more body & more body".dup wizard = CustomWizard::Wizard.new(@template, user) - action = CustomWizard::Action.new( wizard: wizard, action: open_composer, - data: {} + submission: wizard.current_submission ) action.perform @@ -179,20 +214,20 @@ describe CustomWizard::Action do wizard = CustomWizard::Builder.new(@template[:id], user).build wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update wizard.create_updater(wizard.steps[1].id, {}).update - expect(Category.where(id: wizard.current_submission['action_8']).exists?).to eq(true) + expect(Category.where(id: wizard.current_submission.fields['action_8']).exists?).to eq(true) end it 'creates a group' do wizard = CustomWizard::Builder.new(@template[:id], user).build wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update - expect(Group.where(name: wizard.current_submission['action_9']).exists?).to eq(true) + expect(Group.where(name: wizard.current_submission.fields['action_9']).exists?).to eq(true) end it 'adds a user to a group' do wizard = CustomWizard::Builder.new(@template[:id], user).build step_id = wizard.steps[0].id updater = wizard.create_updater(step_id, step_1_field_1: "Text input").update - group = Group.find_by(name: wizard.current_submission['action_9']) + group = Group.find_by(name: wizard.current_submission.fields['action_9']) expect(group.users.first.username).to eq('angus') end @@ -201,7 +236,7 @@ describe CustomWizard::Action do wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update wizard.create_updater(wizard.steps[1].id, {}).update expect(CategoryUser.where( - category_id: wizard.current_submission['action_8'], + category_id: wizard.current_submission.fields['action_8'], user_id: user.id ).first.notification_level).to eq(2) expect(CategoryUser.where( diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index d9d3524e..8e80d806 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -189,7 +189,10 @@ describe CustomWizard::Builder do context "user has partially completed" do before do wizard = CustomWizard::Wizard.new(@template, user) - wizard.set_submissions(step_1_field_1: 'I am a user submission') + data = { + step_1_field_1: 'I am a user submission' + } + CustomWizard::Submission.new(wizard, data).save end it 'returns saved submissions' do @@ -253,9 +256,9 @@ describe CustomWizard::Builder do end it 'is permitted if required data is present' do - CustomWizard::Wizard.set_submissions('super_mega_fun_wizard', user, - required_data: "required_value" - ) + wizard = CustomWizard::Wizard.create('super_mega_fun_wizard', user) + CustomWizard::Submission.new(wizard, step_1_field_1: "required").save + expect( CustomWizard::Builder.new(@template[:id], user).build .steps.first @@ -274,7 +277,7 @@ describe CustomWizard::Builder do wizard = CustomWizard::Builder.new(@template[:id], user).build({}, param: 'param_value' ) - expect(wizard.current_submission['saved_param']).to eq('param_value') + expect(wizard.current_submission.fields['saved_param']).to eq('param_value') end end @@ -336,31 +339,27 @@ describe CustomWizard::Builder do context 'on update' do def perform_update(step_id, submission) - wizard = CustomWizard::Builder.new(@template[:id], user).build - updater = wizard.create_updater(step_id, submission) + updater = @wizard.create_updater(step_id, submission) updater.update updater end it 'saves submissions' do + @wizard = CustomWizard::Builder.new(@template[:id], user).build perform_update('step_1', step_1_field_1: 'Text input') - expect( - CustomWizard::Wizard.submissions(@template[:id], user) - .first['step_1_field_1'] - ).to eq('Text input') + expect(@wizard.current_submission.fields['step_1_field_1']).to eq('Text input') end context 'save submissions disabled' do before do @template[:save_submissions] = false CustomWizard::Template.save(@template.as_json) + @wizard = CustomWizard::Builder.new(@template[:id], user).build end it "does not save submissions" do perform_update('step_1', step_1_field_1: 'Text input') - expect( - CustomWizard::Wizard.submissions(@template[:id], user).first - ).to eq(nil) + expect(@wizard.current_submission.present?).to eq(false) end end end diff --git a/spec/components/custom_wizard/custom_field_spec.rb b/spec/components/custom_wizard/custom_field_spec.rb index 3c9f1706..b17e26c6 100644 --- a/spec/components/custom_wizard/custom_field_spec.rb +++ b/spec/components/custom_wizard/custom_field_spec.rb @@ -49,6 +49,40 @@ describe CustomWizard::CustomField do end context "validation" do + it "does not save without required attributes" do + invalid_field_json = custom_field_json['custom_fields'].first + invalid_field_json['klass'] = nil + + custom_field = CustomWizard::CustomField.new(nil, invalid_field_json) + expect(custom_field.save).to eq(false) + expect(custom_field.valid?).to eq(false) + expect(custom_field.errors.full_messages.first).to eq( + I18n.t("wizard.custom_field.error.required_attribute", attr: "klass") + ) + expect( + PluginStoreRow.where( + plugin_name: CustomWizard::CustomField::NAMESPACE, + key: custom_field.name + ).exists? + ).to eq(false) + end + + it "does save without optional attributes" do + field_json = custom_field_json['custom_fields'].first + field_json['serializers'] = nil + + custom_field = CustomWizard::CustomField.new(nil, field_json) + expect(custom_field.save).to eq(true) + expect(custom_field.valid?).to eq(true) + expect( + PluginStoreRow.where(" + plugin_name = '#{CustomWizard::CustomField::NAMESPACE}' AND + key = '#{custom_field.name}' AND + value::jsonb = '#{field_json.except('name').to_json}'::jsonb + ",).exists? + ).to eq(true) + end + it "does not save with an unsupported class" do invalid_field_json = custom_field_json['custom_fields'].first invalid_field_json['klass'] = 'user' @@ -178,6 +212,22 @@ describe CustomWizard::CustomField do it "lists saved custom field records by attribute value" do expect(CustomWizard::CustomField.list_by(:klass, 'topic').length).to eq(1) end + + it "lists saved custom field records by optional values" do + field_json = custom_field_json['custom_fields'].first + field_json['serializers'] = nil + + custom_field = CustomWizard::CustomField.new(nil, field_json) + expect(CustomWizard::CustomField.list_by(:serializers, ['post']).length).to eq(0) + end + + it "lists custom field records added by other plugins " do + expect(CustomWizard::CustomField.external_list.length).to eq(11) + end + + it "lists all custom field records" do + expect(CustomWizard::CustomField.full_list.length).to eq(15) + end end it "is enabled if there are custom fields" do diff --git a/spec/components/custom_wizard/mapper_spec.rb b/spec/components/custom_wizard/mapper_spec.rb index 434f0001..ed66d7c1 100644 --- a/spec/components/custom_wizard/mapper_spec.rb +++ b/spec/components/custom_wizard/mapper_spec.rb @@ -229,28 +229,40 @@ describe CustomWizard::Mapper do ).perform).to eq("value 2") end - it "interpolates user fields" do - expect(CustomWizard::Mapper.new( - inputs: inputs['interpolate_user_field'], - data: data, - user: user1 - ).perform).to eq("Name: Angus") - end + context "interpolates" do + it "user fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_field'], + data: data, + user: user1 + ).perform).to eq("Name: Angus") + end - it "interpolates wizard fields" do - expect(CustomWizard::Mapper.new( - inputs: inputs['interpolate_wizard_field'], - data: data, - user: user1 - ).perform).to eq("Input 1: value 1") - end + it "user emails" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_email'], + data: data, + user: user1 + ).perform).to eq("Email: angus@email.com") + end - it "interpolates date" do - expect(CustomWizard::Mapper.new( - inputs: inputs['interpolate_timestamp'], - data: data, - user: user1 - ).perform).to eq("Time: #{Time.now.strftime("%B %-d, %Y")}") + it "user options" do + user1.user_option.update_columns(email_level: UserOption.email_level_types[:never]) + + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_option'], + data: data, + user: user1 + ).perform).to eq("Email Level: #{UserOption.email_level_types[:never]}") + end + + it "date" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_timestamp'], + data: data, + user: user1 + ).perform).to eq("Time: #{Time.now.strftime("%B %-d, %Y")}") + end end it "handles greater than pairs" do diff --git a/spec/components/custom_wizard/submission_spec.rb b/spec/components/custom_wizard/submission_spec.rb new file mode 100644 index 00000000..a8c33861 --- /dev/null +++ b/spec/components/custom_wizard/submission_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require_relative '../../plugin_helper' + +describe CustomWizard::Submission do + fab!(:user) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } + + let(:template_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + + before do + CustomWizard::Template.save(template_json, skip_jobs: true) + + template_json_2 = template_json.dup + template_json_2["id"] = "super_mega_fun_wizard_2" + CustomWizard::Template.save(template_json_2, skip_jobs: true) + + @wizard = CustomWizard::Wizard.create(template_json["id"], user) + @wizard2 = CustomWizard::Wizard.create(template_json["id"], user2) + @wizard3 = CustomWizard::Wizard.create(template_json_2["id"], user) + + described_class.new(@wizard, step_1_field_1: "I am a user submission").save + described_class.new(@wizard2, step_1_field_1: "I am another user's submission").save + described_class.new(@wizard3, step_1_field_1: "I am a user submission on another wizard").save + end + + it "saves a user's submission" do + expect( + described_class.get(@wizard, user.id).fields["step_1_field_1"] + ).to eq("I am a user submission") + end + + it "list submissions by wizard" do + expect(described_class.list(@wizard).size).to eq(2) + end + + it "list submissions by wizard and user" do + expect(described_class.list(@wizard, user_id: user.id).size).to eq(1) + end +end diff --git a/spec/components/custom_wizard/template_spec.rb b/spec/components/custom_wizard/template_spec.rb index fb76e0c4..0e3dbdbe 100644 --- a/spec/components/custom_wizard/template_spec.rb +++ b/spec/components/custom_wizard/template_spec.rb @@ -41,6 +41,14 @@ describe CustomWizard::Template do ).to eq(nil) end + it "removes user wizard redirects if template is removed" do + user.custom_fields['redirect_to_wizard'] = 'super_mega_fun_wizard' + user.save_custom_fields(true) + + CustomWizard::Template.remove('super_mega_fun_wizard') + expect(user.reload.custom_fields['redirect_to_wizard']).to eq(nil) + end + it "checks for wizard template existence" do expect( CustomWizard::Template.exists?('super_mega_fun_wizard') diff --git a/spec/components/custom_wizard/update_validator_spec.rb b/spec/components/custom_wizard/update_validator_spec.rb index 81212b4b..e7658d8c 100644 --- a/spec/components/custom_wizard/update_validator_spec.rb +++ b/spec/components/custom_wizard/update_validator_spec.rb @@ -132,4 +132,44 @@ describe CustomWizard::UpdateValidator do updater.errors.messages[:step_2_field_6].first ).to eq(nil) end + + it 'validates date fields' do + @template[:steps][1][:fields][0][:format] = "DD-MM-YYYY" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_1: '13-11-2021') + expect( + updater.errors.messages[:step_2_field_1].first + ).to eq(nil) + end + + it 'doesn\'t validate date field if the format is not respected' do + @template[:steps][1][:fields][0][:format] = "MM-DD-YYYY" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_1: '13-11-2021') + expect( + updater.errors.messages[:step_2_field_1].first + ).to eq(I18n.t('wizard.field.invalid_date')) + end + + it 'validates date time fields' do + @template[:steps][1][:fields][2][:format] = "DD-MM-YYYY HH:mm:ss" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_3: '13-11-2021 09:15:00') + expect( + updater.errors.messages[:step_2_field_3].first + ).to eq(nil) + end + + it 'doesn\'t validate date time field if the format is not respected' do + @template[:steps][1][:fields][2][:format] = "MM-DD-YYYY HH:mm:ss" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_3: '13-11-2021 09:15') + expect( + updater.errors.messages[:step_2_field_3].first + ).to eq(I18n.t('wizard.field.invalid_date')) + end end diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb index aed44fe6..67905f5a 100644 --- a/spec/components/custom_wizard/wizard_spec.rb +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -34,7 +34,7 @@ describe CustomWizard::Wizard do template_json['steps'].each do |step_template| @wizard.append_step(step_template['id']) end - @wizard.update_step_order! + @wizard.update! end def progress_step(step_id, acting_user: user, wizard: @wizard) @@ -44,7 +44,7 @@ describe CustomWizard::Wizard do context: wizard.id, subject: step_id ) - @wizard.update_step_order! + @wizard.update! end it "appends steps" do @@ -72,7 +72,7 @@ describe CustomWizard::Wizard do expect(@wizard.steps.first.index).to eq(2) expect(@wizard.steps.last.index).to eq(0) - @wizard.update_step_order! + @wizard.update! expect(@wizard.steps.first.id).to eq("step_3") expect(@wizard.steps.last.id).to eq("step_1") @@ -173,6 +173,8 @@ describe CustomWizard::Wizard do progress_step("step_2", acting_user: trusted_user) progress_step("step_3", acting_user: trusted_user) + @permitted_template["multiple_submissions"] = true + expect( CustomWizard::Wizard.new(@permitted_template, trusted_user).can_access? ).to eq(true) @@ -197,19 +199,13 @@ describe CustomWizard::Wizard do end it "lists the site categories" do + Site.clear_cache expect(@wizard.categories.length).to eq(1) end context "submissions" do before do - @wizard.set_submissions(step_1_field_1: 'I am a user submission') - end - - it "sets the user's submission" do - expect( - PluginStore.get("#{template_json['id']}_submissions", user.id) - .first['step_1_field_1'] - ).to eq('I am a user submission') + CustomWizard::Submission.new(@wizard, step_1_field_1: "I am a user submission").save end it "lists the user's submissions" do @@ -217,20 +213,10 @@ describe CustomWizard::Wizard do end it "returns the user's current submission" do - expect(@wizard.current_submission['step_1_field_1']).to eq('I am a user submission') + expect(@wizard.current_submission.fields["step_1_field_1"]).to eq("I am a user submission") end end - it "provides class methods to set and list submissions" do - CustomWizard::Wizard.set_submissions(template_json['id'], user, - step_1_field_1: 'I am a user submission' - ) - expect( - CustomWizard::Wizard.submissions(template_json['id'], user) - .first['step_1_field_1'] - ).to eq('I am a user submission') - end - context "class methods" do before do CustomWizard::Template.save(@permitted_template, skip_jobs: true) @@ -271,7 +257,7 @@ describe CustomWizard::Wizard do it "sets wizard redirects if user is permitted" do CustomWizard::Template.save(@permitted_template, skip_jobs: true) - CustomWizard::Wizard.set_wizard_redirect('super_mega_fun_wizard', trusted_user) + CustomWizard::Wizard.set_user_redirect('super_mega_fun_wizard', trusted_user) expect( trusted_user.custom_fields['redirect_to_wizard'] ).to eq("super_mega_fun_wizard") @@ -279,7 +265,7 @@ describe CustomWizard::Wizard do it "does not set a wizard redirect if user is not permitted" do CustomWizard::Template.save(@permitted_template, skip_jobs: true) - CustomWizard::Wizard.set_wizard_redirect('super_mega_fun_wizard', user) + CustomWizard::Wizard.set_user_redirect('super_mega_fun_wizard', user) expect( trusted_user.custom_fields['redirect_to_wizard'] ).to eq(nil) diff --git a/spec/extensions/extra_locales_controller_spec.rb b/spec/extensions/extra_locales_controller_spec.rb index 91a4e8c3..a71e39c4 100644 --- a/spec/extensions/extra_locales_controller_spec.rb +++ b/spec/extensions/extra_locales_controller_spec.rb @@ -37,6 +37,13 @@ describe ExtraLocalesControllerCustomWizard, type: :request do expect(response.status).to eq(200) end + it "returns wizard locales when requested by user in a wizard step" do + sign_in(new_user) + + get @locale_url, headers: { 'REFERER' => "/w/super-mega-fun-wizard/steps/step_1" } + expect(response.status).to eq(200) + end + it "return wizard locales if user cant access wizard" do template[:permitted] = permitted["permitted"] CustomWizard::Template.save(template.as_json) diff --git a/spec/extensions/sprockets_directive_spec.rb b/spec/extensions/sprockets_directive_spec.rb new file mode 100644 index 00000000..5a074040 --- /dev/null +++ b/spec/extensions/sprockets_directive_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative '../plugin_helper' + +describe "Sprockets: require_tree_discourse directive" do + let(:discourse_asset_path) { + "#{Rails.root}/app/assets/javascripts/" + } + let(:fixture_asset_path) { + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/" + } + let(:test_file_contents) { + "console.log('hello')" + } + let(:resolved_file_contents) { + File.read( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/resolved_js_file_contents.txt" + ) + } + + before do + @env ||= Sprockets::Environment.new + discourse_asset_path = "#{Rails.root}/app/assets/javascripts/" + fixture_asset_path = "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/" + @env.append_path(discourse_asset_path) + @env.append_path(fixture_asset_path) + @env.cache = {} + end + + def create_tmp_folder_and_run(path, file_contents, &block) + dir = File.dirname(path) + unless File.directory?(dir) + FileUtils.mkdir_p(dir) + end + + File.new(path, 'w') + File.write(path, file_contents) + yield block if block_given? + FileUtils.rm_r(dir) + end + + it "includes assets from the discourse core" do + create_tmp_folder_and_run("#{discourse_asset_path}/sptest/test.js", test_file_contents) do + expect(@env.find_asset("require_tree_discourse_test.js").to_s).to eq(resolved_file_contents) + end + end + + it "throws ArgumentError if path is empty" do + expect { @env.find_asset("require_tree_discourse_empty.js") }.to raise_error(CustomWizard::SprocketsEmptyPath).with_message("path cannot be empty") + end + + it "throws ArgumentError if path is non non-existent" do + expect { @env.find_asset("require_tree_discourse_non_existant.js") }.to raise_error(CustomWizard::SprocketsFileNotFound) + end +end diff --git a/spec/fixtures/mapper/inputs.json b/spec/fixtures/mapper/inputs.json index f7d98903..443f186b 100644 --- a/spec/fixtures/mapper/inputs.json +++ b/spec/fixtures/mapper/inputs.json @@ -57,6 +57,22 @@ "output": "Name: u{name}" } ], + "interpolate_user_email": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Email: u{email}" + } + ], + "interpolate_user_option": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Email Level: u{email_level}" + } + ], "interpolate_wizard_field": [ { "type": "assignment", diff --git a/spec/fixtures/sprockets/require_tree_discourse_empty.js b/spec/fixtures/sprockets/require_tree_discourse_empty.js new file mode 100644 index 00000000..df264ec5 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_empty.js @@ -0,0 +1 @@ +//= require_tree_discourse \ No newline at end of file diff --git a/spec/fixtures/sprockets/require_tree_discourse_non_existant.js b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js new file mode 100644 index 00000000..d9b2be76 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js @@ -0,0 +1 @@ +//= require_tree_discourse dummy_path \ No newline at end of file diff --git a/spec/fixtures/sprockets/require_tree_discourse_test.js b/spec/fixtures/sprockets/require_tree_discourse_test.js new file mode 100644 index 00000000..a86aa0d7 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_test.js @@ -0,0 +1 @@ +//= require_tree_discourse sptest \ No newline at end of file diff --git a/spec/fixtures/sprockets/resolved_js_file_contents.txt b/spec/fixtures/sprockets/resolved_js_file_contents.txt new file mode 100644 index 00000000..53e2cfa2 --- /dev/null +++ b/spec/fixtures/sprockets/resolved_js_file_contents.txt @@ -0,0 +1,3 @@ +eval("define(\"sptest/test\", [], function () {\n \"use strict\";\n\n console.log('hello');\n});" + "\n//# sourceURL=sptest/test"); +; +eval("" + "\n//# sourceURL=require_tree_discourse_test"); diff --git a/spec/fixtures/step/required_data.json b/spec/fixtures/step/required_data.json index 9f65d516..7ff8bcaf 100644 --- a/spec/fixtures/step/required_data.json +++ b/spec/fixtures/step/required_data.json @@ -6,9 +6,9 @@ "pairs": [ { "index": 0, - "key": "required_data", + "key": "step_1_field_1", "key_type": "text", - "value": "required_value", + "value": "required", "value_type": "text", "connector": "equal" } diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index c21d445c..a505c0d3 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -3,7 +3,6 @@ "name": "Super Mega Fun Wizard", "background": "#333333", "save_submissions": true, - "multiple_submissions": true, "after_signup": false, "prompt_completion": false, "theme_id": 2, @@ -391,10 +390,34 @@ "pairs": [ { "index": 0, - "key": "custom_field_1", + "key": "post_field", "key_type": "text", - "value": "title", - "value_type": "user_field", + "value": "Post custom field value", + "value_type": "text", + "connector": "association" + }, + { + "index": 1, + "key": "topic.topic_field", + "key_type": "text", + "value": "Topic custom field value", + "value_type": "text", + "connector": "association" + }, + { + "index": 2, + "key": "topic.topic_json_field{}.key_1", + "key_type": "text", + "value": "Key 1 value", + "value_type": "text", + "connector": "association" + }, + { + "index": 3, + "key": "topic.topic_json_field{}.key_2", + "key_type": "text", + "value": "Key 2 value", + "value_type": "text", "connector": "association" } ] diff --git a/spec/jobs/clear_after_time_wizard_spec.rb b/spec/jobs/clear_after_time_wizard_spec.rb deleted file mode 100644 index 935036a3..00000000 --- a/spec/jobs/clear_after_time_wizard_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require_relative '../plugin_helper' - -describe Jobs::ClearAfterTimeWizard do - fab!(:user1) { Fabricate(:user) } - fab!(:user2) { Fabricate(:user) } - fab!(:user3) { Fabricate(:user) } - - let(:template) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read).with_indifferent_access - } - - it "clears wizard redirect for all users " do - after_time_template = template.dup - after_time_template["after_time"] = true - after_time_template["after_time_scheduled"] = (Time.now + 3.hours).iso8601 - - CustomWizard::Template.save(after_time_template) - - Jobs::SetAfterTimeWizard.new.execute(wizard_id: 'super_mega_fun_wizard') - - expect( - UserCustomField.where( - name: 'redirect_to_wizard', - value: 'super_mega_fun_wizard' - ).length - ).to eq(3) - - described_class.new.execute(wizard_id: 'super_mega_fun_wizard') - - expect( - UserCustomField.where(" - name = 'redirect_to_wizard' AND - value = 'super_mega_fun_wizard' - ").exists? - ).to eq(false) - end -end diff --git a/spec/plugin_helper.rb b/spec/plugin_helper.rb index 6680874f..93f33a81 100644 --- a/spec/plugin_helper.rb +++ b/spec/plugin_helper.rb @@ -6,7 +6,7 @@ if ENV['SIMPLECOV'] SimpleCov.start do root "plugins/discourse-custom-wizard" track_files "plugins/discourse-custom-wizard/**/*.rb" - add_filter { |src| src.filename =~ /(\/spec\/|\/db\/|plugin\.rb|api)/ } + add_filter { |src| src.filename =~ /(\/spec\/|\/db\/|plugin\.rb|api|gems)/ } SimpleCov.minimum_coverage 80 end end diff --git a/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb b/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb index e006e65a..8c1a8550 100644 --- a/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb @@ -17,9 +17,9 @@ describe CustomWizard::AdminCustomFieldsController do sign_in(admin_user) end - it "returns the list of custom fields" do + it "returns the full list of custom fields" do get "/admin/wizards/custom-fields.json" - expect(response.parsed_body.length).to eq(4) + expect(response.parsed_body.length).to eq(15) end it "saves custom fields" do diff --git a/spec/requests/custom_wizard/admin/submissions_controller_spec.rb b/spec/requests/custom_wizard/admin/submissions_controller_spec.rb index f63eead5..36296e95 100644 --- a/spec/requests/custom_wizard/admin/submissions_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/submissions_controller_spec.rb @@ -5,6 +5,7 @@ describe CustomWizard::AdminSubmissionsController do fab!(:admin_user) { Fabricate(:user, admin: true) } fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } + fab!(:user3) { Fabricate(:user) } let(:template) { JSON.parse(File.open( @@ -12,35 +13,40 @@ describe CustomWizard::AdminSubmissionsController do ).read) } + let(:template_2) { + temp = template.dup + temp["id"] = "super_mega_fun_wizard_2" + temp + } + before do CustomWizard::Template.save(template, skip_jobs: true) - CustomWizard::Wizard.set_submissions(template['id'], user1, - step_1_field_1: "I am a user1's submission" - ) - CustomWizard::Wizard.set_submissions(template['id'], user2, - step_1_field_1: "I am a user2's submission" - ) + CustomWizard::Template.save(template_2, skip_jobs: true) + + wizard1 = CustomWizard::Wizard.create(template["id"], user1) + wizard2 = CustomWizard::Wizard.create(template["id"], user2) + wizard3 = CustomWizard::Wizard.create(template_2["id"], user3) + + CustomWizard::Submission.new(wizard1, step_1_field_1: "I am a user1's submission").save + CustomWizard::Submission.new(wizard2, step_1_field_1: "I am a user2's submission").save + CustomWizard::Submission.new(wizard3, step_1_field_1: "I am a user3's submission").save + sign_in(admin_user) end - it "returns a basic list of wizards" do + it "returns a list of wizards" do get "/admin/wizards/submissions.json" - expect(response.parsed_body.length).to eq(1) + expect(response.parsed_body.length).to eq(2) expect(response.parsed_body.first['id']).to eq(template['id']) end - it "returns the all user's submissions for a wizard" do + it "returns users' submissions for a wizard" do get "/admin/wizards/submissions/#{template['id']}.json" expect(response.parsed_body['submissions'].length).to eq(2) end - it "returns the all user's submissions for a wizard" do - get "/admin/wizards/submissions/#{template['id']}.json" - expect(response.parsed_body['submissions'].length).to eq(2) - end - - it "downloads all user submissions" do - get "/admin/wizards/submissions/#{template['id']}/download" - expect(response.parsed_body.length).to eq(2) + it "downloads submissions" do + get "/admin/wizards/submissions/#{template_2['id']}/download" + expect(response.parsed_body.length).to eq(1) end end diff --git a/spec/requests/custom_wizard/application_controller_spec.rb b/spec/requests/custom_wizard/application_controller_spec.rb index f79db877..0835f246 100644 --- a/spec/requests/custom_wizard/application_controller_spec.rb +++ b/spec/requests/custom_wizard/application_controller_spec.rb @@ -39,10 +39,16 @@ describe ApplicationController do it "saves original destination of user" do get '/', headers: { 'REFERER' => "/t/2" } expect( - CustomWizard::Wizard.submissions(@template['id'], user) - .first['redirect_to'] + 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 "/" + expect(response.status).to eq(200) + end end context "who is not required to complete wizard" do diff --git a/spec/requests/custom_wizard/steps_controller_spec.rb b/spec/requests/custom_wizard/steps_controller_spec.rb index c58f13a2..5da75d8d 100644 --- a/spec/requests/custom_wizard/steps_controller_spec.rb +++ b/spec/requests/custom_wizard/steps_controller_spec.rb @@ -68,7 +68,7 @@ describe CustomWizard::StepsController do wizard_id = response.parsed_body['wizard']['id'] wizard = CustomWizard::Wizard.create(wizard_id, user) - expect(wizard.submissions.last['step_1_field_1']).to eq("Text input") + expect(wizard.current_submission.fields['step_1_field_1']).to eq("Text input") end context "raises an error" do @@ -175,8 +175,11 @@ describe CustomWizard::StepsController do wizard_id = response.parsed_body['wizard']['id'] wizard = CustomWizard::Wizard.create(wizard_id, user) - group_name = wizard.submissions.last['action_9'] + + group_name = wizard.submissions.first.fields['action_9'] group = Group.find_by(name: group_name) + + expect(group.present?).to eq(true) expect(group.full_name).to eq("My cool group") end @@ -249,4 +252,33 @@ describe CustomWizard::StepsController do expect(response.status).to eq(200) expect(response.parsed_body['final']).to eq(true) end + + it "excludes the non-included conditional fields from the submissions" do + new_template = wizard_template.dup + new_template['steps'][1]['fields'][0]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + + put '/w/super-mega-fun-wizard/steps/step_2.json', params: { + fields: { + step_2_field_1: "1995-04-23" + } + } + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } + } + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + submission = wizard.current_submission + expect(submission.fields.keys).not_to include("step_2_field_1") + end end diff --git a/spec/requests/custom_wizard/wizard_controller_spec.rb b/spec/requests/custom_wizard/wizard_controller_spec.rb index 3e7ddd3d..f2000bda 100644 --- a/spec/requests/custom_wizard/wizard_controller_spec.rb +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -11,6 +11,14 @@ describe CustomWizard::WizardController do ) } + let(:permitted_json) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read + ) + } + before do CustomWizard::Template.save( JSON.parse(File.open( @@ -47,6 +55,14 @@ describe CustomWizard::WizardController do expect(response.status).to eq(200) end + it 'lets user skip if user cant access wizard' do + @template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(@template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + end + it 'returns a no skip message if user is not allowed to skip' do @template['required'] = 'true' CustomWizard::Template.save(@template) @@ -55,9 +71,8 @@ describe CustomWizard::WizardController do end it 'skip response contains a redirect_to if in users submissions' do - CustomWizard::Wizard.set_submissions(@template['id'], user, - redirect_to: '/t/2' - ) + @wizard = CustomWizard::Wizard.create(@template["id"], user) + CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save put '/w/super-mega-fun-wizard/skip.json' expect(response.parsed_body['redirect_to']).to eq('/t/2') end diff --git a/views/layouts/wizard.html.erb b/views/layouts/wizard.html.erb index efa09734..16d119b7 100644 --- a/views/layouts/wizard.html.erb +++ b/views/layouts/wizard.html.erb @@ -4,8 +4,8 @@ <%= discourse_stylesheet_link_tag :wizard, theme_id: nil %> <%= discourse_stylesheet_link_tag :wizard_custom %> - <%- if wizard_theme_ids.present? %> - <%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme), theme_ids: wizard_theme_ids %> + <%- if wizard_theme_id.present? %> + <%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme), theme_id: wizard_theme_id %> <%- end %> <%= preload_script "locales/#{I18n.locale}" %> @@ -29,7 +29,7 @@ <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %> - "> + <%= render partial: "layouts/head" %>