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 6352b3b2..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,8 @@ import Controller from "@ember/controller"; -import { fmt } from "discourse/lib/computed"; 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({ @@ -25,6 +27,24 @@ export default Controller.extend({ }); }, + @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) { @@ -32,5 +52,14 @@ export default Controller.extend({ 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 5e94e31e..d0b49079 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) { @@ -224,31 +224,32 @@ CustomWizard.reopenClass({ }) .then((result) => { if (result.wizard) { - let fields = ["username"]; + let fields = [{ id: "username", label: "User" }]; let submissions = []; let wizard = result.wizard; let total = result.total; result.submissions.forEach((s) => { let submission = { - username: s.username, + username: s.user, }; - Object.keys(s.fields).forEach((f) => { - if (fields.indexOf(f) < 0) { - fields.push(f); - } - - if (fields.includes(f)) { - submission[f] = s.fields[f]; + 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); }); - fields.push("submitted_at"); + let submittedAt = { + id: "submitted_at", + label: "Submitted At", + }; + + fields.push(submittedAt); return { wizard, 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 2ff9fbf9..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,6 +1,7 @@ -import CustomWizard from "../models/custom-wizard"; -import DiscourseRoute from "discourse/routes/discourse"; import { A } from "@ember/array"; +import EmberObject from "@ember/object"; +import DiscourseRoute from "discourse/routes/discourse"; +import CustomWizard from "../models/custom-wizard"; export default DiscourseRoute.extend({ model(params) { @@ -8,9 +9,14 @@ export default DiscourseRoute.extend({ }, setupController(controller, model) { + const fields = model.fields.map((f) => { + const fieldsObject = EmberObject.create(f); + fieldsObject.enabled = true; + return fieldsObject; + }); controller.setProperties({ wizard: model.wizard, - fields: model.fields, + 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 9e8e10c8..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"}} @@ -13,21 +29,29 @@
{{#load-more selector=".wizard-submissions tr" action=(action "loadMore")}} {{#if noResults}} -

{{i18n "search.no_results"}}

+

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

{{else}} - {{#each fields as |f|}} - + {{#each fields as |field|}} + {{#if field.enabled}} + + {{/if}} {{/each}} - {{#each submissions as |s|}} + {{#each displaySubmissions as |submission|}} - {{#each-in s as |k v|}} - + {{#each-in submission as |field value|}} + {{/each-in}} {{/each}} @@ -38,4 +62,4 @@ {{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/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/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index 3c4f9ccf..02fab71b 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -72,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 { @@ -204,6 +249,11 @@ &.underline { text-decoration: underline; } + + .controls { + margin-left: auto; + margin-right: 0.5rem; + } } .admin-wizard-buttons { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 16cbe883..aef049fb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -94,6 +94,14 @@ en: destroying: Destroying wizards... import_complete: Import complete destroy_complete: Destruction complete + 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" @@ -362,6 +370,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/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index e8427334..e52feec4 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) diff --git a/plugin.rb b/plugin.rb index e5402542..58811291 100644 --- a/plugin.rb +++ b/plugin.rb @@ -33,6 +33,13 @@ 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/submission_serializer.rb b/serializers/custom_wizard/submission_serializer.rb index 992deacb..e5e88867 100644 --- a/serializers/custom_wizard/submission_serializer.rb +++ b/serializers/custom_wizard/submission_serializer.rb @@ -1,13 +1,32 @@ # frozen_string_literal: true class CustomWizard::SubmissionSerializer < ApplicationSerializer attributes :id, - :username, :fields, :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/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
{{f}} + {{field.label}} +
{{v}} + {{submission-field fieldName=field value=value}} +