diff --git a/assets/javascripts/discourse/components/submission-field.js.es6 b/assets/javascripts/discourse/components/submission-field.js.es6 new file mode 100644 index 00000000..5ebc0ccd --- /dev/null +++ b/assets/javascripts/discourse/components/submission-field.js.es6 @@ -0,0 +1,112 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { equal } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; +import I18n from "I18n"; + +export default Component.extend({ + isText: equal("value.type", "text"), + isComposer: equal("value.type", "composer"), + isDate: equal("value.type", "date"), + isTime: equal("value.type", "time"), + isDateTime: equal("value.type", "date_time"), + isNumber: equal("value.type", "number"), + isCheckbox: equal("value.type", "checkbox"), + isUrl: equal("value.type", "url"), + isUpload: equal("value.type", "upload"), + isDropdown: equal("value.type", "dropdown"), + isTag: equal("value.type", "tag"), + isCategory: equal("value.type", "category"), + isGroup: equal("value.type", "group"), + isUser: equal("fieldName", "username"), + isUserSelector: equal("value.type", "user_selector"), + isSubmittedAt: equal("fieldName", "submitted_at"), + isTextArea: equal("value.type", "textarea"), + isComposerPreview: equal("value.type", "composer_preview"), + textState: "text-collapsed", + toggleText: I18n.t("admin.wizard.submissions.expand_text"), + + @discourseComputed("value") + checkboxValue(value) { + const isCheckbox = this.get("isCheckbox"); + if (isCheckbox) { + if (value.value.includes("true")) { + return true; + } else if (value.value.includes("false")) { + return false; + } + } + }, + + @action + expandText() { + const state = this.get("textState"); + + if (state === "text-collapsed") { + this.set("textState", "text-expanded"); + this.set("toggleText", I18n.t("admin.wizard.submissions.collapse_text")); + } else if (state === "text-expanded") { + this.set("textState", "text-collapsed"); + this.set("toggleText", I18n.t("admin.wizard.submissions.expand_text")); + } + }, + + @discourseComputed("value") + file(value) { + const isUpload = this.get("isUpload"); + if (isUpload) { + return value.value; + } + }, + + @discourseComputed("value") + submittedUsers(value) { + const isUserSelector = this.get("isUserSelector"); + const users = []; + + if (isUserSelector) { + const userData = value.value; + const usernames = []; + + if (userData.indexOf(",")) { + usernames.push(...userData.split(",")); + + usernames.forEach((u) => { + const user = { + username: u, + url: `/u/${u}`, + }; + users.push(user); + }); + } + } + return users; + }, + + @discourseComputed("value") + userProfileUrl(value) { + const isUser = this.get("isUser"); + + if (isUser) { + return `/u/${value.username}`; + } + }, + + @discourseComputed("value") + categoryUrl(value) { + const isCategory = this.get("isCategory"); + + if (isCategory) { + return `/c/${value.value}`; + } + }, + + @discourseComputed("value") + groupUrl(value) { + const isGroup = this.get("isGroup"); + + if (isGroup) { + return `/g/${value.value}`; + } + }, +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 index 9559b01b..f45013d7 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 @@ -9,6 +9,8 @@ export default Controller.extend({ page: 0, canLoadMore: true, logs: [], + documentationUrl: "https://thepavilion.io/t/2818", + messageKey: "viewing", loadLogs() { if (!this.canLoadMore) { diff --git a/assets/javascripts/discourse/controllers/admin-wizards-submissions-columns.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-submissions-columns.js.es6 new file mode 100644 index 00000000..4af487ee --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-submissions-columns.js.es6 @@ -0,0 +1,15 @@ +import Controller from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Controller.extend(ModalFunctionality, { + actions: { + save() { + this.send("closeModal"); + }, + resetToDefault() { + this.get("model.fields").forEach((field) => { + field.set("enabled", true); + }); + }, + }, +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 index f5f9926d..7ba0050f 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 @@ -1,6 +1,65 @@ import Controller from "@ember/controller"; +import { empty } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; +import showModal from "discourse/lib/show-modal"; +import CustomWizard from "../models/custom-wizard"; export default Controller.extend({ downloadUrl: fmt("wizard.id", "/admin/wizards/submissions/%@/download"), + noResults: empty("submissions"), + page: 0, + total: 0, + + loadMoreSubmissions() { + const page = this.get("page"); + const wizardId = this.get("wizard.id"); + + this.set("loadingMore", true); + CustomWizard.submissions(wizardId, page) + .then((result) => { + if (result.submissions) { + this.get("submissions").pushObjects(result.submissions); + } + }) + .finally(() => { + this.set("loadingMore", false); + }); + }, + + @discourseComputed("submissions", "fields.@each.enabled") + displaySubmissions(submissions, fields) { + let result = []; + + submissions.forEach((submission) => { + let sub = {}; + + Object.keys(submission).forEach((fieldId) => { + if (fields.some((f) => f.id === fieldId && f.enabled)) { + sub[fieldId] = submission[fieldId]; + } + }); + result.push(sub); + }); + + return result; + }, + + actions: { + loadMore() { + if (!this.loadingMore && this.submissions.length < this.total) { + this.set("page", this.get("page") + 1); + this.loadMoreSubmissions(); + } + }, + + showEditColumnsModal() { + return showModal("admin-wizards-submissions-columns", { + model: { + fields: this.get("fields"), + submissions: this.get("submissions"), + }, + }); + }, + }, }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 new file mode 100644 index 00000000..7388a8d6 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 @@ -0,0 +1,34 @@ +import Controller from "@ember/controller"; +import { default as discourseComputed } from "discourse-common/utils/decorators"; + +export default Controller.extend({ + documentationUrl: "https://thepavilion.io/t/2818", + + @discourseComputed("wizardId") + wizardName(wizardId) { + let currentWizard = this.wizardList.find( + (wizard) => wizard.id === wizardId + ); + if (currentWizard) { + return currentWizard.name; + } + }, + + @discourseComputed("wizardName") + messageOpts(wizardName) { + return { + wizardName, + }; + }, + + @discourseComputed("wizardId") + messageKey(wizardId) { + let key = "select"; + + if (wizardId) { + key = "viewing"; + } + + return key; + }, +}); diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 80c4d86a..301354e9 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -1,10 +1,10 @@ -import { ajax } from "discourse/lib/ajax"; import EmberObject from "@ember/object"; -import { buildProperties, mapped, present } from "../lib/wizard-json"; -import { listProperties, snakeCase } from "../lib/wizard"; -import wizardSchema from "../lib/wizard-schema"; -import { Promise } from "rsvp"; +import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { Promise } from "rsvp"; +import { listProperties, snakeCase } from "../lib/wizard"; +import { buildProperties, mapped, present } from "../lib/wizard-json"; +import wizardSchema from "../lib/wizard-schema"; const CustomWizard = EmberObject.extend({ save(opts) { @@ -211,10 +211,55 @@ CustomWizard.reopenClass({ .catch(popupAjaxError); }, - submissions(wizardId) { + submissions(wizardId, page = null) { + let data = {}; + + if (page) { + data.page = page; + } + return ajax(`/admin/wizards/submissions/${wizardId}`, { type: "GET", - }).catch(popupAjaxError); + data, + }) + .then((result) => { + if (result.wizard) { + let fields = [{ id: "username", label: "User" }]; + let submissions = []; + let wizard = result.wizard; + let total = result.total; + + result.submissions.forEach((s) => { + let submission = { + username: s.user, + }; + + Object.keys(s.fields).forEach((fieldId) => { + if (!fields.some((field) => field.id === fieldId)) { + fields.push({ id: fieldId, label: s.fields[fieldId].label }); + } + submission[fieldId] = s.fields[fieldId]; + }); + submission["submitted_at"] = s.submitted_at; + submissions.push(submission); + }); + + let submittedAt = { + id: "submitted_at", + label: "Submitted At", + }; + + fields.push(submittedAt); + + return { + wizard, + fields, + submissions, + total, + }; + } + }) + .catch(popupAjaxError); }, create(wizardJson = {}) { 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 73168ff3..829d4d13 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 @@ -1,7 +1,7 @@ -import CustomWizard from "../models/custom-wizard"; +import { A } from "@ember/array"; +import EmberObject from "@ember/object"; import DiscourseRoute from "discourse/routes/discourse"; - -const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"]; +import CustomWizard from "../models/custom-wizard"; export default DiscourseRoute.extend({ model(params) { @@ -9,34 +9,16 @@ export default DiscourseRoute.extend({ }, setupController(controller, model) { - if (model && model.submissions) { - let fields = ["username"]; - model.submissions.forEach((s) => { - Object.keys(s.fields).forEach((k) => { - if (!excludedMetaFields.includes(k) && fields.indexOf(k) < 0) { - fields.push(k); - } - }); - }); - - let submissions = []; - model.submissions.forEach((s) => { - let submission = { - username: s.username, - }; - Object.keys(s.fields).forEach((f) => { - if (fields.includes(f)) { - submission[f] = s.fields[f]; - } - }); - submissions.push(submission); - }); - - controller.setProperties({ - wizard: model.wizard, - submissions, - fields, - }); - } + const fields = model.fields.map((f) => { + const fieldsObject = EmberObject.create(f); + fieldsObject.enabled = true; + return fieldsObject; + }); + controller.setProperties({ + wizard: model.wizard, + fields: A(fields), + submissions: A(model.submissions), + total: model.total, + }); }, }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs index b0dd3de6..ea0afb7c 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs @@ -8,6 +8,12 @@ class="refresh"}} +{{wizard-message + key=messageKey + opts=messageOpts + url=documentationUrl + component="logs"}} + {{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}} {{#if noResults}}

{{i18n "search.no_results"}}

diff --git a/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs index 6d1f255b..6f4996f6 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs @@ -1,8 +1,24 @@ {{#if submissions}}
- + - +
+ {{d-button + icon="sliders-h" + label="admin.wizard.submissions.edit_columns" + action=(action "showEditColumnsModal") + class="btn-default open-edit-columns-btn download-link" + }} +
+ +
{{d-icon "download"}} {{i18n "admin.wizard.submissions.download"}} @@ -11,23 +27,39 @@
- - - - {{#each fields as |f|}} - - {{/each}} - - - - {{#each submissions as |s|}} - - {{#each-in s as |k v|}} - - {{/each-in}} - - {{/each}} - -
{{f}}
{{v}}
+ {{#load-more selector=".wizard-submissions tr" action=(action "loadMore")}} + {{#if noResults}} +

+ {{i18n "search.no_results"}} +

+ {{else}} + + + + {{#each fields as |field|}} + {{#if field.enabled}} + + {{/if}} + {{/each}} + + + + {{#each displaySubmissions as |submission|}} + + {{#each-in submission as |field value|}} + + {{/each-in}} + + {{/each}} + +
+ {{field.label}} +
+ {{submission-field fieldName=field value=value}} +
+ {{/if}} + + {{conditional-loading-spinner condition=loadingMore}} + {{/load-more}}
-{{/if}} +{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs index d843485a..07dd1682 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs @@ -1,4 +1,4 @@ -
+
{{combo-box value=wizardId content=wizardList @@ -8,6 +8,12 @@ )}}
+{{wizard-message + key=messageKey + opts=messageOpts + url=documentationUrl + component="submissions"}} +
{{outlet}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index a2e104f7..0578e01f 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -8,6 +8,10 @@ {{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}} {{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}} {{nav-item route="adminWizardsPro" label="admin.wizard.pro.nav_label"}} + +
+ {{d-icon "far-life-ring"}}{{i18n "admin.wizard.pro_support_button.label"}} +
{{/admin-nav}}
diff --git a/assets/javascripts/discourse/templates/components/submission-field.hbs b/assets/javascripts/discourse/templates/components/submission-field.hbs new file mode 100644 index 00000000..831dcc3a --- /dev/null +++ b/assets/javascripts/discourse/templates/components/submission-field.hbs @@ -0,0 +1,163 @@ +{{#if isText}} + {{value.value}} +{{/if}} + +{{#if isTextArea}} +
+

+ {{value.value}} +

+ + {{toggleText}} + +
+{{/if}} + +{{#if isComposer}} +
+

+ {{value.value}} +

+ + {{toggleText}} + +
+{{/if}} + +{{#if isComposerPreview}} + {{d-icon "comment-alt"}} + + {{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}} + +{{/if}} + +{{#if isTextOnly}} + {{value.value}} +{{/if}} + +{{#if isDate}} + + {{d-icon "calendar"}}{{value.value}} + +{{/if}} + +{{#if isTime}} + + {{d-icon "clock"}}{{value.value}} + +{{/if}} + +{{#if isDateTime}} + + {{d-icon "calendar"}}{{format-date value.value format="medium"}} + +{{/if}} + +{{#if isNumber}} + {{value.value}} +{{/if}} + +{{#if isCheckbox}} + {{#if checkboxValue}} + + {{d-icon "check"}}{{value.value}} + + {{else}} + + {{d-icon "times"}}{{value.value}} + + {{/if}} +{{/if}} + +{{#if isUrl}} + + {{d-icon "link"}} + + {{value.value}} + + +{{/if}} + +{{#if isUpload}} + + {{file.original_filename}} + +{{/if}} + +{{#if isDropdown}} + + {{d-icon "check-square"}} + {{value.value}} + +{{/if}} + +{{#if isTag}} + {{#each value.value as |tag|}} + {{discourse-tag tag}} + {{/each}} +{{/if}} + +{{#if isCategory}} + + {{i18n "admin.wizard.submissions.category_id"}}: + + + {{value.value}} + +{{/if}} + +{{#if isGroup}} + + {{i18n "admin.wizard.submissions.group_id"}}: + + {{value.value}} +{{/if}} + +{{#if isUserSelector}} + {{#each submittedUsers as |user|}} + {{d-icon "user"}} + + {{user.username}} + + {{/each}} +{{/if}} + +{{#if isUser}} + {{#link-to "user" value}} + {{avatar value imageSize="tiny"}} + {{/link-to}} + + {{value.username}} + +{{/if}} + +{{#if isSubmittedAt}} + + {{d-icon "clock"}}{{format-date value format="tiny"}} + +{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/modal/admin-wizards-submissions-columns.hbs b/assets/javascripts/discourse/templates/modal/admin-wizards-submissions-columns.hbs new file mode 100644 index 00000000..24743a92 --- /dev/null +++ b/assets/javascripts/discourse/templates/modal/admin-wizards-submissions-columns.hbs @@ -0,0 +1,32 @@ +{{#d-modal-body title="directory.edit_columns.title"}} + {{#if loading}} + {{loading-spinner size="large"}} + {{else}} +
+ {{#each model.fields as |field|}} +
+
+ +
+
+ {{/each}} +
+ {{/if}} +{{/d-modal-body}} + + \ No newline at end of file diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index 8b30ad94..d0fef99a 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -2,6 +2,7 @@ //= require discourse/app/mixins/singleton //= require discourse/app/mixins/upload +//= require discourse/app/mixins/composer-upload //= require discourse/app/adapters/rest diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index 3146da13..6e797405 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -2,6 +2,7 @@ @import "wizard-manager"; @import "wizard-api"; @import "common/components/buttons"; +@import "wizard-variables"; .admin-wizard-controls { display: flex; @@ -71,6 +72,51 @@ table td { min-width: 150px; } + + table thead th { + text-transform: capitalize; + } + + .submission-icon-item { + display: flex; + align-items: center; + svg { + margin-right: 5px; + } + } + + .submission-checkbox-true { + text-transform: capitalize; + color: var(--success); + } + + .submission-checkbox-false { + text-transform: capitalize; + color: var(--danger); + } + + .submission-long-text { + &-content { + white-space: nowrap; + word-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; + width: 250px; + margin-bottom: 0; + + &.text-expanded { + white-space: normal; + } + } + + a { + font-size: var(--font-down-1); + } + } + + .submission-composer-text { + font-family: monospace; + } } .admin-wizards-logs { @@ -203,6 +249,11 @@ &.underline { text-decoration: underline; } + + .controls { + margin-left: auto; + margin-right: 0.5rem; + } } .admin-wizard-buttons { @@ -833,3 +884,22 @@ padding-top: .25em; } } + +.btn.btn-pavilion-pro { + background: var(--pavilion-primary); + color: var(--pavilion-secondary); + + .d-icon { + color: var(--pavilion-secondary); + } + + &:hover, + &:focus { + background: darken($pavilionPrimary, 5%); + + &[href], + svg.d-icon { + color: darken($pavilionSecondary, 10%); + } + } +} diff --git a/assets/stylesheets/common/wizard-variables.scss b/assets/stylesheets/common/wizard-variables.scss new file mode 100644 index 00000000..68f02b6b --- /dev/null +++ b/assets/stylesheets/common/wizard-variables.scss @@ -0,0 +1,7 @@ +$pavilionPrimary: #3c1c8c; +$pavilionSecondary: #ffffff; + +:root { + --pavilion-primary: #3c1c8c; + --pavilion-secondary: #ffffff; +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 451c7786..f24b5866 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -59,6 +59,10 @@ en: condition: "Condition" index: "Index" + pro_support_button: + title: "Request Pro Support" + label: "Pro Support" + message: wizard: select: "Select a wizard, or create a new one" @@ -99,6 +103,13 @@ en: subscription_inactive: "Your subscription is inactive on this forum. Read more in the documentation." unauthorized: "You're unauthorized. If you have a subscription, it will become inactive in the next 48 hours." unauthorize_failed: Failed to unauthorize. + submissions: + select: "Select a wizard to see its submissions" + viewing: "You're viewing the submissions of the %{wizardName}. Click 'Download' on the right to download them." + documentation: "Check out the submissions documentation" + logs: + viewing: "View recent logs for wizards on the forum" + documentation: "Check out the logs documentation editor: show: "Show" @@ -367,6 +378,12 @@ en: nav_label: "Submissions" title: "{{name}} Submissions" download: "Download" + edit_columns: "Edit Columns" + expand_text: "Read More" + collapse_text: "Show Less" + group_id: "Group ID" + category_id: "Category ID" + composer_preview: "Composer Preview" api: label: "API" diff --git a/controllers/custom_wizard/admin/submissions.rb b/controllers/custom_wizard/admin/submissions.rb index 4cb2a0e4..c3bf809f 100644 --- a/controllers/custom_wizard/admin/submissions.rb +++ b/controllers/custom_wizard/admin/submissions.rb @@ -13,12 +13,16 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController def show render_json_dump( wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), - submissions: ActiveModel::ArraySerializer.new(ordered_submissions, each_serializer: CustomWizard::SubmissionSerializer) + submissions: ActiveModel::ArraySerializer.new( + submission_list.submissions, + each_serializer: CustomWizard::SubmissionSerializer + ), + total: submission_list.total ) end def download - send_data ordered_submissions.to_json, + send_data submission_list.submissions.to_json, filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json", content_type: "application/json", disposition: "attachment" @@ -26,7 +30,7 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController protected - def ordered_submissions - CustomWizard::Submission.list(@wizard, order_by: 'id') + def submission_list + CustomWizard::Submission.list(@wizard, page: params[:page].to_i) end end diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index 78f2fd4a..732c5d5e 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -69,7 +69,8 @@ class CustomWizard::WizardController < ::ApplicationController result.merge!(redirect_to: submission.redirect_to) end - wizard.final_cleanup! + submission.remove if submission.present? + wizard.reset end render json: result diff --git a/db/migrate/20210806135416_split_custom_wizard_log_fields.rb b/db/migrate/20210806135416_split_custom_wizard_log_fields.rb new file mode 100644 index 00000000..984a7a23 --- /dev/null +++ b/db/migrate/20210806135416_split_custom_wizard_log_fields.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1] + def change + reversible do |dir| + dir.up do + # separate wizard/action/user into their own keys + + wizard_logs = PluginStoreRow.where(" + plugin_name = 'custom_wizard_log' + ") + + if wizard_logs.exists? + wizard_logs.each do |row| + begin + log_json = JSON.parse(row.value) + rescue TypeError, JSON::ParserError + next + end + + if log_json.key?('message') && log_json['message'].is_a?(String) + + attr_strs = [] + + # assumes no whitespace in the values + attr_strs << log_json['message'].slice!(/(wizard: \S*; )/, 1) + attr_strs << log_json['message'].slice!(/(action: \S*; )/, 1) + attr_strs << log_json['message'].slice!(/(user: \S*; )/, 1) + + attr_strs.each do |attr_str| + if attr_str.is_a? String + attr_str.gsub!(/[;]/ , "") + key, value = attr_str.split(': ') + value.strip! if value + log_json[key] = value ? value : '' + end + end + + row.value = log_json.to_json + row.save + + end + end + end + end + dir.down do + wizard_logs = PluginStoreRow.where(" + plugin_name = 'custom_wizard_log' + ") + + if wizard_logs.exists? + wizard_logs.each do |row| + begin + log_json = JSON.parse(row.value) + rescue TypeError, JSON::ParserError + next + end + + # concatenate wizard/action/user to start of message + prefixes = log_json.extract!('wizard', 'action', 'user') + + message_prefix = prefixes.map { |k, v| "#{k}: #{v}" }.join('; ') + + if log_json.key?('message') + log_json['message'] = "#{message_prefix}; #{log_json['message']}" + else + log_json['message'] = message_prefix + end + + row.value = log_json.to_json + row.save + + end + end + end + end + end +end diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index 74dc4680..aa8b4d6a 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -742,15 +742,12 @@ class CustomWizard::Action end def save_log - log = "wizard: #{@wizard.id}; action: #{action['type']}; user: #{user.username}" - - if @log.any? - @log.each do |item| - log += "; #{item.to_s}" - end - end - - CustomWizard::Log.create(log) + CustomWizard::Log.create( + @wizard.id, + action['type'], + user.username, + @log.join('; ') + ) end def pro_actions diff --git a/lib/custom_wizard/log.rb b/lib/custom_wizard/log.rb index fc747440..c50a5712 100644 --- a/lib/custom_wizard/log.rb +++ b/lib/custom_wizard/log.rb @@ -2,22 +2,28 @@ class CustomWizard::Log include ActiveModel::Serialization - attr_accessor :message, :date + attr_accessor :date, :wizard, :action, :user, :message PAGE_LIMIT = 100 def initialize(attrs) - @message = attrs['message'] @date = attrs['date'] + @wizard = attrs['wizard'] + @action = attrs['action'] + @user = attrs['user'] + @message = attrs['message'] end - def self.create(message) + def self.create(wizard, action, user, message) log_id = SecureRandom.hex(12) PluginStore.set('custom_wizard_log', log_id.to_s, { date: Time.now, + wizard: wizard, + action: action, + user: user, message: message } ) diff --git a/lib/custom_wizard/submission.rb b/lib/custom_wizard/submission.rb index e12299d1..89de50bf 100644 --- a/lib/custom_wizard/submission.rb +++ b/lib/custom_wizard/submission.rb @@ -2,6 +2,7 @@ class CustomWizard::Submission include ActiveModel::SerializerSupport + PAGE_LIMIT = 50 KEY ||= "submissions" META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to) @@ -44,7 +45,7 @@ class CustomWizard::Submission validate submission_list = self.class.list(wizard, user_id: user.id) - submissions = submission_list.select { |submission| submission.id != self.id } + submissions = submission_list.submissions.select { |submission| submission.id != self.id } self.updated_at = Time.now.iso8601 submissions.push(self) @@ -93,14 +94,25 @@ class CustomWizard::Submission end def self.get(wizard, user_id) - data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).first + data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).last new(wizard, data, user_id) end + def remove + if present? + user_id = @user.id + wizard_id = @wizard.id + submission_id = @id + data = PluginStore.get("#{wizard_id}_#{KEY}", user_id) + data.delete_if { |sub| sub["id"] == submission_id } + PluginStore.set("#{wizard_id}_#{KEY}", user_id, data) + end + end + def self.cleanup_incomplete_submissions(wizard) user_id = wizard.user.id all_submissions = list(wizard, user_id: user_id) - sorted_submissions = all_submissions.sort_by do |submission| + sorted_submissions = all_submissions.submissions.sort_by do |submission| zero_epoch_time = DateTime.strptime("0", '%s') [ submission.submitted_at ? Time.iso8601(submission.submitted_at) : zero_epoch_time, @@ -120,23 +132,34 @@ class CustomWizard::Submission PluginStore.set("#{wizard.id}_#{KEY}", user_id, valid_data) end - def self.list(wizard, user_id: nil, order_by: nil) + def self.list(wizard, user_id: nil, order_by: nil, page: 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 = [] + result = OpenStruct.new(submissions: [], total: nil) query.each do |record| if (submission_data = ::JSON.parse(record.value)).any? submission_data.each do |data| - result.push(new(wizard, data, record.key)) + result.submissions.push(new(wizard, data, record.key)) end end end + result.total = result.submissions.size + + if !page.nil? + start = page * PAGE_LIMIT + length = PAGE_LIMIT + + if result.submissions.length > start + result.submissions = result.submissions[start, length] + else + result.submissions = [] + end + end + result end end diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index cd59f361..98e7938b 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -32,7 +32,8 @@ class CustomWizard::Wizard :actions, :action_ids, :user, - :submissions + :submissions, + :template attr_reader :all_step_ids @@ -79,6 +80,7 @@ class CustomWizard::Wizard @actions = attrs['actions'] || [] @action_ids = @actions.map { |a| a['id'] } + @template = attrs end def cast_bool(val) @@ -272,7 +274,7 @@ class CustomWizard::Wizard def submissions return nil unless user.present? - @submissions ||= CustomWizard::Submission.list(self, user_id: user.id) + @submissions ||= CustomWizard::Submission.list(self, user_id: user.id).submissions end def current_submission diff --git a/plugin.rb b/plugin.rb index b2c6d54c..808975d6 100644 --- a/plugin.rb +++ b/plugin.rb @@ -33,6 +33,14 @@ if respond_to?(:register_svg_icon) register_svg_icon "chevron-right" register_svg_icon "chevron-left" register_svg_icon "save" + register_svg_icon "sliders-h" + register_svg_icon "calendar" + register_svg_icon "check" + register_svg_icon "times" + register_svg_icon "clock" + register_svg_icon "link" + register_svg_icon "comment-alt" + register_svg_icon "far-life-ring" register_svg_icon "arrow-right" end diff --git a/serializers/custom_wizard/log_serializer.rb b/serializers/custom_wizard/log_serializer.rb index e521c573..c4683ba8 100644 --- a/serializers/custom_wizard/log_serializer.rb +++ b/serializers/custom_wizard/log_serializer.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true class CustomWizard::LogSerializer < ApplicationSerializer - attributes :message, :date + attributes :date, :wizard, :action, :user, :message end diff --git a/serializers/custom_wizard/submission_serializer.rb b/serializers/custom_wizard/submission_serializer.rb index 52f0cb32..e5e88867 100644 --- a/serializers/custom_wizard/submission_serializer.rb +++ b/serializers/custom_wizard/submission_serializer.rb @@ -1,16 +1,32 @@ # frozen_string_literal: true class CustomWizard::SubmissionSerializer < ApplicationSerializer attributes :id, - :username, :fields, - :submitted_at, - :route_to, - :redirect_on_complete, - :redirect_to + :submitted_at - def username - object.user.present? ? - object.user.username : - I18n.t('admin.wizard.submission.no_user', user_id: object.user_id) + has_one :user, serializer: ::BasicUserSerializer, embed: :objects + + def include_user? + object.user.present? + end + + def fields + @fields ||= begin + result = {} + + object.wizard.template['steps'].each do |step| + step['fields'].each do |field| + if value = object.fields[field['id']] + result[field['id']] = { + value: value, + type: field['type'], + label: field['label'] + } + end + end + end + + result + end end end diff --git a/spec/components/custom_wizard/log_spec.rb b/spec/components/custom_wizard/log_spec.rb index 30fd0173..62f2e6df 100644 --- a/spec/components/custom_wizard/log_spec.rb +++ b/spec/components/custom_wizard/log_spec.rb @@ -3,9 +3,9 @@ require_relative '../../plugin_helper' describe CustomWizard::Log do before do - CustomWizard::Log.create("First log message") - CustomWizard::Log.create("Second log message") - CustomWizard::Log.create("Third log message") + CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message') + CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message') + CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message') end it "creates logs" do diff --git a/spec/components/custom_wizard/submission_spec.rb b/spec/components/custom_wizard/submission_spec.rb index a838820d..a6c8a0d3 100644 --- a/spec/components/custom_wizard/submission_spec.rb +++ b/spec/components/custom_wizard/submission_spec.rb @@ -8,32 +8,44 @@ describe CustomWizard::Submission do 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 + described_class.new(@wizard, step_1_field_1: "I am user submission").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") + ).to eq("I am user submission") end - it "list submissions by wizard" do - expect(described_class.list(@wizard).size).to eq(2) - end + context "#list" do + before do + template_json_2 = template_json.dup + template_json_2["id"] = "super_mega_fun_wizard_2" + CustomWizard::Template.save(template_json_2, skip_jobs: true) - it "list submissions by wizard and user" do - expect(described_class.list(@wizard, user_id: user.id).size).to eq(1) + @wizard2 = CustomWizard::Wizard.create(template_json["id"], user2) + @wizard3 = CustomWizard::Wizard.create(template_json_2["id"], user) + @count = CustomWizard::Submission::PAGE_LIMIT + 20 + + @count.times do |index| + described_class.new(@wizard, step_1_field_1: "I am user submission #{index + 1}").save + end + 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 "list submissions by wizard" do + expect(described_class.list(@wizard).total).to eq(@count + 2) + end + + it "list submissions by wizard and user" do + expect(described_class.list(@wizard, user_id: user.id).total).to eq(@count + 1) + end + + it "paginates submission lists" do + expect(described_class.list(@wizard, page: 1).submissions.size).to eq((@count + 2) - CustomWizard::Submission::PAGE_LIMIT) + end end context "#cleanup_incomplete_submissions" do @@ -42,10 +54,10 @@ describe CustomWizard::Submission do described_class.new(@wizard, step_1_field_1: "I am the second submission").save builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - sub_list = described_class.list(@wizard, user_id: @wizard.user.id) + submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions - expect(sub_list.length).to eq(1) - expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the second submission") + expect(submissions.length).to eq(1) + expect(submissions.first.fields["step_1_field_1"]).to eq("I am the second submission") end it "handles submissions without 'updated_at' field correctly" do @@ -58,10 +70,10 @@ describe CustomWizard::Submission do PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, sub_data) builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - sub_list = described_class.list(@wizard, user_id: @wizard.user.id) + submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions - expect(sub_list.length).to eq(1) - expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission") + expect(submissions.length).to eq(1) + expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission") end it "handles submissions with and without 'updated_at' field correctly" do @@ -75,10 +87,10 @@ describe CustomWizard::Submission do builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - sub_list = described_class.list(@wizard, user_id: @wizard.user.id) + submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions - expect(sub_list.length).to eq(1) - expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission") + expect(submissions.length).to eq(1) + expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission") end end end diff --git a/spec/requests/custom_wizard/admin/logs_controller_spec.rb b/spec/requests/custom_wizard/admin/logs_controller_spec.rb index 28b7d785..5aaf9578 100644 --- a/spec/requests/custom_wizard/admin/logs_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/logs_controller_spec.rb @@ -5,9 +5,9 @@ describe CustomWizard::AdminLogsController do fab!(:admin_user) { Fabricate(:user, admin: true) } before do - CustomWizard::Log.create("First log message") - CustomWizard::Log.create("Second log message") - CustomWizard::Log.create("Third log message") + CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message') + CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message') + CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message') sign_in(admin_user) end diff --git a/spec/requests/custom_wizard/wizard_controller_spec.rb b/spec/requests/custom_wizard/wizard_controller_spec.rb index c28f8783..f6211b55 100644 --- a/spec/requests/custom_wizard/wizard_controller_spec.rb +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -33,30 +33,55 @@ describe CustomWizard::WizardController do expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.") end - it 'skips a wizard if user is allowed to skip' do - put '/w/super-mega-fun-wizard/skip.json' - expect(response.status).to eq(200) - end + context 'when user skips the wizard' do - it 'lets user skip if user cant access wizard' do - @template["permitted"] = permitted_json["permitted"] - CustomWizard::Template.save(@template, skip_jobs: true) + it 'skips a wizard if user is allowed to skip' do + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + end - put '/w/super-mega-fun-wizard/skip.json' - 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) - it 'returns a no skip message if user is not allowed to skip' do - @template['required'] = 'true' - CustomWizard::Template.save(@template) - put '/w/super-mega-fun-wizard/skip.json' - expect(response.parsed_body['error']).to eq("Wizard can't be skipped") - end + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + end - it 'skip response contains a redirect_to if in users submissions' do - @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') + it 'returns a no skip message if user is not allowed to skip' do + @template['required'] = 'true' + CustomWizard::Template.save(@template) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.parsed_body['error']).to eq("Wizard can't be skipped") + end + + it 'skip response contains a redirect_to if in users submissions' do + @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 + + it "deletes the submission if user has filled up some data" do + @wizard = CustomWizard::Wizard.create(@template["id"], user) + CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save + current_submission = @wizard.current_submission + put '/w/super-mega-fun-wizard/skip.json' + submissions = CustomWizard::Submission.list(@wizard).submissions + + expect(submissions.any? { |submission| submission.id == current_submission.id }).to eq(false) + end + + it "starts from the first step if user visits after skipping the wizard" do + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Text input" + } + } + put '/w/super-mega-fun-wizard/skip.json' + get '/w/super-mega-fun-wizard.json' + + expect(response.parsed_body["start"]).to eq('step_1') + end end end diff --git a/spec/serializers/custom_wizard/log_serializer_spec.rb b/spec/serializers/custom_wizard/log_serializer_spec.rb index bde16199..b452b9c5 100644 --- a/spec/serializers/custom_wizard/log_serializer_spec.rb +++ b/spec/serializers/custom_wizard/log_serializer_spec.rb @@ -6,14 +6,17 @@ describe CustomWizard::LogSerializer do fab!(:user) { Fabricate(:user) } it 'should return log attributes' do - CustomWizard::Log.create("First log message") - CustomWizard::Log.create("Second log message") + CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message') + CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message') json_array = ActiveModel::ArraySerializer.new( CustomWizard::Log.list(0), each_serializer: CustomWizard::LogSerializer ).as_json expect(json_array.length).to eq(2) + expect(json_array[0][:wizard]).to eq("second-test-wizard") + expect(json_array[0][:action]).to eq("perform_second_action") + expect(json_array[0][:user]).to eq("second_test_user") expect(json_array[0][:message]).to eq("Second log message") end end diff --git a/spec/serializers/custom_wizard/submission_serializer_spec.rb b/spec/serializers/custom_wizard/submission_serializer_spec.rb new file mode 100644 index 00000000..02d8be8a --- /dev/null +++ b/spec/serializers/custom_wizard/submission_serializer_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative '../../plugin_helper' + +describe CustomWizard::SubmissionSerializer do + fab!(:user) { 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) + wizard = CustomWizard::Wizard.create(template_json["id"], user) + CustomWizard::Submission.new(wizard, + step_1_field_1: "I am user submission", + submitted_at: Time.now.iso8601 + ).save + @list = CustomWizard::Submission.list(wizard, page: 0) + end + + it 'should return submission attributes' do + json_array = ActiveModel::ArraySerializer.new( + @list.submissions, + each_serializer: described_class + ).as_json + + expect(json_array.length).to eq(1) + expect(json_array[0][:id].present?).to eq(true) + expect(json_array[0][:user].present?).to eq(true) + expect(json_array[0][:submitted_at].present?).to eq(true) + end + + it "should return field values, types and labels" do + json_array = ActiveModel::ArraySerializer.new( + @list.submissions, + each_serializer: described_class + ).as_json + + expect(json_array.length).to eq(1) + expect(json_array[0][:fields].as_json).to eq({ + "step_1_field_1": { + "value": "I am user submission", + "type": "text", + "label": "Text" + } + }.as_json) + end +end