0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-25 18:50:27 +01:00

Merge pull request #204 from paviliondev/pro-release

Custom Wizard 2.0
Dieser Commit ist enthalten in:
Angus McLeod 2022-09-23 17:47:48 +02:00 committet von GitHub
Commit e38b54351f
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
166 geänderte Dateien mit 3791 neuen und 1938 gelöschten Zeilen

Datei anzeigen

@ -8,7 +8,7 @@ on:
pull_request: pull_request:
concurrency: concurrency:
group: plugin-tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }} group: tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:

Datei anzeigen

@ -1,2 +1,8 @@
inherit_gem: inherit_gem:
rubocop-discourse: default.yml rubocop-discourse: default.yml
RSpec/ContextWording:
Enabled: false
RSpec/DescribeClass:
Enabled: false

Datei anzeigen

@ -3,6 +3,13 @@ class CustomWizard::AdminController < ::Admin::AdminController
before_action :ensure_admin before_action :ensure_admin
def index def index
subcription = CustomWizard::Subscription.new
render_json_dump(
subscribed: subcription.subscribed?,
subscription_type: subcription.type,
subscription_attributes: CustomWizard::Subscription.attributes,
subscription_client_installed: subcription.client_installed?
)
end end
private private

Datei anzeigen

@ -1,7 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController
def index def index
render_json_dump(custom_field_list) render_json_dump(
custom_fields: custom_field_list
)
end end
def update def update

Datei anzeigen

@ -1,9 +1,44 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::AdminLogsController < CustomWizard::AdminController class CustomWizard::AdminLogsController < CustomWizard::AdminController
before_action :find_wizard, except: [:index]
def index def index
render_serialized( render json: ActiveModel::ArraySerializer.new(
CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i), CustomWizard::Wizard.list(current_user),
CustomWizard::LogSerializer each_serializer: CustomWizard::BasicWizardSerializer
) )
end end
def show
render_json_dump(
wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false),
logs: ActiveModel::ArraySerializer.new(
log_list.logs,
each_serializer: CustomWizard::LogSerializer
),
total: log_list.total
)
end
protected
def log_list
@log_list ||= begin
list = CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i, params[:wizard_id])
if list.logs.any? && (usernames = list.logs.map(&:username)).present?
user_map = User.where(username: usernames)
.reduce({}) do |result, user|
result[user.username] = user
result
end
list.logs.each do |log_item|
log_item.user = user_map[log_item.username]
end
end
list
end
end
end end

Datei anzeigen

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

Datei anzeigen

@ -1,4 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::LogSerializer < ApplicationSerializer class CustomWizard::LogSerializer < ApplicationSerializer
attributes :message, :date attributes :date,
:action,
:username,
:message
has_one :user, serializer: ::BasicUserSerializer, embed: :objects
end end

Datei anzeigen

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

Datei anzeigen

@ -42,13 +42,8 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer
object.value object.value
end end
def i18n_key
@i18n_key ||= "wizard.step.#{object.step.id}.fields.#{object.id}".underscore
end
def label def label
return object.label if object.label.present? I18n.t("#{i18n_key}.label", default: object.label, base_url: Discourse.base_url)
I18n.t("#{object.key || i18n_key}.label", default: '')
end end
def include_label? def include_label?
@ -56,14 +51,21 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer
end end
def description def description
return object.description if object.description.present? I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url)
I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)
end end
def include_description? def include_description?
description.present? description.present?
end end
def placeholder
I18n.t("#{i18n_key}.placeholder", default: object.placeholder)
end
def include_placeholder?
placeholder.present?
end
def image def image
object.image object.image
end end
@ -72,15 +74,6 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer
object.image.present? object.image.present?
end end
def placeholder
return object.placeholder if object.placeholder.present?
I18n.t("#{object.key || i18n_key}.placeholder", default: '')
end
def include_placeholder?
placeholder.present?
end
def file_types def file_types
object.file_types object.file_types
end end
@ -127,4 +120,14 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer
def preview_template def preview_template
object.preview_template object.preview_template
end end
protected
def i18n_key
@i18n_key ||= "#{object.step.wizard.id}.#{object.step.id}.#{object.id}".underscore
end
def subscribed?
@subscribed ||= CustomWizard::Subscription.subscribed?
end
end end

Datei anzeigen

@ -39,13 +39,8 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer
object.previous.present? object.previous.present?
end end
def i18n_key
@i18n_key ||= "wizard.step.#{object.id}".underscore
end
def title def title
return PrettyText.cook(object.title) if object.title I18n.t("#{i18n_key}.title", default: object.title, base_url: Discourse.base_url)
PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: ''))
end end
def include_title? def include_title?
@ -53,8 +48,7 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer
end end
def description def description
return object.description if object.description I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url)
PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url))
end end
def include_description? def include_description?
@ -80,4 +74,10 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer
def final def final
object.final? object.final?
end end
protected
def i18n_key
@i18n_key ||= "#{object.wizard.id}.#{object.id}".underscore
end
end end

Datei anzeigen

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Custom Wizard QUnit Test Runner</title>
<%= discourse_stylesheet_link_tag(:test_helper, theme_id: nil) %>
<%= discourse_stylesheet_link_tag :wizard, theme_id: nil %>
<%= discourse_stylesheet_link_tag :wizard_custom %>
<%= preload_script "locales/en" %>
<%= preload_script "ember_jquery" %>
<%= preload_script "wizard-vendor" %>
<%= preload_script "wizard-custom" %>
<%= preload_script "wizard-raw-templates" %>
<%= preload_script "wizard-plugin" %>
<%= preload_script "pretty-text-bundle" %>
<%= preload_script "wizard-qunit" %>
<%= csrf_meta_tags %>
<script src="<%= ExtraLocalesController.url("wizard") %>"></script>
<%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
<meta name="discourse_theme_id" content="">
<meta name="discourse-base-uri" content="<%= Discourse.base_path %>">
</head>
<body class="custom-wizard">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>

Datei anzeigen

@ -3,27 +3,12 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { alias, equal, or } from "@ember/object/computed"; import { alias, equal, or } from "@ember/object/computed";
import I18n from "I18n"; import I18n from "I18n";
const generateContent = function (array, type) {
return array.map((key) => ({
id: key,
name: I18n.t(`admin.wizard.custom_field.${type}.${key}`),
}));
};
export default Component.extend({ export default Component.extend({
tagName: "tr", tagName: "tr",
topicSerializers: ["topic_view", "topic_list_item"], topicSerializers: ["topic_view", "topic_list_item"],
postSerializers: ["post"], postSerializers: ["post"],
groupSerializers: ["basic_group"], groupSerializers: ["basic_group"],
categorySerializers: ["basic_category"], categorySerializers: ["basic_category"],
klassContent: generateContent(
["topic", "post", "group", "category"],
"klass"
),
typeContent: generateContent(
["string", "boolean", "integer", "json"],
"type"
),
showInputs: or("field.new", "field.edit"), showInputs: or("field.new", "field.edit"),
classNames: ["custom-field-input"], classNames: ["custom-field-input"],
loading: or("saving", "destroying"), loading: or("saving", "destroying"),
@ -40,9 +25,13 @@ export default Component.extend({
const serializers = this.get(`${klass}Serializers`); const serializers = this.get(`${klass}Serializers`);
if (serializers) { if (serializers) {
return generateContent(serializers, "serializers"); return serializers.reduce((result, key) => {
} else { result.push({
return []; id: key,
name: I18n.t(`admin.wizard.custom_field.serializers.${key}`),
});
return result;
}, []);
} }
}, },

Datei anzeigen

@ -1,21 +0,0 @@
import { default as discourseComputed } from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
classNames: "wizard-advanced-toggle",
@discourseComputed("showAdvanced")
toggleClass(showAdvanced) {
let classes = "btn";
if (showAdvanced) {
classes += " btn-primary";
}
return classes;
},
actions: {
toggleAdvanced() {
this.toggleProperty("showAdvanced");
},
},
});

Datei anzeigen

@ -1,8 +1,8 @@
import { default as discourseComputed } from "discourse-common/utils/decorators"; import { default as discourseComputed } from "discourse-common/utils/decorators";
import { and, empty, equal, or } from "@ember/object/computed"; import { subscriptionSelectKitContent } from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-subscription";
import { empty, equal, or } from "@ember/object/computed";
import { notificationLevels, selectKitContent } from "../lib/wizard"; import { notificationLevels, selectKitContent } from "../lib/wizard";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import wizardSchema from "../lib/wizard-schema";
import UndoChanges from "../mixins/undo-changes"; import UndoChanges from "../mixins/undo-changes";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
@ -25,8 +25,6 @@ export default Component.extend(UndoChanges, {
createGroup: equal("action.type", "create_group"), createGroup: equal("action.type", "create_group"),
apiEmpty: empty("action.api"), apiEmpty: empty("action.api"),
groupPropertyTypes: selectKitContent(["id", "name"]), groupPropertyTypes: selectKitContent(["id", "name"]),
hasAdvanced: or("hasCustomFields", "routeTo"),
showAdvanced: and("hasAdvanced", "action.type"),
hasCustomFields: or( hasCustomFields: or(
"basicTopicFields", "basicTopicFields",
"updateProfile", "updateProfile",
@ -36,12 +34,6 @@ export default Component.extend(UndoChanges, {
basicTopicFields: or("createTopic", "sendMessage", "openComposer"), basicTopicFields: or("createTopic", "sendMessage", "openComposer"),
publicTopicFields: or("createTopic", "openComposer"), publicTopicFields: or("createTopic", "openComposer"),
showPostAdvanced: or("createTopic", "sendMessage"), showPostAdvanced: or("createTopic", "sendMessage"),
actionTypes: Object.keys(wizardSchema.action.types).map((type) => {
return {
id: type,
name: I18n.t(`admin.wizard.action.${type}.label`),
};
}),
availableNotificationLevels: notificationLevels.map((type) => { availableNotificationLevels: notificationLevels.map((type) => {
return { return {
id: type, id: type,
@ -102,6 +94,11 @@ export default Component.extend(UndoChanges, {
return apis.find((a) => a.name === api).endpoints; return apis.find((a) => a.name === api).endpoints;
}, },
@discourseComputed
actionTypes() {
return subscriptionSelectKitContent("action", "types");
},
@discourseComputed("fieldTypes") @discourseComputed("fieldTypes")
hasEventsField(fieldTypes) { hasEventsField(fieldTypes) {
return fieldTypes.map((ft) => ft.id).includes("event"); return fieldTypes.map((ft) => ft.id).includes("event");

Datei anzeigen

@ -1,5 +1,5 @@
import { default as discourseComputed } from "discourse-common/utils/decorators"; import { default as discourseComputed } from "discourse-common/utils/decorators";
import { alias, equal, or } from "@ember/object/computed"; import { equal, or } from "@ember/object/computed";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { selectKitContent } from "../lib/wizard"; import { selectKitContent } from "../lib/wizard";
import UndoChanges from "../mixins/undo-changes"; import UndoChanges from "../mixins/undo-changes";
@ -27,7 +27,6 @@ export default Component.extend(UndoChanges, {
isTextType: or("isText", "isTextarea", "isComposer"), isTextType: or("isText", "isTextarea", "isComposer"),
isComposerPreview: equal("field.type", "composer_preview"), isComposerPreview: equal("field.type", "composer_preview"),
categoryPropertyTypes: selectKitContent(["id", "slug"]), categoryPropertyTypes: selectKitContent(["id", "slug"]),
showAdvanced: alias("field.type"),
messageUrl: "https://discourse.pluginmanager.org/t/field-settings", messageUrl: "https://discourse.pluginmanager.org/t/field-settings",
@discourseComputed("field.type") @discourseComputed("field.type")

Datei anzeigen

@ -6,6 +6,7 @@ import I18n from "I18n";
const icons = { const icons = {
error: "times-circle", error: "times-circle",
success: "check-circle", success: "check-circle",
warn: "exclamation-circle",
info: "info-circle", info: "info-circle",
}; };

Datei anzeigen

@ -6,7 +6,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
export default Component.extend({ export default Component.extend({
classNames: ["realtime-validations"], classNames: ["realtime-validations", "setting", "full", "subscription"],
@discourseComputed @discourseComputed
timeUnits() { timeUnits() {
return ["days", "weeks", "months", "years"].map((unit) => { return ["days", "weeks", "months", "years"].map((unit) => {

Datei anzeigen

@ -0,0 +1,30 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import Subscription from "../mixins/subscription";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
export default Component.extend(Subscription, {
tagName: "a",
classNameBindings: [":wizard-subscription-badge", "subscriptionType"],
attributeBindings: ["title"],
@discourseComputed("subscriptionType")
i18nKey(type) {
return `admin.wizard.subscription.type.${type ? type : "none"}`;
},
@discourseComputed("i18nKey")
title(i18nKey) {
return I18n.t(`${i18nKey}.title`);
},
@discourseComputed("i18nKey")
label(i18nKey) {
return I18n.t(`${i18nKey}.label`);
},
click() {
DiscourseURL.routeTo(this.subscriptionLink);
},
});

Datei anzeigen

@ -0,0 +1,26 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import Subscription from "../mixins/subscription";
export default Component.extend(Subscription, {
classNameBindings: [":wizard-subscription-container", "subscribed"],
@discourseComputed("subscribed")
subscribedIcon(subscribed) {
return subscribed ? "check" : "dash";
},
@discourseComputed("subscribed")
subscribedLabel(subscribed) {
return `admin.wizard.subscription.${
subscribed ? "subscribed" : "not_subscribed"
}.label`;
},
@discourseComputed("subscribed")
subscribedTitle(subscribed) {
return `admin.wizard.subscription.${
subscribed ? "subscribed" : "not_subscribed"
}.title`;
},
});

Datei anzeigen

@ -0,0 +1,36 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import Subscription from "../mixins/subscription";
import I18n from "I18n";
export default Component.extend(Subscription, {
tagName: "a",
classNameBindings: [":btn", ":btn-pavilion-support", "subscriptionType"],
attributeBindings: ["title"],
@discourseComputed("subscribed")
i18nKey(subscribed) {
return `admin.wizard.subscription.cta.${
subscribed ? "subscribed" : "none"
}`;
},
@discourseComputed("subscribed")
icon(subscribed) {
return subscribed ? "far-life-ring" : "external-link-alt";
},
@discourseComputed("i18nKey")
title(i18nKey) {
return I18n.t(`${i18nKey}.title`);
},
@discourseComputed("i18nKey")
label(i18nKey) {
return I18n.t(`${i18nKey}.label`);
},
click() {
window.open(this.subscriptionCtaLink, "_blank").focus();
},
});

Datei anzeigen

@ -0,0 +1,96 @@
import SingleSelectComponent from "select-kit/components/single-select";
import Subscription from "../mixins/subscription";
import wizardSchema from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-schema";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
const nameKey = function (feature, attribute, value) {
if (feature === "action") {
return `admin.wizard.action.${value}.label`;
} else {
return `admin.wizard.${feature}.${attribute}.${value}`;
}
};
export default SingleSelectComponent.extend(Subscription, {
classNames: ["combo-box", "wizard-subscription-selector"],
selectKitOptions: {
autoFilterable: false,
filterable: false,
showFullTitle: true,
headerComponent:
"wizard-subscription-selector/wizard-subscription-selector-header",
caretUpIcon: "caret-up",
caretDownIcon: "caret-down",
},
allowedSubscriptionTypes(feature, attribute, value) {
let attributes = this.subscriptionAttributes[feature];
if (!attributes || !attributes[attribute]) {
return ["none"];
}
let allowedTypes = [];
Object.keys(attributes[attribute]).forEach((subscriptionType) => {
let values = attributes[attribute][subscriptionType];
if (values[0] === "*" || values.includes(value)) {
allowedTypes.push(subscriptionType);
}
});
return allowedTypes;
},
@discourseComputed("feature", "attribute")
content(feature, attribute) {
return wizardSchema[feature][attribute]
.map((value) => {
let allowedSubscriptionTypes = this.allowedSubscriptionTypes(
feature,
attribute,
value
);
let subscriptionRequired =
allowedSubscriptionTypes.length &&
!allowedSubscriptionTypes.includes("none");
let attrs = {
id: value,
name: I18n.t(nameKey(feature, attribute, value)),
subscriptionRequired,
};
if (subscriptionRequired) {
let subscribed = allowedSubscriptionTypes.includes(
this.subscriptionType
);
let selectorKey = subscribed ? "subscribed" : "not_subscribed";
let selectorLabel = `admin.wizard.subscription.${selectorKey}.selector`;
attrs.disabled = !subscribed;
attrs.selectorLabel = selectorLabel;
}
return attrs;
})
.sort(function (a, b) {
if (a.subscriptionType && !b.subscriptionType) {
return 1;
}
if (!a.subscriptionType && b.subscriptionType) {
return -1;
}
if (a.subscriptionType === b.subscriptionType) {
return a.subscriptionType
? a.subscriptionType.localeCompare(b.subscriptionType)
: 0;
} else {
return a.subscriptionType === "standard" ? -1 : 0;
}
});
},
modifyComponentForRow() {
return "wizard-subscription-selector/wizard-subscription-selector-row";
},
});

Datei anzeigen

@ -0,0 +1,17 @@
import SingleSelectHeaderComponent from "select-kit/components/select-kit/single-select-header";
import { computed } from "@ember/object";
import { reads } from "@ember/object/computed";
export default SingleSelectHeaderComponent.extend({
classNames: ["combo-box-header", "wizard-subscription-selector-header"],
caretUpIcon: reads("selectKit.options.caretUpIcon"),
caretDownIcon: reads("selectKit.options.caretDownIcon"),
caretIcon: computed(
"selectKit.isExpanded",
"caretUpIcon",
"caretDownIcon",
function () {
return this.selectKit.isExpanded ? this.caretUpIcon : this.caretDownIcon;
}
),
});

Datei anzeigen

@ -0,0 +1,20 @@
import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
import { default as discourseComputed } from "discourse-common/utils/decorators";
export default SelectKitRowComponent.extend({
classNameBindings: ["isDisabled:disabled"],
@discourseComputed("item")
isDisabled() {
return this.item.disabled;
},
click(event) {
event.preventDefault();
event.stopPropagation();
if (!this.item.disabled) {
this.selectKit.select(this.rowValue, this.item);
}
return false;
},
});

Datei anzeigen

@ -0,0 +1,139 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { equal, notEmpty } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
classNameBindings: ["value.type"],
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"),
isUserSelector: equal("value.type", "user_selector"),
isSubmittedAt: equal("field", "submitted_at"),
isComposerPreview: equal("value.type", "composer_preview"),
textState: "text-collapsed",
toggleText: I18n.t("admin.wizard.expand_text"),
@discourseComputed("value", "isUser")
hasValue(value, isUser) {
if (isUser) {
return value;
}
return value && value.value;
},
@discourseComputed("field", "value.type")
isUser(field, type) {
return field === "username" || field === "user" || type === "user";
},
@discourseComputed("value.type")
isLongtext(type) {
return type === "textarea" || type === "long_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.collapse_text"));
} else if (state === "text-expanded") {
this.set("textState", "text-collapsed");
this.set("toggleText", I18n.t("admin.wizard.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("isUser", "field", "value")
username(isUser, field, value) {
if (isUser) {
return value.username;
}
if (field === "username") {
return value.value;
}
return null;
},
showUsername: notEmpty("username"),
@discourseComputed("username")
userProfileUrl(username) {
if (username) {
return `/u/${username}`;
}
return "/";
},
@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}`;
}
},
});

Datei anzeigen

@ -1,3 +1,7 @@
{{#if currentUser.admin}} {{#if currentUser.admin}}
{{nav-item route="adminWizards" label="admin.wizard.nav_label"}} {{nav-item route="adminWizards" label="admin.wizard.nav_label"}}
{{#if wizardErrorNotice}}
{{d-icon "exclaimation-circle"}}
{{/if}}
{{/if}} {{/if}}

Datei anzeigen

@ -11,7 +11,7 @@ export default Controller.extend({
queryParams: ["refresh_list"], queryParams: ["refresh_list"],
loadingSubscriptions: false, loadingSubscriptions: false,
notAuthorized: not("api.authorized"), notAuthorized: not("api.authorized"),
endpointMethods: selectKitContent(["GET", "PUT", "POST", "PATCH", "DELETE"]), endpointMethods: selectKitContent(["PUT", "POST", "PATCH", "DELETE"]),
showRemove: not("isNew"), showRemove: not("isNew"),
showRedirectUri: and("threeLeggedOauth", "api.name"), showRedirectUri: and("threeLeggedOauth", "api.name"),
responseIcon: null, responseIcon: null,
@ -88,6 +88,11 @@ export default Controller.extend({
twoLeggedOauth: equal("api.authType", "oauth_2"), twoLeggedOauth: equal("api.authType", "oauth_2"),
threeLeggedOauth: equal("api.authType", "oauth_3"), threeLeggedOauth: equal("api.authType", "oauth_3"),
@discourseComputed("api.isNew")
nameClass(isNew) {
return isNew ? "new" : "saved";
},
actions: { actions: {
addParam() { addParam() {
this.get("api.authParams").pushObject({}); this.get("api.authParams").pushObject({});

Datei anzeigen

@ -0,0 +1,14 @@
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.reset")();
},
},
});

Datei anzeigen

@ -0,0 +1,52 @@
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import CustomWizardLogs from "../models/custom-wizard-logs";
import Controller from "@ember/controller";
export default Controller.extend({
refreshing: false,
hasLogs: notEmpty("logs"),
page: 0,
canLoadMore: true,
logs: [],
messageKey: "viewing",
loadLogs() {
if (!this.canLoadMore) {
return;
}
const page = this.get("page");
const wizardId = this.get("wizard.id");
this.set("refreshing", true);
CustomWizardLogs.list(wizardId, page)
.then((result) => {
this.set("logs", this.logs.concat(result.logs));
})
.finally(() => this.set("refreshing", false));
},
@discourseComputed("hasLogs", "refreshing")
noResults(hasLogs, refreshing) {
return !hasLogs && !refreshing;
},
actions: {
loadMore() {
if (!this.loadingMore && this.logs.length < this.total) {
this.set("page", (this.page += 1));
this.loadLogs();
}
},
refresh() {
this.setProperties({
canLoadMore: true,
page: 0,
logs: [],
});
this.loadLogs();
},
},
});

Datei anzeigen

@ -1,50 +1,34 @@
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import CustomWizardLogs from "../models/custom-wizard-logs";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { default as discourseComputed } from "discourse-common/utils/decorators";
export default Controller.extend({ export default Controller.extend({
refreshing: false, documentationUrl: "https://thepavilion.io/t/2818",
hasLogs: notEmpty("logs"),
page: 0,
canLoadMore: true,
logs: [],
loadLogs() { @discourseComputed("wizardId")
if (!this.canLoadMore) { wizardName(wizardId) {
return; 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";
} }
this.set("refreshing", true); return key;
CustomWizardLogs.list()
.then((result) => {
if (!result || result.length === 0) {
this.set("canLoadMore", false);
}
this.set("logs", this.logs.concat(result));
})
.finally(() => this.set("refreshing", false));
},
@discourseComputed("hasLogs", "refreshing")
noResults(hasLogs, refreshing) {
return !hasLogs && !refreshing;
},
actions: {
loadMore() {
this.set("page", (this.page += 1));
this.loadLogs();
},
refresh() {
this.setProperties({
canLoadMore: true,
page: 0,
logs: [],
});
this.loadLogs();
},
}, },
}); });

Datei anzeigen

@ -1,6 +1,69 @@
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-columns", {
model: {
columns: this.get("fields"),
reset: () => {
this.get("fields").forEach((field) => {
field.set("enabled", true);
});
},
},
});
},
},
}); });

Datei anzeigen

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

Datei anzeigen

@ -92,7 +92,11 @@ export default Controller.extend({
wizard wizard
.save(opts) .save(opts)
.then((result) => { .then((result) => {
if (result.wizard_id) {
this.send("afterSave", result.wizard_id); this.send("afterSave", result.wizard_id);
} else if (result.errors) {
this.set("error", result.errors.join(", "));
}
}) })
.catch((result) => { .catch((result) => {
this.set("error", this.getErrorMessage(result)); this.set("error", this.getErrorMessage(result));
@ -118,10 +122,6 @@ export default Controller.extend({
controller.setup(); controller.setup();
}, },
toggleAdvanced() {
this.toggleProperty("wizard.showAdvanced");
},
copyUrl() { copyUrl() {
const $copyRange = $('<p id="copy-range"></p>'); const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(this.wizardUrl); $copyRange.html(this.wizardUrl);

Datei anzeigen

@ -0,0 +1,9 @@
import Controller from "@ember/controller";
import { equal, or } from "@ember/object/computed";
export default Controller.extend({
businessSubscription: equal("subscriptionType", "business"),
communitySubscription: equal("subscriptionType", "community"),
standardSubscription: equal("subscriptionType", "standard"),
showApi: or("businessSubscription", "communitySubscription"),
});

Datei anzeigen

@ -43,7 +43,16 @@ export default {
} }
); );
this.route("adminWizardsLogs", { path: "/logs", resetNamespace: true }); this.route(
"adminWizardsLogs",
{ path: "/logs", resetNamespace: true },
function () {
this.route("adminWizardsLogsShow", {
path: "/:wizardId/",
resetNamespace: true,
});
}
);
this.route("adminWizardsManager", { this.route("adminWizardsManager", {
path: "/manager", path: "/manager",

Datei anzeigen

@ -20,7 +20,7 @@ export default {
return existing.apply(this, [path, opts]); return existing.apply(this, [path, opts]);
}; };
withPluginApi("0.8.7", (api) => { withPluginApi("0.8.36", (api) => {
api.modifyClass("component:d-navigation", { api.modifyClass("component:d-navigation", {
pluginId: "custom-wizard", pluginId: "custom-wizard",
actions: { actions: {

Datei anzeigen

@ -97,11 +97,6 @@ function buildObjectArray(json, type) {
if (present(json)) { if (present(json)) {
json.forEach((objJson, objectIndex) => { json.forEach((objJson, objectIndex) => {
let object = buildObject(objJson, type, objectIndex); let object = buildObject(objJson, type, objectIndex);
if (hasAdvancedProperties(object, type)) {
object.set("showAdvanced", true);
}
array.pushObject(object); array.pushObject(object);
}); });
} }
@ -112,21 +107,11 @@ function buildObjectArray(json, type) {
function buildBasicProperties(json, type, props, objectIndex = null) { function buildBasicProperties(json, type, props, objectIndex = null) {
listProperties(type).forEach((p) => { listProperties(type).forEach((p) => {
props[p] = buildProperty(json, p, type, objectIndex); props[p] = buildProperty(json, p, type, objectIndex);
if (hasAdvancedProperties(json, type)) {
props.showAdvanced = true;
}
}); });
return props; return props;
} }
function hasAdvancedProperties(object, type) {
return Object.keys(object).some((p) => {
return wizardSchema[type].advanced.indexOf(p) > -1 && present(object[p]);
});
}
/// to be removed: necessary due to action array being moved from step to wizard /// to be removed: necessary due to action array being moved from step to wizard
function actionPatch(json) { function actionPatch(json) {
let actions = json.actions || []; let actions = json.actions || [];

Datei anzeigen

@ -19,7 +19,6 @@ const wizard = {
permitted: null, permitted: null,
}, },
mapped: ["permitted"], mapped: ["permitted"],
advanced: ["restart_on_revisit"],
required: ["id"], required: ["id"],
dependent: { dependent: {
after_time: "after_time_scheduled", after_time: "after_time_scheduled",
@ -41,7 +40,6 @@ const step = {
id: null, id: null,
index: null, index: null,
title: null, title: null,
key: null,
banner: null, banner: null,
banner_upload_id: null, banner_upload_id: null,
raw_description: null, raw_description: null,
@ -52,7 +50,6 @@ const step = {
force_final: false, force_final: false,
}, },
mapped: ["required_data", "permitted_params", "condition", "index"], mapped: ["required_data", "permitted_params", "condition", "index"],
advanced: ["required_data", "permitted_params", "condition", "index"],
required: ["id"], required: ["id"],
dependent: {}, dependent: {},
objectArrays: { objectArrays: {
@ -71,14 +68,13 @@ const field = {
image: null, image: null,
image_upload_id: null, image_upload_id: null,
description: null, description: null,
property: null,
required: null, required: null,
key: null,
type: null, type: null,
condition: null, condition: null,
}, },
types: {}, types: {},
mapped: ["prefill", "content", "condition", "index"], mapped: ["prefill", "content", "condition", "index"],
advanced: ["property", "key", "condition", "index"],
required: ["id", "type"], required: ["id", "type"],
dependent: {}, dependent: {},
objectArrays: {}, objectArrays: {},
@ -201,22 +197,24 @@ const action = {
"members_visibility_level", "members_visibility_level",
"add_event", "add_event",
], ],
advanced: [
"code",
"custom_fields",
"skip_redirect",
"suppress_notifications",
"required",
],
required: ["id", "type"], required: ["id", "type"],
dependent: {}, dependent: {},
objectArrays: {}, objectArrays: {},
}; };
const custom_field = {
klass: ["topic", "post", "group", "category"],
type: ["string", "boolean", "integer", "json"],
};
field.type = Object.keys(field.types);
action.type = Object.keys(action.types);
const wizardSchema = { const wizardSchema = {
wizard, wizard,
step, step,
field, field,
custom_field,
action, action,
}; };

Datei anzeigen

@ -0,0 +1,53 @@
import Mixin from "@ember/object/mixin";
import { getOwner } from "discourse-common/lib/get-owner";
import { readOnly } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
const PRODUCT_PAGE = "https://custom-wizard.pavilion.tech";
const SUPPORT_MESSAGE =
"https://coop.pavilion.tech/new-message?username=support&title=Custom%20Wizard%20Support";
const MANAGER_CATEGORY =
"https://discourse.pluginmanager.org/c/discourse-custom-wizard";
export default Mixin.create({
subscriptionLandingUrl: PRODUCT_PAGE,
subscriptionClientUrl: "/admin/plugins/subscription-client",
@discourseComputed
adminWizards() {
return getOwner(this).lookup("controller:admin-wizards");
},
subscribed: readOnly("adminWizards.subscribed"),
subscriptionType: readOnly("adminWizards.subscriptionType"),
businessSubscription: readOnly("adminWizards.businessSubscription"),
communitySubscription: readOnly("adminWizards.communitySubscription"),
standardSubscription: readOnly("adminWizards.standardSubscription"),
subscriptionAttributes: readOnly("adminWizards.subscriptionAttributes"),
subscriptionClientInstalled: readOnly(
"adminWizards.subscriptionClientInstalled"
),
@discourseComputed("subscriptionClientInstalled")
subscriptionLink(subscriptionClientInstalled) {
return subscriptionClientInstalled
? this.subscriptionClientUrl
: this.subscriptionLandingUrl;
},
@discourseComputed("subscriptionType")
subscriptionCtaLink(subscriptionType) {
switch (subscriptionType) {
case "none":
return PRODUCT_PAGE;
case "standard":
return SUPPORT_MESSAGE;
case "business":
return SUPPORT_MESSAGE;
case "community":
return MANAGER_CATEGORY;
default:
return PRODUCT_PAGE;
}
},
});

Datei anzeigen

@ -3,14 +3,54 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
const CustomWizardLogs = EmberObject.extend(); const CustomWizardLogs = EmberObject.extend();
const logItemTypes = {
date: "date_time",
action: "text",
message: "long_text",
user: "user",
username: "text",
};
function logItem(item, attr) {
return {
value: item[attr],
type: logItemTypes[attr],
};
}
CustomWizardLogs.reopenClass({ CustomWizardLogs.reopenClass({
list(page = 0) { list(wizardId, page = 0) {
return ajax("/admin/wizards/logs", { let data = {
data: {
page, page,
}, };
}).catch(popupAjaxError);
return ajax(`/admin/wizards/logs/${wizardId}`, { data })
.catch(popupAjaxError)
.then((result) => {
if (result.logs) {
result.logs = result.logs.map((item) => {
let map = {};
if (item.date) {
map.date = logItem(item, "date");
}
if (item.action) {
map.action = logItem(item, "action");
}
if (item.user) {
map.user = item.user;
} else {
map.user = logItem(item, "username");
}
if (item.message) {
map.message = logItem(item, "message");
}
return map;
});
}
return result;
});
}, },
}); });

Datei anzeigen

@ -8,7 +8,10 @@ export default DiscourseRoute.extend({
}, },
setupController(controller, model) { setupController(controller, model) {
const customFields = A(model || []); const customFields = A(model.custom_fields || []);
controller.set("customFields", customFields);
controller.setProperties({
customFields,
});
}, },
}); });

Datei anzeigen

@ -0,0 +1,17 @@
import CustomWizardLogs from "../models/custom-wizard-logs";
import DiscourseRoute from "discourse/routes/discourse";
import { A } from "@ember/array";
export default DiscourseRoute.extend({
model(params) {
return CustomWizardLogs.list(params.wizardId);
},
setupController(controller, model) {
controller.setProperties({
wizard: model.wizard,
logs: A(model.logs),
total: model.total,
});
},
});

Datei anzeigen

@ -1,12 +1,24 @@
import CustomWizardLogs from "../models/custom-wizard-logs";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model() { model() {
return CustomWizardLogs.list(); return ajax(`/admin/wizards/wizard`);
}, },
setupController(controller, model) { setupController(controller, model) {
controller.set("logs", model); const showParams = this.paramsFor("adminWizardsLogsShow");
controller.setProperties({
wizardId: showParams.wizardId,
wizardList: model.wizard_list,
});
},
actions: {
changeWizard(wizardId) {
this.controllerFor("adminWizardsLogs").set("wizardId", wizardId);
this.transitionTo("adminWizardsLogsShow", wizardId);
},
}, },
}); });

Datei anzeigen

@ -1,42 +1,24 @@
import { A } from "@ember/array";
import EmberObject from "@ember/object";
import CustomWizardAdmin from "../models/custom-wizard-admin"; import CustomWizardAdmin from "../models/custom-wizard-admin";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"];
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model(params) { model(params) {
return CustomWizardAdmin.submissions(params.wizardId); return CustomWizardAdmin.submissions(params.wizardId);
}, },
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);
}
}); });
});
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({ controller.setProperties({
wizard: model.wizard, wizard: model.wizard,
submissions, fields: A(fields),
fields, submissions: A(model.submissions),
total: model.total,
}); });
}
}, },
}); });

Datei anzeigen

@ -1,7 +1,21 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
beforeModel(transition) { model() {
return ajax("/admin/wizards");
},
setupController(controller, model) {
controller.setProperties({
subscribed: model.subscribed,
subscriptionType: model.subscription_type,
subscriptionAttributes: model.subscription_attributes,
subscriptionClientInstalled: model.subscription_client_installed,
});
},
afterModel(model, transition) {
if (transition.targetName === "adminWizards.index") { if (transition.targetName === "adminWizards.index") {
this.transitionTo("adminWizardsWizard"); this.transitionTo("adminWizardsWizard");
} }

Datei anzeigen

@ -21,11 +21,11 @@
{{/if}} {{/if}}
</div> </div>
<div class="wizard-header"> <div class="wizard-header large">
{{#if api.isNew}} {{#if api.isNew}}
{{i18n "admin.wizard.api.new"}} {{i18n "admin.wizard.api.new"}}
{{else}} {{else}}
{{api.title}} <span>{{api.title}}</span>
{{/if}} {{/if}}
</div> </div>
@ -35,12 +35,12 @@
{{input value=api.title placeholder=(i18n "admin.wizard.api.title_placeholder")}} {{input value=api.title placeholder=(i18n "admin.wizard.api.title_placeholder")}}
</div> </div>
<div class="name"> <div class="name {{nameClass}}">
<label>{{i18n "admin.wizard.api.name"}}</label> <label>{{i18n "admin.wizard.api.name"}}</label>
{{#if api.isNew}} {{#if api.isNew}}
{{input value=api.name placeholder=(i18n "admin.wizard.api.name_placeholder")}} {{input value=api.name placeholder=(i18n "admin.wizard.api.name_placeholder")}}
{{else}} {{else}}
{{api.name}} <span>{{api.name}}</span>
{{/if}} {{/if}}
</div> </div>
</div> </div>
@ -63,7 +63,7 @@
{{/if}} {{/if}}
</div> </div>
<div class="wizard-header"> <div class="wizard-header medium">
{{i18n "admin.wizard.api.auth.label"}} {{i18n "admin.wizard.api.auth.label"}}
</div> </div>
</div> </div>
@ -71,7 +71,7 @@
<div class="wizard-api-authentication"> <div class="wizard-api-authentication">
<div class="settings"> <div class="settings">
<div class="wizard-header medium"> <div class="wizard-header small">
{{i18n "admin.wizard.api.auth.settings"}} {{i18n "admin.wizard.api.auth.settings"}}
</div> </div>
@ -174,7 +174,7 @@
{{/if}} {{/if}}
</div> </div>
<div class="wizard-header medium"> <div class="wizard-header small">
{{i18n "admin.wizard.api.status.label"}} {{i18n "admin.wizard.api.status.label"}}
</div> </div>
@ -220,7 +220,7 @@
{{/if}} {{/if}}
</div> </div>
<div class="wizard-header"> <div class="wizard-header medium">
{{i18n "admin.wizard.api.endpoint.label"}} {{i18n "admin.wizard.api.endpoint.label"}}
</div> </div>
@ -277,11 +277,15 @@
{{/if}} {{/if}}
</div> </div>
<div class="wizard-header"> <div class="wizard-header medium">
{{i18n "admin.wizard.api.log.label"}} {{i18n "admin.wizard.api.log.label"}}
{{d-button action=(action "clearLogs")
icon="trash-alt" <div class="controls">
class="clear-logs"}} {{d-button
action=(action "clearLogs")
class="clear-logs"
label="admin.wizard.api.log.clear"}}
</div>
</div> </div>
<div class="wizard-api-log"> <div class="wizard-api-log">
@ -300,7 +304,7 @@
<td>{{logentry.time}}</td> <td>{{logentry.time}}</td>
<td class="user-image"> <td class="user-image">
<div class="user-image-inner"> <div class="user-image-inner">
<a href={{logentry.userpath}} data-user-card={{logentry.username}}>{{avatar logentry imageSize="large"}}</a> <a href={{logentry.userpath}} data-user-card={{logentry.username}}>{{avatar logentry imageSize="medium"}}</a>
</div> </div>
</td> </td>
<td>{{logentry.status}}</td> <td>{{logentry.status}}</td>

Datei anzeigen

@ -8,7 +8,7 @@
)}} )}}
{{d-button {{d-button
action="createApi" action=(route-action "createApi")
label="admin.wizard.api.create" label="admin.wizard.api.create"
icon="plus"}} icon="plus"}}
</div> </div>

Datei anzeigen

@ -5,7 +5,7 @@
{{d-button {{d-button
label="admin.wizard.custom_field.add" label="admin.wizard.custom_field.add"
icon="plus" icon="plus"
action="addField"}} action=(action "addField")}}
</div> </div>
</div> </div>

Datei anzeigen

@ -0,0 +1,45 @@
{{#if logs}}
<div class="wizard-header large">
<label>
{{i18n "admin.wizard.log.title" name=wizard.name}}
</label>
<div class="controls">
{{d-button
label="refresh"
icon="sync"
action=(action "refresh")
class="refresh"}}
</div>
</div>
<div class="wizard-table">
{{#load-more selector=".wizard-table tr" action=(action "loadMore")}}
{{#if noResults}}
<p>{{i18n "search.no_results"}}</p>
{{else}}
<table>
<thead>
<tr>
<th class="date">{{i18n "admin.wizard.log.date"}}</th>
<th>{{i18n "admin.wizard.log.action"}}</th>
<th>{{i18n "admin.wizard.log.user"}}</th>
<th>{{i18n "admin.wizard.log.message"}}</th>
</tr>
</thead>
<tbody>
{{#each logs as |log|}}
<tr>
{{#each-in log as |field value|}}
<td class="small">{{wizard-table-field field=field value=value}}</td>
{{/each-in}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{conditional-loading-spinner condition=refreshing}}
{{/load-more}}
</div>
{{/if}}

Datei anzeigen

@ -1,34 +1,19 @@
<div class="admin-wizard-controls"> <div class="admin-wizard-select admin-wizard-controls">
<h3>{{i18n "admin.wizard.log.nav_label"}}</h3> {{combo-box
value=wizardId
{{d-button content=wizardList
label="refresh" onChange=(route-action "changeWizard")
icon="sync" options=(hash
action="refresh" none="admin.wizard.select"
class="refresh"}} )}}
</div> </div>
{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}} {{wizard-message
{{#if noResults}} key=messageKey
<p>{{i18n "search.no_results"}}</p> opts=messageOpts
{{else}} url=documentationUrl
<table class="table grid"> component="logs"}}
<thead>
<tr>
<th>Message</th>
<th class="date">Date</th>
</tr>
</thead>
<tbody>
{{#each logs as |log|}}
<tr>
<td>{{log.message}}</td>
<td class="date">{{bound-date log.date}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{conditional-loading-spinner condition=refreshing}} <div class="admin-wizard-container">
{{/load-more}} {{outlet}}
</div>

Datei anzeigen

@ -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.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"}}
@ -10,24 +26,36 @@
</a> </a>
</div> </div>
<div class="wizard-submissions"> <div class="wizard-table">
{{#load-more selector=".wizard-table tr" action=(action "loadMore")}}
{{#if noResults}}
<p>{{i18n "search.no_results"}}</p>
{{else}}
<table> <table>
<thead> <thead>
<tr> <tr>
{{#each fields as |f|}} {{#each fields as |field|}}
<th>{{f}}</th> {{#if field.enabled}}
<th>
{{field.label}}
</th>
{{/if}}
{{/each}} {{/each}}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each submissions as |s|}} {{#each displaySubmissions as |submission|}}
<tr> <tr>
{{#each-in s as |k v|}} {{#each-in submission as |field value|}}
<td>{{v}}</td> <td>{{wizard-table-field field=field value=value}}</td>
{{/each-in}} {{/each-in}}
</tr> </tr>
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
{{/if}}
{{conditional-loading-spinner condition=loadingMore}}
{{/load-more}}
</div> </div>
{{/if}} {{/if}}

Datei anzeigen

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

Datei anzeigen

@ -101,7 +101,7 @@
{{input type="checkbox" checked=wizard.after_time}} {{input type="checkbox" checked=wizard.after_time}}
<span>{{i18n "admin.wizard.after_time_label"}}</span> <span>{{i18n "admin.wizard.after_time_label"}}</span>
{{d-button {{d-button
action="setNextSessionScheduled" action=(action "setNextSessionScheduled")
translatedLabel=nextSessionScheduledLabel translatedLabel=nextSessionScheduledLabel
class="btn-after-time" class="btn-after-time"
icon="far-calendar"}} icon="far-calendar"}}
@ -126,11 +126,7 @@
</div> </div>
</div> </div>
{{wizard-advanced-toggle showAdvanced=wizard.showAdvanced}} {{#wizard-subscription-container}}
{{#if wizard.showAdvanced}}
<div class="advanced-settings">
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n "admin.wizard.save_submissions"}}</label> <label>{{i18n "admin.wizard.save_submissions"}}</label>
@ -150,19 +146,7 @@
<span>{{i18n "admin.wizard.restart_on_revisit_label"}}</span> <span>{{i18n "admin.wizard.restart_on_revisit_label"}}</span>
</div> </div>
</div> </div>
{{/wizard-subscription-container}}
<div class="setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.resume_on_revisit"}}</label>
</div>
<div class="setting-value">
{{input type="checkbox" checked=wizard.resume_on_revisit}}
<span>{{i18n "admin.wizard.resume_on_revisit_label"}}</span>
</div>
</div>
</div>
{{/if}}
</div> </div>
{{wizard-links {{wizard-links
@ -176,7 +160,8 @@
wizard=wizard wizard=wizard
currentField=currentField currentField=currentField
wizardFields=wizardFields wizardFields=wizardFields
fieldTypes=fieldTypes}} fieldTypes=fieldTypes
subscribed=subscribed}}
{{/if}} {{/if}}
{{wizard-links {{wizard-links
@ -185,9 +170,9 @@
items=wizard.actions items=wizard.actions
generateLabels=true}} generateLabels=true}}
{{#each wizard.actions as |action|}} {{#each wizard.actions as |wizardAction|}}
{{wizard-custom-action {{wizard-custom-action
action=action action=wizardAction
currentActionId=currentAction.id currentActionId=currentAction.id
wizard=wizard wizard=wizard
apis=apis apis=apis

Datei anzeigen

@ -8,7 +8,7 @@
)}} )}}
{{d-button {{d-button
action="createWizard" action=(route-action "createWizard")
label="admin.wizard.create" label="admin.wizard.create"
icon="plus"}} icon="plus"}}
</div> </div>

Datei anzeigen

@ -2,17 +2,15 @@
{{nav-item route="adminWizardsWizard" label="admin.wizard.nav_label"}} {{nav-item route="adminWizardsWizard" label="admin.wizard.nav_label"}}
{{nav-item route="adminWizardsCustomFields" label="admin.wizard.custom_field.nav_label"}} {{nav-item route="adminWizardsCustomFields" label="admin.wizard.custom_field.nav_label"}}
{{nav-item route="adminWizardsSubmissions" label="admin.wizard.submissions.nav_label"}} {{nav-item route="adminWizardsSubmissions" label="admin.wizard.submissions.nav_label"}}
{{#if siteSettings.wizard_apis_enabled}} {{#if showApi}}
{{nav-item route="adminWizardsApi" label="admin.wizard.api.nav_label"}} {{nav-item route="adminWizardsApi" label="admin.wizard.api.nav_label"}}
{{/if}} {{/if}}
{{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"}}
<div class="announcement"> <div class="admin-actions">
<a href="https://custom-wizard.pavilion.tech/subscriptions" target="_blank" title="Click to learn more about Custom Wizard Subscriptions"> {{wizard-subscription-badge}}
<img src='/images/emoji/twitter/man_mage.png?v=12'> {{wizard-subscription-cta}}
<span>Custom Wizard Subscriptions Are Coming!</span>
</a>
</div> </div>
{{/admin-nav}} {{/admin-nav}}

Datei anzeigen

@ -1,17 +1,23 @@
{{#if showInputs}} {{#if showInputs}}
<td> <td>
{{combo-box {{wizard-subscription-selector
value=field.klass value=field.klass
content=klassContent feature="custom_field"
attribute="klass"
onChange=(action (mut field.klass))
options=(hash
none="admin.wizard.custom_field.klass.select" none="admin.wizard.custom_field.klass.select"
onChange=(action (mut field.klass))}} )}}
</td> </td>
<td> <td>
{{combo-box {{wizard-subscription-selector
value=field.type value=field.type
content=typeContent feature="custom_field"
attribute="type"
onChange=(action (mut field.type))
options=(hash
none="admin.wizard.custom_field.type.select" none="admin.wizard.custom_field.type.select"
onChange=(action (mut field.type))}} )}}
</td> </td>
<td class="input"> <td class="input">
{{input {{input
@ -22,8 +28,10 @@
{{multi-select {{multi-select
value=field.serializers value=field.serializers
content=serializerContent content=serializerContent
onChange=(action (mut field.serializers))
options=(hash
none="admin.wizard.custom_field.serializers.select" none="admin.wizard.custom_field.serializers.select"
onChange=(action (mut field.serializers))}} )}}
</td> </td>
<td class="actions"> <td class="actions">
{{#if loading}} {{#if loading}}
@ -34,17 +42,17 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{d-button {{d-button
action="destroy" action=(action "destroy")
icon="trash-alt" icon="trash-alt"
class="destroy" class="destroy"
disabled=destroyDisabled}} disabled=destroyDisabled}}
{{d-button {{d-button
icon="save" icon="save"
action="save" action=(action "save")
disabled=saveDisabled disabled=saveDisabled
class="save"}} class="save"}}
{{d-button {{d-button
action="close" action=(action "close")
icon="times" icon="times"
disabled=closeDisabled}} disabled=closeDisabled}}
</td> </td>
@ -69,7 +77,7 @@
</td> </td>
{{else}} {{else}}
<td class="actions"> <td class="actions">
{{d-button action="edit" icon="pencil-alt"}} {{d-button action=(action "edit") icon="pencil-alt"}}
</td> </td>
{{/if}} {{/if}}
{{/if}} {{/if}}

Datei anzeigen

@ -1,4 +0,0 @@
{{d-button
action="toggleAdvanced"
label="admin.wizard.advanced"
class=toggleClass}}

Datei anzeigen

@ -1,6 +1,6 @@
{{#if showUndo}} {{#if showUndo}}
{{d-button {{d-button
action="undoChanges" action=(action "undoChanges")
icon=undoIcon icon=undoIcon
label=undoKey label=undoKey
class="undo-changes"}} class="undo-changes"}}
@ -12,13 +12,15 @@
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{combo-box {{wizard-subscription-selector
value=action.type value=action.type
content=actionTypes feature="action"
attribute="type"
onChange=(action "changeType") onChange=(action "changeType")
options=(hash options=(hash
none="admin.wizard.select_type" none="admin.wizard.select_type"
)}} )
}}
</div> </div>
</div> </div>
@ -733,12 +735,6 @@
</div> </div>
{{/if}} {{/if}}
{{#if showAdvanced}}
{{wizard-advanced-toggle showAdvanced=action.showAdvanced}}
{{#if action.showAdvanced}}
<div class="advanced-settings">
{{#if hasCustomFields}} {{#if hasCustomFields}}
<div class="setting full field-mapper-setting"> <div class="setting full field-mapper-setting">
<div class="setting-label"> <div class="setting-label">
@ -826,6 +822,3 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
</div>
{{/if}}
{{/if}}

Datei anzeigen

@ -1,6 +1,6 @@
{{#if showUndo}} {{#if showUndo}}
{{d-button {{d-button
action="undoChanges" action=(action "undoChanges")
icon=undoIcon icon=undoIcon
label=undoKey label=undoKey
class="undo-changes"}} class="undo-changes"}}
@ -224,12 +224,7 @@
</div> </div>
{{/if}} {{/if}}
{{#if showAdvanced}} {{#wizard-subscription-container}}
{{wizard-advanced-toggle showAdvanced=field.showAdvanced}}
{{#if field.showAdvanced}}
<div class="advanced-settings">
<div class="setting full field-mapper-setting"> <div class="setting full field-mapper-setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n "admin.wizard.condition"}}</label> <label>{{i18n "admin.wizard.condition"}}</label>
@ -272,22 +267,7 @@
</div> </div>
{{/if}} {{/if}}
<div class="setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.translation"}}</label>
</div>
<div class="setting-value medium">
{{input
name="key"
value=field.key
class="medium"
placeholderKey="admin.wizard.translation_placeholder"}}
</div>
</div>
{{#if validations}} {{#if validations}}
{{wizard-realtime-validations field=field validations=validations}} {{wizard-realtime-validations field=field validations=validations}}
{{/if}} {{/if}}
</div> {{/wizard-subscription-container}}
{{/if}}
{{/if}}

Datei anzeigen

@ -34,11 +34,7 @@
</div> </div>
</div> </div>
{{wizard-advanced-toggle showAdvanced=step.showAdvanced}} {{#wizard-subscription-container}}
{{#if step.showAdvanced}}
<div class="advanced-settings">
<div class="setting full field-mapper-setting"> <div class="setting full field-mapper-setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n "admin.wizard.condition"}}</label> <label>{{i18n "admin.wizard.condition"}}</label>
@ -64,6 +60,7 @@
<div class="setting-label"> <div class="setting-label">
<label>{{i18n "admin.wizard.step.required_data.label"}}</label> <label>{{i18n "admin.wizard.step.required_data.label"}}</label>
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=step.required_data inputs=step.required_data
@ -102,20 +99,7 @@
)}} )}}
</div> </div>
</div> </div>
{{/wizard-subscription-container}}
<div class="setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.translation"}}</label>
</div>
<div class="setting-value">
{{input
name="key"
value=step.key
placeholderKey="admin.wizard.translation_placeholder"}}
</div>
</div>
</div>
{{/if}}
{{wizard-links {{wizard-links
itemType="field" itemType="field"
@ -130,5 +114,6 @@
currentFieldId=currentField.id currentFieldId=currentField.id
fieldTypes=fieldTypes fieldTypes=fieldTypes
removeField="removeField" removeField="removeField"
wizardFields=wizardFields}} wizardFields=wizardFields
subscribed=subscribed}}
{{/each}} {{/each}}

Datei anzeigen

@ -4,17 +4,16 @@
{{#if anyLinks}} {{#if anyLinks}}
{{#each links as |link|}} {{#each links as |link|}}
<div data-id={{link.id}}> <div data-id={{link.id}}>
{{d-button action="change" actionParam=link.id translatedLabel=link.label class=link.classes}} {{d-button action=(action "change") actionParam=link.id translatedLabel=link.label class=link.classes}}
{{#unless link.first}} {{#unless link.first}}
{{d-button action="back" actionParam=link icon="arrow-left" class="back"}} {{d-button action=(action "back") actionParam=link icon="arrow-left" class="back"}}
{{/unless}} {{/unless}}
{{#unless link.last}} {{#unless link.last}}
{{d-button action="forward" actionParam=link icon="arrow-right" class="forward"}} {{d-button action=(action "forward") actionParam=link icon="arrow-right" class="forward"}}
{{/unless}} {{/unless}}
{{d-button action="remove" actionParam=link.id icon="times" class="remove"}} {{d-button action=(action "remove") actionParam=link.id icon="times" class="remove"}}
</div> </div>
{{/each}} {{/each}}
{{/if}} {{/if}}
{{d-button action="add" label="admin.wizard.add" icon="plus"}} {{d-button action=(action "add") label="admin.wizard.add" icon="plus"}}
</div> </div>

Datei anzeigen

@ -15,6 +15,6 @@
{{#if canAdd}} {{#if canAdd}}
<span class="add-mapper-input"> <span class="add-mapper-input">
{{d-button action="add" label="admin.wizard.add" icon="plus"}} {{d-button action=(action "add") label="admin.wizard.add" icon="plus"}}
</span> </span>
{{/if}} {{/if}}

Datei anzeigen

@ -1,5 +1,7 @@
<h3>{{i18n "admin.wizard.field.validations.header"}}</h3> <div class="setting-label">
<label>{{i18n "admin.wizard.field.validations.header"}}</label>
</div>
<div class="setting-value full">
<ul> <ul>
{{#each-in field.validations as |type props|}} {{#each-in field.validations as |type props|}}
<li> <li>
@ -39,12 +41,13 @@
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{radio-button name=(concat type field.id) value="above" selection=props.position}} {{radio-button name=(concat type field.id) value="above" selection=props.position}}
{{i18n "admin.wizard.field.validations.above"}} <span>{{i18n "admin.wizard.field.validations.above"}}</span>
{{radio-button name=(concat type field.id) value="below" selection=props.position}} {{radio-button name=(concat type field.id) value="below" selection=props.position}}
{{i18n "admin.wizard.field.validations.below"}} <span>{{i18n "admin.wizard.field.validations.below"}}</span>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
{{/each-in}} {{/each-in}}
</ul> </ul>
</div>

Datei anzeigen

@ -0,0 +1,6 @@
<svg width="300px" height="300px" viewBox="0 0 300 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="pavilion-logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path id="Combined-Shape" stroke="currentColor" stroke-width="35" d="M41.1381822,291.00006 L40.5778853,130.009744 M258.850727,291.638415 L259.290397,130.37133 M36.0002279,140.721678 L139.995368,36.2122772 M263.350577,141.009083 L138.927245,16.2478517"></path>
</g>
</svg>
<span>{{label}}</span>

Nachher

Breite:  |  Höhe:  |  Größe: 538 B

Datei anzeigen

@ -0,0 +1,12 @@
<div class="subscription-header">
<h4>{{i18n "admin.wizard.subscription.title"}}</h4>
<a href={{subscriptionLink}} title={{i18n subscribedTitle}}>
{{d-icon subscribedIcon}}
{{i18n subscribedLabel}}
</a>
</div>
<div class="subscription-settings">
{{yield}}
</div>

Datei anzeigen

@ -0,0 +1 @@
{{d-icon icon}}{{label}}

Datei anzeigen

@ -0,0 +1,15 @@
<div class="select-kit-header-wrapper">
{{component selectKit.options.selectedNameComponent
tabindex=tabindex
item=selectedContent
selectKit=selectKit
shouldDisplayClearableButton=shouldDisplayClearableButton
}}
{{#if subscriptionRequired}}
<span class="subscription-label">{{i18n selectorLabel}}</span>
{{/if}}
{{d-icon caretIcon class="caret-icon"}}
</div>

Datei anzeigen

@ -0,0 +1,15 @@
{{#if icons}}
<div class="icons">
<span class="selection-indicator"></span>
{{#each icons as |icon|}}
{{d-icon icon translatedtitle=(dasherize title)}}
{{/each}}
</div>
{{/if}}
<div class="texts">
<span class="name">{{html-safe label}}</span>
{{#if item.subscriptionRequired}}
<span class="subscription-label">{{i18n item.selectorLabel}}</span>
{{/if}}
</div>

Datei anzeigen

@ -0,0 +1,161 @@
{{#if hasValue}}
{{#if isText}}
{{value.value}}
{{/if}}
{{#if isLongtext}}
<div class="wizard-table-long-text">
<p class="wizard-table-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposer}}
<div class="wizard-table-long-text">
<p class="wizard-table-composer-text wizard-table-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposerPreview}}
{{d-icon "comment-alt"}}
<span class="wizard-table-composer-text">
{{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}}
</span>
{{/if}}
{{#if isTextOnly}}
{{value.value}}
{{/if}}
{{#if isDate}}
<span class="wizard-table-icon-item">
{{d-icon "calendar"}}{{value.value}}
</span>
{{/if}}
{{#if isTime}}
<span class="wizard-table-icon-item">
{{d-icon "clock"}}{{value.value}}
</span>
{{/if}}
{{#if isDateTime}}
<span class="wizard-table-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="wizard-table-icon-item checkbox-true">
{{d-icon "check"}}{{value.value}}
</span>
{{else}}
<span class="wizard-table-icon-item checkbox-false">
{{d-icon "times"}}{{value.value}}
</span>
{{/if}}
{{/if}}
{{#if isUrl}}
<span class="wizard-table-icon-item 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="wizard-table-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}}
{{/if}}
{{#if showUsername}}
<a target="_blank" rel="noopener noreferrer" href={{userProfileUrl}} title={{username}}>
{{username}}
</a>
{{/if}}
{{#if isSubmittedAt}}
<span class="date" title={{value.value}}>
{{d-icon "clock"}}{{format-date value format="tiny"}}
</span>
{{/if}}
{{else}}
&mdash;
{{/if}}

Datei anzeigen

@ -6,13 +6,13 @@
<div class="wizard-editor-gutter"> <div class="wizard-editor-gutter">
{{#if previewEnabled}} {{#if previewEnabled}}
{{d-button {{d-button
action="togglePreview" action=(action "togglePreview")
translatedLabel=previewLabel}} translatedLabel=previewLabel}}
{{/if}} {{/if}}
{{#if fieldsEnabled}} {{#if fieldsEnabled}}
{{d-button {{d-button
action="togglePopover" action=(action "togglePopover")
translatedLabel=popoverLabel}} translatedLabel=popoverLabel}}
{{#if showPopover}} {{#if showPopover}}

Datei anzeigen

@ -0,0 +1,32 @@
{{#d-modal-body title="admin.wizard.edit_columns"}}
{{#if loading}}
{{loading-spinner size="large"}}
{{else}}
<div class="edit-directory-columns-container">
{{#each model.columns as |column|}}
<div class="edit-directory-column">
<div class="left-content">
<label class="column-name">
{{input type="checkbox" checked=column.enabled}}
{{directory-table-header-title field=column.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>

Datei anzeigen

@ -9,7 +9,7 @@
<div class="modal-footer"> <div class="modal-footer">
{{d-button {{d-button
action="submit" action=(action "submit")
class="btn-primary" class="btn-primary"
label="admin.wizard.after_time_modal.done" label="admin.wizard.after_time_modal.done"
disabled=submitDisabled}} disabled=submitDisabled}}

Datei anzeigen

@ -1,7 +1,13 @@
@import "wizard-mapper"; @import "admin/mapper";
@import "wizard-manager"; @import "admin/manager";
@import "wizard-api"; @import "admin/api";
@import "common/components/buttons"; @import "common/components/buttons";
@import "admin/variables";
$expired: #339b18;
$info: #038ae7;
$warning: #d47e00;
$error: #ef1700;
.announcement { .announcement {
margin-left: auto; margin-left: auto;
@ -25,11 +31,24 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 20px; margin-bottom: 10px;
min-height: 34px;
& + .wizard-message + div { & + .wizard-message + div {
margin-top: 20px; margin-top: 20px;
} }
h3 {
margin-bottom: 0;
}
}
.admin-wizards .admin-actions {
display: flex;
.btn-pavilion-support {
margin-left: 10px;
}
} }
.wizard-message { .wizard-message {
@ -78,35 +97,68 @@
} }
} }
& + div { & + div,
& + div + div {
margin-top: 30px; margin-top: 30px;
} }
} }
.wizard-submissions { .wizard-table {
overflow: scroll; overflow: scroll;
table td { table td:not(.small) {
min-width: 150px; min-width: 150px;
} }
table thead th {
text-transform: capitalize;
} }
.admin-wizards-logs { .wizard-table-icon-item {
.admin-wizard-controls { display: flex;
h3 { align-items: center;
margin: 0 7px;
svg {
margin-right: 5px;
} }
} }
.wizard-logs { .wizard-table-checkbox-true {
.date { text-transform: capitalize;
width: 100px; color: var(--success);
} }
.wizard-table-checkbox-false {
text-transform: capitalize;
color: var(--danger);
}
.wizard-table-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);
}
}
.wizard-table-composer-text {
font-family: monospace;
} }
} }
.wizard-settings-parent { .wizard-settings-parent {
padding: 20px; padding: 1em;
border: 1px solid var(--primary-low); border: 1px solid var(--primary-low);
} }
@ -127,10 +179,20 @@
.wizard-basic-details, .wizard-basic-details,
.wizard-custom-field, .wizard-custom-field,
.advanced-settings { .subscription-settings {
@extend .wizard-settings-group; @extend .wizard-settings-group;
} }
.admin-wizard-container.settings {
.wizard-settings {
.wizard-subscription-container {
[class~="setting"] {
margin-bottom: 0;
}
}
}
}
.wizard-custom-field { .wizard-custom-field {
background: transparent; background: transparent;
background-color: var(--primary-very-low); background-color: var(--primary-very-low);
@ -169,6 +231,7 @@
.wizard-header { .wizard-header {
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
&.large { &.large {
font-size: 1.5em; font-size: 1.5em;
@ -187,6 +250,10 @@
margin-bottom: 0; margin-bottom: 0;
} }
button {
font-size: 1rem;
}
.download-link { .download-link {
font-size: 1rem; font-size: 1rem;
line-height: 20px; line-height: 20px;
@ -202,25 +269,28 @@
font-size: 1rem; font-size: 1rem;
background-color: var(--primary-low); background-color: var(--primary-low);
} }
button {
font-size: 1rem;
}
} }
} }
&.medium { &.medium {
font-size: 1.3em; font-size: 1.2em;
} }
&.small { &.small {
font-size: 1em; font-size: 1em;
font-weight: 700;
margin-bottom: 5px; margin-bottom: 5px;
} }
&.underline { &.underline {
text-decoration: underline; text-decoration: underline;
} }
.controls {
font-size: 1rem;
display: flex;
margin-left: auto;
}
} }
.admin-wizard-buttons { .admin-wizard-buttons {
@ -261,7 +331,7 @@
font-size: 0.85em; font-size: 0.85em;
} }
span { > span {
font-size: 0.929em; font-size: 0.929em;
} }
@ -394,14 +464,16 @@
.setting-gutter { .setting-gutter {
margin-top: 5px; margin-top: 5px;
} }
&.subscription {
.setting-label {
display: flex;
flex-direction: column;
label {
margin: 0;
}
} }
.advanced-settings {
width: 100%;
margin-top: 30px;
[class~="setting"]:first-of-type {
border-top: none;
} }
} }
@ -699,27 +771,59 @@
} }
} }
.realtime-validations > ul { .admin-wizard-container.settings .realtime-validations .setting-value > ul {
list-style: none; list-style: none;
margin: 0; margin: 0;
width: 100%;
display: flex;
flex-wrap: wrap;
> li { > li {
background-color: var(--primary-low); border: 1px solid var(--primary);
padding: 1em; padding: 1em;
margin: 0 0 1em 0; margin: 0 0 1em 0;
input { .setting-title {
margin-bottom: 0; display: flex;
align-items: center;
h4 {
margin: 0 15px 0 0;
}
input[type="checkbox"] {
margin: 0 5px 0 0;
}
}
.setting-label {
width: 100px;
}
.setting-value {
display: flex;
align-items: center;
.input .select-kit,
> .select-kit {
max-width: unset !important;
}
> span {
margin-right: 1em;
}
} }
} }
} }
.validation-container { .validation-container {
display: flex; display: flex;
flex-direction: column;
padding: 1em 0; padding: 1em 0;
.validation-section { .validation-section {
width: 250px; min-width: 250px;
margin: 0.5em 0;
} }
} }
@ -736,3 +840,113 @@
width: 80px; width: 80px;
vertical-align: middle; vertical-align: middle;
} }
.btn.btn-pavilion-support {
background: var(--pavilion-primary);
color: var(--pavilion-secondary);
.d-icon {
color: var(--pavilion-secondary);
}
&:hover,
&:focus {
background: darken($pavilion_primary, 5%);
&[href],
svg.d-icon {
color: darken($pavilion_secondary, 10%);
}
}
}
.wizard-subscription-container {
width: 100%;
padding: 1em;
background-color: rgba($pavilion_primary, 0.1);
.subscription-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.25em;
h3 {
margin: 0;
}
a {
color: var(--primary);
}
}
&:not(.subscribed) .subscription-settings {
filter: blur(1px);
pointer-events: none;
}
}
.wizard-subscription-badge {
display: inline-flex;
align-items: center;
max-height: 34px;
box-sizing: border-box;
position: relative;
cursor: pointer;
padding: 0.5em 0.65em;
background-color: rgba($primary-medium, 0.05);
border: 1.5px solid rgba($primary-medium, 0.5);
color: $primary-medium;
&:hover {
color: $primary-medium;
}
svg {
width: 15px;
height: 15px;
margin-right: 0.45em;
margin-bottom: 0.15em;
}
&.standard {
background-color: rgba($subscription_standard, 0.05);
border: 1.5px solid rgba($subscription_standard, 0.5);
color: $subscription_standard;
}
&.business {
background-color: $subscription_business;
border: 1.5px solid $subscription_business;
color: $secondary;
}
&.community {
background-color: $subscription_community;
border: 1.5px solid $pavilion_primary;
color: $pavilion_primary;
}
.d-icon {
margin-right: 0.75em;
}
}
.wizard-subscription-selector.select-kit.single-select {
.select-kit-row {
.texts {
display: flex;
align-items: center;
}
&.disabled {
background: var(--primary-low);
cursor: unset;
}
}
.subscription-label {
margin-left: 0.75em;
padding-top: 0.25em;
color: var(--pavilion-primary);
font-size: 0.75em;
}
}

Datei anzeigen

@ -40,9 +40,18 @@
float: right; float: right;
} }
.wizard-header { .metadata {
overflow: hidden; display: flex;
font-size: 1.3em;
.title {
margin-right: 1em;
}
.name.saved span {
display: inline-block;
padding: 6px 12px;
background-color: var(--primary-low);
}
} }
} }
@ -74,10 +83,6 @@
width: 50%; width: 50%;
max-width: 50%; max-width: 50%;
.wizard-header {
overflow: hidden;
}
.authorization { .authorization {
float: right; float: right;
} }

Datei anzeigen

@ -2,10 +2,6 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
h3 {
margin-bottom: 0;
}
.buttons { .buttons {
display: flex; display: flex;
margin-left: auto; margin-left: auto;

Datei anzeigen

@ -0,0 +1,16 @@
$pavilion_primary: #3c1c8c;
$pavilion_secondary: #ffffff;
$pavilion_warning: rgb(243, 163, 61);
$subscription_standard: $pavilion_primary;
$subscription_business: #333;
$subscription_community: #fff;
:root {
--pavilion-primary: #{$pavilion_primary};
--pavilion-secondary: #{$pavilion_secondary};
--pavilion-warning: #{$pavilion_warning};
--subscription-standard: #{$subscription_standard};
--subscription-business: #{$subscription_business};
--subscription-community: #{$subscription_community};
}

Datei anzeigen

@ -1,14 +0,0 @@
@import "custom/base";
@import "custom/wizard";
@import "custom/header";
@import "custom/step";
@import "custom/badges";
@import "custom/buttons";
@import "custom/field";
@import "custom/validators";
@import "custom/mobile";
@import "custom/autocomplete";
@import "custom/composer";
@import "custom/events";
@import "custom/locations";
@import "custom/mentionables";

Datei anzeigen

@ -0,0 +1,14 @@
@import "wizard/base";
@import "wizard/wizard";
@import "wizard/header";
@import "wizard/step";
@import "wizard/badges";
@import "wizard/buttons";
@import "wizard/field";
@import "wizard/validators";
@import "wizard/mobile";
@import "wizard/autocomplete";
@import "wizard/composer";
@import "wizard/events";
@import "wizard/locations";
@import "wizard/mentionables";

Datei anzeigen

@ -67,6 +67,10 @@ body.custom-wizard {
margin-top: 0; margin-top: 0;
order: 2; order: 2;
} }
& > .field-label + .field-description {
margin-left: 0.5em;
}
} }
.url-field input { .url-field input {

Datei anzeigen

@ -114,8 +114,6 @@ en:
key: "Key" key: "Key"
value: "Value" value: "Value"
profile: "profile" profile: "profile"
translation: "Translation"
translation_placeholder: "key"
type: "Type" type: "Type"
none: "Make a selection" none: "Make a selection"
submission_key: 'submission key' submission_key: 'submission key'
@ -128,6 +126,9 @@ en:
select_type: "Select a type" select_type: "Select a type"
condition: "Condition" condition: "Condition"
index: "Index" index: "Index"
edit_columns: "Edit Columns"
expand_text: "Read More"
collapse_text: "Show Less"
category_settings: category_settings:
custom_wizard: custom_wizard:
title: "Custom Wizard" title: "Custom Wizard"
@ -160,11 +161,30 @@ en:
no_file: Please choose a file to import no_file: Please choose a file to import
file_size_error: The file size must be 512kb or less file_size_error: The file size must be 512kb or less
file_format_error: The file must be a .json file file_format_error: The file must be a .json file
server_error: "Error: {{message}}"
importing: Importing wizards... importing: Importing wizards...
destroying: Destroying wizards... destroying: Destroying wizards...
import_complete: Import complete import_complete: Import complete
destroy_complete: Destruction complete destroy_complete: Destruction complete
subscription:
documentation: Check out the subscription documentation
authorize: "Authorize this forum to use your Custom Wizard subscription plan on %{server}."
not_subscribed: "You've authorized, but are not currently subscribed to a Custom Wizard plan on %{server}."
subscription_expiring: "Your subscription is active, but will expire in the next 48 hours."
subscription_active: "Your subscription is active."
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."
unauthorize_failed: Failed to unauthorize.
submissions:
select: "Select a wizard to see its submissions"
viewing: "You're viewing the submissions of the %{wizardName}"
documentation: "Check out the submissions documentation"
logs:
select: "Select a wizard to see its logs"
viewing: "View recent logs for wizards on the forum"
documentation: "Check out the logs documentation"
notices:
info: "Plugin status and subscription notices"
documentation: Check out the notices documentation
editor: editor:
show: "Show" show: "Show"
@ -227,10 +247,10 @@ en:
banner: "Banner" banner: "Banner"
description: "Description" description: "Description"
required_data: required_data:
label: "Required" label: "Required Data"
not_permitted_message: "Message shown when required data not present" not_permitted_message: "Message shown when required data not present"
permitted_params: permitted_params:
label: "Params" label: "Permitted Params"
force_final: force_final:
label: "Conditional Final Step" label: "Conditional Final Step"
description: "Display this step as the final step if conditions on later steps have not passed when the user reaches this step." description: "Display this step as the final step if conditions on later steps have not passed when the user reaches this step."
@ -247,9 +267,9 @@ en:
min_length_placeholder: "Minimum length in characters" min_length_placeholder: "Minimum length in characters"
max_length: "Max Length" max_length: "Max Length"
max_length_placeholder: "Maximum length in characters" max_length_placeholder: "Maximum length in characters"
char_counter: "Character Counter" char_counter: "Counter"
char_counter_placeholder: "Display Character Counter" char_counter_placeholder: "Display Character Counter"
field_placeholder: "Field Placeholder" field_placeholder: "Placeholder"
file_types: "File Types" file_types: "File Types"
preview_template: "Template" preview_template: "Template"
limit: "Limit" limit: "Limit"
@ -261,7 +281,7 @@ en:
label: "Format" label: "Format"
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>" instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
validations: validations:
header: "Realtime Validations" header: "Validations"
enabled: "Enabled" enabled: "Enabled"
similar_topics: "Similar Topics" similar_topics: "Similar Topics"
position: "Position" position: "Position"
@ -438,6 +458,9 @@ en:
nav_label: "Submissions" nav_label: "Submissions"
title: "{{name}} Submissions" title: "{{name}} Submissions"
download: "Download" download: "Download"
group_id: "Group ID"
category_id: "Category ID"
composer_preview: "Composer Preview"
api: api:
label: "API" label: "API"
@ -491,9 +514,15 @@ en:
log: log:
label: "Logs" label: "Logs"
clear: clear
log: log:
nav_label: "Logs" nav_label: "Logs"
title: "{{name}} Logs"
date: Date
action: Action
user: User
message: Message
manager: manager:
nav_label: Manager nav_label: Manager
@ -505,6 +534,38 @@ en:
destroy: Destroy destroy: Destroy
destroyed: destroyed destroyed: destroyed
subscription:
title: Subscriber Features
subscribed:
label: Subscribed
title: You're subscribed and can use these features
selector: subscribed
not_subscribed:
label: Not Subscribed
title: Subscribe to use these features
selector: not subscribed
type:
none:
label: Not Subscribed
title: There is no Custom Wizard subscription active on this forum.
business:
label: Business
title: There is a Custom Wizard Business subscription active on this forum.
standard:
label: Standard
title: There is a Custom Wizard Standard subscription active on this forum.
community:
label: Community
title: There is a Custom Wizard Community subscription active on this forum.
cta:
none:
label: Get a Subscription
title: Get a subscription for this forum.
subscribed:
label: Support
title: Get support for your subscription.
wizard_js: wizard_js:
group: group:
select: "Select a group" select: "Select a group"

Datei anzeigen

@ -17,6 +17,7 @@ en:
name_too_short: "'%{name}' is too short for a custom field name (min length is #{min_length})" name_too_short: "'%{name}' is too short for a custom field name (min length is #{min_length})"
name_already_taken: "'%{name}' is already taken as a custom field name" name_already_taken: "'%{name}' is already taken as a custom field name"
save_default: "Failed to save custom field '%{name}'" save_default: "Failed to save custom field '%{name}'"
subscription_type: "%{type} custom fields require a subscription"
field: field:
too_short: "%{label} must be at least %{min} characters" too_short: "%{label} must be at least %{min} characters"
@ -52,9 +53,11 @@ en:
after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard." after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard."
after_time: "After time setting is invalid." after_time: "After time setting is invalid."
liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}" liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}"
subscription: "%{type} %{property} is subscription only"
site_settings: site_settings:
custom_wizard_enabled: "Enable custom wizards." custom_wizard_enabled: "Enable custom wizards."
wizard_redirect_exclude_paths: "Routes excluded from wizard redirects." wizard_redirect_exclude_paths: "Routes excluded from wizard redirects."
wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview" wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview"
wizard_apis_enabled: "Enable API features (experimental)." wizard_apis_enabled: "Enable API features (experimental)."
wizard_critical_notices_on_dashboard: "Show critical notices about the custom wizard plugin on the admin dashboard."

Datei anzeigen

@ -38,6 +38,7 @@ Discourse::Application.routes.append do
get 'admin/wizards/api/:name/authorize' => 'admin_api#authorize' get 'admin/wizards/api/:name/authorize' => 'admin_api#authorize'
get 'admin/wizards/logs' => 'admin_logs#index' get 'admin/wizards/logs' => 'admin_logs#index'
get 'admin/wizards/logs/:wizard_id' => 'admin_logs#show'
get 'admin/wizards/manager' => 'admin_manager#index' get 'admin/wizards/manager' => 'admin_manager#index'
get 'admin/wizards/manager/export' => 'admin_manager#export' get 'admin/wizards/manager/export' => 'admin_manager#export'

Datei anzeigen

@ -15,6 +15,6 @@ plugins:
refresh: true refresh: true
type: list type: list
list_type: compact list_type: compact
wizard_apis_enabled: wizard_important_notices_on_dashboard:
client: true client: true
default: false default: true

Datei anzeigen

@ -0,0 +1,89 @@
# frozen_string_literal: true
class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1]
KEY_MAP = {
wizard: "wizard_id",
action: "action",
user: "username",
date: "date",
message: "message"
}
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
key = KEY_MAP[key.to_sym] ? KEY_MAP[key.to_sym] : key
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_id', 'action', 'username')
message_prefix = ""
if prefixes.present?
message_prefix = prefixes.map do |k, v|
key = KEY_MAP.key(k) ? KEY_MAP.key(k) : k
"#{key.to_s}: #{v};"
end.join(' ')
end
if log_json.key?('message')
message = log_json['message']
message = "#{message_prefix} #{message}" if message_prefix.present?
log_json['message'] = message
else
log_json['message'] = message_prefix
end
row.value = log_json.to_json
row.save
end
end
end
end
end
end

Datei anzeigen

@ -763,14 +763,11 @@ 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
end end

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden Mehr anzeigen