Merge branch 'pro-release' into pro-features
Dieser Commit ist enthalten in:
Commit
f4d419cac2
33 geänderte Dateien mit 965 neuen und 153 gelöschten Zeilen
112
assets/javascripts/discourse/components/submission-field.js.es6
Normale Datei
112
assets/javascripts/discourse/components/submission-field.js.es6
Normale Datei
|
@ -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}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -9,6 +9,8 @@ export default Controller.extend({
|
||||||
page: 0,
|
page: 0,
|
||||||
canLoadMore: true,
|
canLoadMore: true,
|
||||||
logs: [],
|
logs: [],
|
||||||
|
documentationUrl: "https://thepavilion.io/t/2818",
|
||||||
|
messageKey: "viewing",
|
||||||
|
|
||||||
loadLogs() {
|
loadLogs() {
|
||||||
if (!this.canLoadMore) {
|
if (!this.canLoadMore) {
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,6 +1,65 @@
|
||||||
import Controller from "@ember/controller";
|
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 { fmt } from "discourse/lib/computed";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import CustomWizard from "../models/custom-wizard";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
downloadUrl: fmt("wizard.id", "/admin/wizards/submissions/%@/download"),
|
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"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,10 +1,10 @@
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { buildProperties, mapped, present } from "../lib/wizard-json";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { listProperties, snakeCase } from "../lib/wizard";
|
|
||||||
import wizardSchema from "../lib/wizard-schema";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
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({
|
const CustomWizard = EmberObject.extend({
|
||||||
save(opts) {
|
save(opts) {
|
||||||
|
@ -211,10 +211,55 @@ CustomWizard.reopenClass({
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
submissions(wizardId) {
|
submissions(wizardId, page = null) {
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
data.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
return ajax(`/admin/wizards/submissions/${wizardId}`, {
|
return ajax(`/admin/wizards/submissions/${wizardId}`, {
|
||||||
type: "GET",
|
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 = {}) {
|
create(wizardJson = {}) {
|
||||||
|
|
|
@ -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";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import CustomWizard from "../models/custom-wizard";
|
||||||
const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"];
|
|
||||||
|
|
||||||
export default DiscourseRoute.extend({
|
export default DiscourseRoute.extend({
|
||||||
model(params) {
|
model(params) {
|
||||||
|
@ -9,34 +9,16 @@ export default DiscourseRoute.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
if (model && model.submissions) {
|
const fields = model.fields.map((f) => {
|
||||||
let fields = ["username"];
|
const fieldsObject = EmberObject.create(f);
|
||||||
model.submissions.forEach((s) => {
|
fieldsObject.enabled = true;
|
||||||
Object.keys(s.fields).forEach((k) => {
|
return fieldsObject;
|
||||||
if (!excludedMetaFields.includes(k) && fields.indexOf(k) < 0) {
|
});
|
||||||
fields.push(k);
|
controller.setProperties({
|
||||||
}
|
wizard: model.wizard,
|
||||||
});
|
fields: A(fields),
|
||||||
});
|
submissions: A(model.submissions),
|
||||||
|
total: model.total,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
class="refresh"}}
|
class="refresh"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{wizard-message
|
||||||
|
key=messageKey
|
||||||
|
opts=messageOpts
|
||||||
|
url=documentationUrl
|
||||||
|
component="logs"}}
|
||||||
|
|
||||||
{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}}
|
{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}}
|
||||||
{{#if noResults}}
|
{{#if noResults}}
|
||||||
<p>{{i18n "search.no_results"}}</p>
|
<p>{{i18n "search.no_results"}}</p>
|
||||||
|
|
|
@ -1,8 +1,24 @@
|
||||||
{{#if submissions}}
|
{{#if submissions}}
|
||||||
<div class="wizard-header large">
|
<div class="wizard-header large">
|
||||||
<label>{{i18n "admin.wizard.submissions.title" name=wizard.name}}</label>
|
<label>
|
||||||
|
{{i18n "admin.wizard.submissions.title" name=wizard.name}}
|
||||||
|
</label>
|
||||||
|
|
||||||
<a class="btn btn-default download-link" href={{downloadUrl}} target="_blank" rel="noopener noreferrer">
|
<div class="controls">
|
||||||
|
{{d-button
|
||||||
|
icon="sliders-h"
|
||||||
|
label="admin.wizard.submissions.edit_columns"
|
||||||
|
action=(action "showEditColumnsModal")
|
||||||
|
class="btn-default open-edit-columns-btn download-link"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="btn btn-default download-link"
|
||||||
|
href={{downloadUrl}}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{{d-icon "download"}}
|
{{d-icon "download"}}
|
||||||
<span class="d-button-label">
|
<span class="d-button-label">
|
||||||
{{i18n "admin.wizard.submissions.download"}}
|
{{i18n "admin.wizard.submissions.download"}}
|
||||||
|
@ -11,23 +27,39 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wizard-submissions">
|
<div class="wizard-submissions">
|
||||||
<table>
|
{{#load-more selector=".wizard-submissions tr" action=(action "loadMore")}}
|
||||||
<thead>
|
{{#if noResults}}
|
||||||
<tr>
|
<p>
|
||||||
{{#each fields as |f|}}
|
{{i18n "search.no_results"}}
|
||||||
<th>{{f}}</th>
|
</p>
|
||||||
{{/each}}
|
{{else}}
|
||||||
</tr>
|
<table>
|
||||||
</thead>
|
<thead>
|
||||||
<tbody>
|
<tr>
|
||||||
{{#each submissions as |s|}}
|
{{#each fields as |field|}}
|
||||||
<tr>
|
{{#if field.enabled}}
|
||||||
{{#each-in s as |k v|}}
|
<th>
|
||||||
<td>{{v}}</td>
|
{{field.label}}
|
||||||
{{/each-in}}
|
</th>
|
||||||
</tr>
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each displaySubmissions as |submission|}}
|
||||||
|
<tr>
|
||||||
|
{{#each-in submission as |field value|}}
|
||||||
|
<td>
|
||||||
|
{{submission-field fieldName=field value=value}}
|
||||||
|
</td>
|
||||||
|
{{/each-in}}
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{conditional-loading-spinner condition=loadingMore}}
|
||||||
|
{{/load-more}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="admin-wizard-select">
|
<div class="admin-wizard-select admin-wizard-controls">
|
||||||
{{combo-box
|
{{combo-box
|
||||||
value=wizardId
|
value=wizardId
|
||||||
content=wizardList
|
content=wizardList
|
||||||
|
@ -8,6 +8,12 @@
|
||||||
)}}
|
)}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{wizard-message
|
||||||
|
key=messageKey
|
||||||
|
opts=messageOpts
|
||||||
|
url=documentationUrl
|
||||||
|
component="submissions"}}
|
||||||
|
|
||||||
<div class="admin-wizard-container">
|
<div class="admin-wizard-container">
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,10 @@
|
||||||
{{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}}
|
{{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}}
|
||||||
{{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}}
|
{{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}}
|
||||||
{{nav-item route="adminWizardsPro" label="admin.wizard.pro.nav_label"}}
|
{{nav-item route="adminWizardsPro" label="admin.wizard.pro.nav_label"}}
|
||||||
|
|
||||||
|
<div class="admin-actions">
|
||||||
|
<a target="_blank" class="btn btn-pavilion-pro" rel="noreferrer noopener" href="https://thepavilion.io/w/support" title={{i18n "admin.wizard.pro_support_button.title"}}>{{d-icon "far-life-ring"}}{{i18n "admin.wizard.pro_support_button.label"}}</a>
|
||||||
|
</div>
|
||||||
{{/admin-nav}}
|
{{/admin-nav}}
|
||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
|
|
163
assets/javascripts/discourse/templates/components/submission-field.hbs
Normale Datei
163
assets/javascripts/discourse/templates/components/submission-field.hbs
Normale Datei
|
@ -0,0 +1,163 @@
|
||||||
|
{{#if isText}}
|
||||||
|
{{value.value}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isTextArea}}
|
||||||
|
<div class="submission-long-text">
|
||||||
|
<p class="submission-long-text-content {{textState}}">
|
||||||
|
{{value.value}}
|
||||||
|
</p>
|
||||||
|
<a href {{action "expandText"}}>
|
||||||
|
{{toggleText}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isComposer}}
|
||||||
|
<div class="submission-long-text">
|
||||||
|
<p
|
||||||
|
class="submission-composer-text submission-long-text-content {{
|
||||||
|
textState
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
{{value.value}}
|
||||||
|
</p>
|
||||||
|
<a href {{action "expandText"}}>
|
||||||
|
{{toggleText}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isComposerPreview}}
|
||||||
|
{{d-icon "comment-alt"}}
|
||||||
|
<span class="submission-composer-text">
|
||||||
|
{{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isTextOnly}}
|
||||||
|
{{value.value}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isDate}}
|
||||||
|
<span class="submission-icon-item">
|
||||||
|
{{d-icon "calendar"}}{{value.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isTime}}
|
||||||
|
<span class="submission-icon-item">
|
||||||
|
{{d-icon "clock"}}{{value.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isDateTime}}
|
||||||
|
<span class="submission-icon-item" title={{value.value}}>
|
||||||
|
{{d-icon "calendar"}}{{format-date value.value format="medium"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isNumber}}
|
||||||
|
{{value.value}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isCheckbox}}
|
||||||
|
{{#if checkboxValue}}
|
||||||
|
<span class="submission-icon-item submission-checkbox-true">
|
||||||
|
{{d-icon "check"}}{{value.value}}
|
||||||
|
</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="submission-icon-item submission-checkbox-false">
|
||||||
|
{{d-icon "times"}}{{value.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isUrl}}
|
||||||
|
<span class="submission-icon-item submission-url">
|
||||||
|
{{d-icon "link"}}
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href={{value.value}}>
|
||||||
|
{{value.value}}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isUpload}}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="attachment"
|
||||||
|
href={{file.url}}
|
||||||
|
download
|
||||||
|
>
|
||||||
|
{{file.original_filename}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isDropdown}}
|
||||||
|
<span class="submission-icon-item">
|
||||||
|
{{d-icon "check-square"}}
|
||||||
|
{{value.value}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isTag}}
|
||||||
|
{{#each value.value as |tag|}}
|
||||||
|
{{discourse-tag tag}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isCategory}}
|
||||||
|
<strong>
|
||||||
|
{{i18n "admin.wizard.submissions.category_id"}}:
|
||||||
|
</strong>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={{categoryUrl}}
|
||||||
|
title={{value.value}}
|
||||||
|
>
|
||||||
|
{{value.value}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isGroup}}
|
||||||
|
<strong>
|
||||||
|
{{i18n "admin.wizard.submissions.group_id"}}:
|
||||||
|
</strong>
|
||||||
|
{{value.value}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isUserSelector}}
|
||||||
|
{{#each submittedUsers as |user|}}
|
||||||
|
{{d-icon "user"}}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={{user.url}}
|
||||||
|
title={{user.username}}
|
||||||
|
>
|
||||||
|
{{user.username}}
|
||||||
|
</a>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isUser}}
|
||||||
|
{{#link-to "user" value}}
|
||||||
|
{{avatar value imageSize="tiny"}}
|
||||||
|
{{/link-to}}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={{userProfileUrl}}
|
||||||
|
title={{value.name}}
|
||||||
|
>
|
||||||
|
{{value.username}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isSubmittedAt}}
|
||||||
|
<span class="submission-date" title={{value.value}}>
|
||||||
|
{{d-icon "clock"}}{{format-date value format="tiny"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{#d-modal-body title="directory.edit_columns.title"}}
|
||||||
|
{{#if loading}}
|
||||||
|
{{loading-spinner size="large"}}
|
||||||
|
{{else}}
|
||||||
|
<div class="edit-directory-columns-container">
|
||||||
|
{{#each model.fields as |field|}}
|
||||||
|
<div class="edit-directory-column">
|
||||||
|
<div class="left-content">
|
||||||
|
<label class="column-name">
|
||||||
|
{{input type="checkbox" checked=field.enabled}}
|
||||||
|
{{directory-table-header-title field=field.label translated=true}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button
|
||||||
|
class="btn-primary"
|
||||||
|
label="directory.edit_columns.save"
|
||||||
|
action=(action "save")
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{d-button
|
||||||
|
class="btn-secondary reset-to-default"
|
||||||
|
label="directory.edit_columns.reset_to_default"
|
||||||
|
action=(action "resetToDefault")
|
||||||
|
}}
|
||||||
|
</div>
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
//= require discourse/app/mixins/singleton
|
//= require discourse/app/mixins/singleton
|
||||||
//= require discourse/app/mixins/upload
|
//= require discourse/app/mixins/upload
|
||||||
|
//= require discourse/app/mixins/composer-upload
|
||||||
|
|
||||||
//= require discourse/app/adapters/rest
|
//= require discourse/app/adapters/rest
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
@import "wizard-manager";
|
@import "wizard-manager";
|
||||||
@import "wizard-api";
|
@import "wizard-api";
|
||||||
@import "common/components/buttons";
|
@import "common/components/buttons";
|
||||||
|
@import "wizard-variables";
|
||||||
|
|
||||||
.admin-wizard-controls {
|
.admin-wizard-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -71,6 +72,51 @@
|
||||||
table td {
|
table td {
|
||||||
min-width: 150px;
|
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 {
|
.admin-wizards-logs {
|
||||||
|
@ -203,6 +249,11 @@
|
||||||
&.underline {
|
&.underline {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-wizard-buttons {
|
.admin-wizard-buttons {
|
||||||
|
@ -833,3 +884,22 @@
|
||||||
padding-top: .25em;
|
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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
7
assets/stylesheets/common/wizard-variables.scss
Normale Datei
7
assets/stylesheets/common/wizard-variables.scss
Normale Datei
|
@ -0,0 +1,7 @@
|
||||||
|
$pavilionPrimary: #3c1c8c;
|
||||||
|
$pavilionSecondary: #ffffff;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--pavilion-primary: #3c1c8c;
|
||||||
|
--pavilion-secondary: #ffffff;
|
||||||
|
}
|
|
@ -59,6 +59,10 @@ en:
|
||||||
condition: "Condition"
|
condition: "Condition"
|
||||||
index: "Index"
|
index: "Index"
|
||||||
|
|
||||||
|
pro_support_button:
|
||||||
|
title: "Request Pro Support"
|
||||||
|
label: "Pro Support"
|
||||||
|
|
||||||
message:
|
message:
|
||||||
wizard:
|
wizard:
|
||||||
select: "Select a wizard, or create a new one"
|
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 <a href='https://thepavilion.io/t/3652'>the documentation</a>."
|
subscription_inactive: "Your subscription is inactive on this forum. Read more in <a href='https://thepavilion.io/t/3652'>the documentation</a>."
|
||||||
unauthorized: "You're unauthorized. If you have a subscription, it will become inactive in the next 48 hours."
|
unauthorized: "You're unauthorized. If you have a subscription, it will become inactive in the next 48 hours."
|
||||||
unauthorize_failed: Failed to unauthorize.
|
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:
|
editor:
|
||||||
show: "Show"
|
show: "Show"
|
||||||
|
@ -367,6 +378,12 @@ en:
|
||||||
nav_label: "Submissions"
|
nav_label: "Submissions"
|
||||||
title: "{{name}} Submissions"
|
title: "{{name}} Submissions"
|
||||||
download: "Download"
|
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:
|
api:
|
||||||
label: "API"
|
label: "API"
|
||||||
|
|
|
@ -13,12 +13,16 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController
|
||||||
def show
|
def show
|
||||||
render_json_dump(
|
render_json_dump(
|
||||||
wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false),
|
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
|
end
|
||||||
|
|
||||||
def download
|
def download
|
||||||
send_data ordered_submissions.to_json,
|
send_data submission_list.submissions.to_json,
|
||||||
filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json",
|
filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json",
|
||||||
content_type: "application/json",
|
content_type: "application/json",
|
||||||
disposition: "attachment"
|
disposition: "attachment"
|
||||||
|
@ -26,7 +30,7 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def ordered_submissions
|
def submission_list
|
||||||
CustomWizard::Submission.list(@wizard, order_by: 'id')
|
CustomWizard::Submission.list(@wizard, page: params[:page].to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,7 +69,8 @@ class CustomWizard::WizardController < ::ApplicationController
|
||||||
result.merge!(redirect_to: submission.redirect_to)
|
result.merge!(redirect_to: submission.redirect_to)
|
||||||
end
|
end
|
||||||
|
|
||||||
wizard.final_cleanup!
|
submission.remove if submission.present?
|
||||||
|
wizard.reset
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: result
|
render json: result
|
||||||
|
|
77
db/migrate/20210806135416_split_custom_wizard_log_fields.rb
Normale Datei
77
db/migrate/20210806135416_split_custom_wizard_log_fields.rb
Normale Datei
|
@ -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
|
|
@ -742,15 +742,12 @@ class CustomWizard::Action
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_log
|
def save_log
|
||||||
log = "wizard: #{@wizard.id}; action: #{action['type']}; user: #{user.username}"
|
CustomWizard::Log.create(
|
||||||
|
@wizard.id,
|
||||||
if @log.any?
|
action['type'],
|
||||||
@log.each do |item|
|
user.username,
|
||||||
log += "; #{item.to_s}"
|
@log.join('; ')
|
||||||
end
|
)
|
||||||
end
|
|
||||||
|
|
||||||
CustomWizard::Log.create(log)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def pro_actions
|
def pro_actions
|
||||||
|
|
|
@ -2,22 +2,28 @@
|
||||||
class CustomWizard::Log
|
class CustomWizard::Log
|
||||||
include ActiveModel::Serialization
|
include ActiveModel::Serialization
|
||||||
|
|
||||||
attr_accessor :message, :date
|
attr_accessor :date, :wizard, :action, :user, :message
|
||||||
|
|
||||||
PAGE_LIMIT = 100
|
PAGE_LIMIT = 100
|
||||||
|
|
||||||
def initialize(attrs)
|
def initialize(attrs)
|
||||||
@message = attrs['message']
|
|
||||||
@date = attrs['date']
|
@date = attrs['date']
|
||||||
|
@wizard = attrs['wizard']
|
||||||
|
@action = attrs['action']
|
||||||
|
@user = attrs['user']
|
||||||
|
@message = attrs['message']
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create(message)
|
def self.create(wizard, action, user, message)
|
||||||
log_id = SecureRandom.hex(12)
|
log_id = SecureRandom.hex(12)
|
||||||
|
|
||||||
PluginStore.set('custom_wizard_log',
|
PluginStore.set('custom_wizard_log',
|
||||||
log_id.to_s,
|
log_id.to_s,
|
||||||
{
|
{
|
||||||
date: Time.now,
|
date: Time.now,
|
||||||
|
wizard: wizard,
|
||||||
|
action: action,
|
||||||
|
user: user,
|
||||||
message: message
|
message: message
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
class CustomWizard::Submission
|
class CustomWizard::Submission
|
||||||
include ActiveModel::SerializerSupport
|
include ActiveModel::SerializerSupport
|
||||||
|
|
||||||
|
PAGE_LIMIT = 50
|
||||||
KEY ||= "submissions"
|
KEY ||= "submissions"
|
||||||
META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to)
|
META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to)
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ class CustomWizard::Submission
|
||||||
validate
|
validate
|
||||||
|
|
||||||
submission_list = self.class.list(wizard, user_id: user.id)
|
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
|
self.updated_at = Time.now.iso8601
|
||||||
submissions.push(self)
|
submissions.push(self)
|
||||||
|
|
||||||
|
@ -93,14 +94,25 @@ class CustomWizard::Submission
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.get(wizard, user_id)
|
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)
|
new(wizard, data, user_id)
|
||||||
end
|
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)
|
def self.cleanup_incomplete_submissions(wizard)
|
||||||
user_id = wizard.user.id
|
user_id = wizard.user.id
|
||||||
all_submissions = list(wizard, user_id: 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')
|
zero_epoch_time = DateTime.strptime("0", '%s')
|
||||||
[
|
[
|
||||||
submission.submitted_at ? Time.iso8601(submission.submitted_at) : zero_epoch_time,
|
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)
|
PluginStore.set("#{wizard.id}_#{KEY}", user_id, valid_data)
|
||||||
end
|
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 = { plugin_name: "#{wizard.id}_#{KEY}" }
|
||||||
params[:key] = user_id if user_id.present?
|
params[:key] = user_id if user_id.present?
|
||||||
|
|
||||||
query = PluginStoreRow.where(params)
|
query = PluginStoreRow.where(params)
|
||||||
query = query.order("#{order_by} DESC") if order_by.present?
|
result = OpenStruct.new(submissions: [], total: nil)
|
||||||
|
|
||||||
result = []
|
|
||||||
|
|
||||||
query.each do |record|
|
query.each do |record|
|
||||||
if (submission_data = ::JSON.parse(record.value)).any?
|
if (submission_data = ::JSON.parse(record.value)).any?
|
||||||
submission_data.each do |data|
|
submission_data.each do |data|
|
||||||
result.push(new(wizard, data, record.key))
|
result.submissions.push(new(wizard, data, record.key))
|
||||||
end
|
end
|
||||||
end
|
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
|
result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,8 @@ class CustomWizard::Wizard
|
||||||
:actions,
|
:actions,
|
||||||
:action_ids,
|
:action_ids,
|
||||||
:user,
|
:user,
|
||||||
:submissions
|
:submissions,
|
||||||
|
:template
|
||||||
|
|
||||||
attr_reader :all_step_ids
|
attr_reader :all_step_ids
|
||||||
|
|
||||||
|
@ -79,6 +80,7 @@ class CustomWizard::Wizard
|
||||||
|
|
||||||
@actions = attrs['actions'] || []
|
@actions = attrs['actions'] || []
|
||||||
@action_ids = @actions.map { |a| a['id'] }
|
@action_ids = @actions.map { |a| a['id'] }
|
||||||
|
@template = attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
def cast_bool(val)
|
def cast_bool(val)
|
||||||
|
@ -272,7 +274,7 @@ class CustomWizard::Wizard
|
||||||
|
|
||||||
def submissions
|
def submissions
|
||||||
return nil unless user.present?
|
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
|
end
|
||||||
|
|
||||||
def current_submission
|
def current_submission
|
||||||
|
|
|
@ -33,6 +33,14 @@ if respond_to?(:register_svg_icon)
|
||||||
register_svg_icon "chevron-right"
|
register_svg_icon "chevron-right"
|
||||||
register_svg_icon "chevron-left"
|
register_svg_icon "chevron-left"
|
||||||
register_svg_icon "save"
|
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"
|
register_svg_icon "arrow-right"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class CustomWizard::LogSerializer < ApplicationSerializer
|
class CustomWizard::LogSerializer < ApplicationSerializer
|
||||||
attributes :message, :date
|
attributes :date, :wizard, :action, :user, :message
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,32 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class CustomWizard::SubmissionSerializer < ApplicationSerializer
|
class CustomWizard::SubmissionSerializer < ApplicationSerializer
|
||||||
attributes :id,
|
attributes :id,
|
||||||
:username,
|
|
||||||
:fields,
|
:fields,
|
||||||
:submitted_at,
|
:submitted_at
|
||||||
:route_to,
|
|
||||||
:redirect_on_complete,
|
|
||||||
:redirect_to
|
|
||||||
|
|
||||||
def username
|
has_one :user, serializer: ::BasicUserSerializer, embed: :objects
|
||||||
object.user.present? ?
|
|
||||||
object.user.username :
|
def include_user?
|
||||||
I18n.t('admin.wizard.submission.no_user', user_id: object.user_id)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,9 +3,9 @@ require_relative '../../plugin_helper'
|
||||||
|
|
||||||
describe CustomWizard::Log do
|
describe CustomWizard::Log do
|
||||||
before do
|
before do
|
||||||
CustomWizard::Log.create("First log message")
|
CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
|
||||||
CustomWizard::Log.create("Second log message")
|
CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
|
||||||
CustomWizard::Log.create("Third log message")
|
CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates logs" do
|
it "creates logs" do
|
||||||
|
|
|
@ -8,32 +8,44 @@ describe CustomWizard::Submission do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
CustomWizard::Template.save(template_json, skip_jobs: true)
|
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)
|
@wizard = CustomWizard::Wizard.create(template_json["id"], user)
|
||||||
@wizard2 = CustomWizard::Wizard.create(template_json["id"], user2)
|
described_class.new(@wizard, step_1_field_1: "I am user submission").save
|
||||||
@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
|
end
|
||||||
|
|
||||||
it "saves a user's submission" do
|
it "saves a user's submission" do
|
||||||
expect(
|
expect(
|
||||||
described_class.get(@wizard, user.id).fields["step_1_field_1"]
|
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
|
end
|
||||||
|
|
||||||
it "list submissions by wizard" do
|
context "#list" do
|
||||||
expect(described_class.list(@wizard).size).to eq(2)
|
before do
|
||||||
end
|
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
|
@wizard2 = CustomWizard::Wizard.create(template_json["id"], user2)
|
||||||
expect(described_class.list(@wizard, user_id: user.id).size).to eq(1)
|
@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
|
end
|
||||||
|
|
||||||
context "#cleanup_incomplete_submissions" do
|
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
|
described_class.new(@wizard, step_1_field_1: "I am the second submission").save
|
||||||
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
|
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
|
||||||
builder.build
|
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(submissions.length).to eq(1)
|
||||||
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the second submission")
|
expect(submissions.first.fields["step_1_field_1"]).to eq("I am the second submission")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "handles submissions without 'updated_at' field correctly" do
|
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)
|
PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, sub_data)
|
||||||
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
|
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
|
||||||
builder.build
|
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(submissions.length).to eq(1)
|
||||||
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission")
|
expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "handles submissions with and without 'updated_at' field correctly" do
|
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 = CustomWizard::Builder.new(@wizard.id, @wizard.user)
|
||||||
builder.build
|
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(submissions.length).to eq(1)
|
||||||
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission")
|
expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,9 +5,9 @@ describe CustomWizard::AdminLogsController do
|
||||||
fab!(:admin_user) { Fabricate(:user, admin: true) }
|
fab!(:admin_user) { Fabricate(:user, admin: true) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
CustomWizard::Log.create("First log message")
|
CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
|
||||||
CustomWizard::Log.create("Second log message")
|
CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
|
||||||
CustomWizard::Log.create("Third log message")
|
CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message')
|
||||||
sign_in(admin_user)
|
sign_in(admin_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -33,30 +33,55 @@ describe CustomWizard::WizardController do
|
||||||
expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.")
|
expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'skips a wizard if user is allowed to skip' do
|
context 'when user skips the wizard' do
|
||||||
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
|
it 'skips a wizard if user is allowed to skip' do
|
||||||
@template["permitted"] = permitted_json["permitted"]
|
put '/w/super-mega-fun-wizard/skip.json'
|
||||||
CustomWizard::Template.save(@template, skip_jobs: true)
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
put '/w/super-mega-fun-wizard/skip.json'
|
it 'lets user skip if user cant access wizard' do
|
||||||
expect(response.status).to eq(200)
|
@template["permitted"] = permitted_json["permitted"]
|
||||||
end
|
CustomWizard::Template.save(@template, skip_jobs: true)
|
||||||
|
|
||||||
it 'returns a no skip message if user is not allowed to skip' do
|
put '/w/super-mega-fun-wizard/skip.json'
|
||||||
@template['required'] = 'true'
|
expect(response.status).to eq(200)
|
||||||
CustomWizard::Template.save(@template)
|
end
|
||||||
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
|
it 'returns a no skip message if user is not allowed to skip' do
|
||||||
@wizard = CustomWizard::Wizard.create(@template["id"], user)
|
@template['required'] = 'true'
|
||||||
CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save
|
CustomWizard::Template.save(@template)
|
||||||
put '/w/super-mega-fun-wizard/skip.json'
|
put '/w/super-mega-fun-wizard/skip.json'
|
||||||
expect(response.parsed_body['redirect_to']).to eq('/t/2')
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,14 +6,17 @@ describe CustomWizard::LogSerializer do
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
it 'should return log attributes' do
|
it 'should return log attributes' do
|
||||||
CustomWizard::Log.create("First log message")
|
CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
|
||||||
CustomWizard::Log.create("Second log message")
|
CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
|
||||||
|
|
||||||
json_array = ActiveModel::ArraySerializer.new(
|
json_array = ActiveModel::ArraySerializer.new(
|
||||||
CustomWizard::Log.list(0),
|
CustomWizard::Log.list(0),
|
||||||
each_serializer: CustomWizard::LogSerializer
|
each_serializer: CustomWizard::LogSerializer
|
||||||
).as_json
|
).as_json
|
||||||
expect(json_array.length).to eq(2)
|
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")
|
expect(json_array[0][:message]).to eq("Second log message")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
51
spec/serializers/custom_wizard/submission_serializer_spec.rb
Normale Datei
51
spec/serializers/custom_wizard/submission_serializer_spec.rb
Normale Datei
|
@ -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
|
Laden …
In neuem Issue referenzieren