1
0
Fork 0

Commits vergleichen

..

Keine gemeinsamen Commits. „main“ und „add_tests“ haben vollständig unterschiedliche Historien.

635 geänderte Dateien mit 8243 neuen und 80557 gelöschten Zeilen

Datei anzeigen

@ -1,3 +0,0 @@
3.1.999: 1f35b80f85e5fd1efb7f4851f0845700432febdc
2.7.99: e07a57e398b6b1676ab42a7e34467556fca5416b
2.5.1: bb85b3a0d2c0ab6b59bcb405731c39089ec6731c

Datei anzeigen

@ -1,3 +0,0 @@
{
"extends": "eslint-config-discourse"
}

Datei anzeigen

@ -1,13 +0,0 @@
name: Discourse Plugin
on:
push:
branches:
- main
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
ci:
uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1

Datei anzeigen

@ -1,44 +0,0 @@
name: Metadata
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout head repository
uses: actions/checkout@v2
- name: Store head version
run: |
sed -n -e 's/^.*version: /head_version=/p' plugin.rb >> $GITHUB_ENV
- name: Checkout base repository
uses: actions/checkout@v2
with:
ref: "${{ github.base_ref }}"
- name: Store base version
run: |
sed -n -e 's/^.*version: /base_version=/p' plugin.rb >> $GITHUB_ENV
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install semver
run: npm install --include=dev
- name: Check version
uses: actions/github-script@v5
with:
script: |
const semver = require('semver');
const { head_version, base_version } = process.env;
if (semver.lte(head_version, base_version)) {
core.setFailed("Head version is equal to or lower than base version.");
}

9
.gitignore gevendort
Datei anzeigen

@ -1,8 +1 @@
coverage/*
!coverage/.last_run.json
gems/*
.bundle/
auto_generated
.DS_Store
node_modules/
vendor/*
coverage

Datei anzeigen

@ -1 +0,0 @@
{}

Datei anzeigen

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

Datei anzeigen

@ -1,7 +0,0 @@
# frozen_string_literal: true
plugin = "discourse-custom-wizard"
SimpleCov.configure do
track_files "plugins/#{plugin}/**/*.rb"
add_filter { |src| !(src.filename =~ /(\/#{plugin}\/app\/|\/#{plugin}\/lib\/)/) }
end

Datei anzeigen

@ -1,4 +0,0 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};

Datei anzeigen

@ -1,10 +1,12 @@
# We want to use the KVM-based system, so require sudo
sudo: required
services:
- docker
before_install:
- git clone --depth=1 https://github.com/discourse/discourse-plugin-ci
install: true
install: true # Prevent travis doing bundle install
script:
- discourse-plugin-ci/script.sh

Datei anzeigen

@ -1,4 +1,4 @@
All code in this repository is Copyright 2023 by Angus McLeod.
All code in this repository is Copyright 2018 by Angus McLeod.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

Datei anzeigen

@ -1,7 +0,0 @@
# frozen_string_literal: true
source 'https://rubygems.org'
group :development do
gem 'rubocop-discourse'
end

Datei anzeigen

@ -1,39 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.2)
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
rainbow (3.1.1)
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.36.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.20.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.22.0)
parser (>= 3.1.1.0)
rubocop-discourse (3.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.13.2)
rubocop (~> 1.33)
ruby-progressbar (1.11.0)
unicode-display_width (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
rubocop-discourse
BUNDLED WITH
2.2.16

Datei anzeigen

@ -1,27 +1,3 @@
# Discourse Custom Wizard Plugin
# discourse-custom-wizard
The Custom Wizard Plugin lets you make forms for your Discourse forum. Better user onboarding, structured posting, data enrichment, automated actions and much more for your community.
<img src="https://camo.githubusercontent.com/593432f1fc9658ffca104065668cc88fa21dffcd3002cb78ffd50c71f33a2523/68747470733a2f2f706176696c696f6e2d6173736574732e6e7963332e63646e2e6469676974616c6f6365616e7370616365732e636f6d2f706c7567696e732f77697a6172642d7265706f7369746f72792d62616e6e65722e706e67" alt="" data-canonical-src="https://pavilion-assets.nyc3.cdn.digitaloceanspaces.com/plugins/wizard-repository-banner.png" style="max-width: 100%;" width="400">
👋 Looking to report an issue? We're managing issues for this plugin using our [bug report wizard](https://coop.pavilion.tech/w/bug-report).
## Install
If you're not sure how to install a plugin in Discourse, please follow the [plugin installation guide](https://meta.discourse.org/t/install-a-plugin/19157) or contact your Discourse hosting provider.
## Documentation
[Read the full documentation here](https://coop.pavilion.tech/c/82), or go directly to the relevant section
- [Wizard Administration](https://coop.pavilion.tech/t/1602)
- [Wizard Settings](https://coop.pavilion.tech/t/1614)
- [Step Settings](https://coop.pavilion.tech/t/1735)
- [Field Settings](https://coop.pavilion.tech/t/1580)
- [Conditional Settings](https://coop.pavilion.tech/t/1673)
- [Field Interpolation](https://coop.pavilion.tech/t/1557)
- [Handling Dates and Times](https://coop.pavilion.tech/t/1708)
## Support
- [Report an issue](https://coop.pavilion.tech/w/bug-report)
See further: https://meta.discourse.org/t/custom-wizard-plugin/73345

Datei anzeigen

@ -1,7 +0,0 @@
# Security Policy
The security of Discourse plugins are premised on the security of [Discourse](https://github.com/discourse/discourse). Please first consider whether a security issue is best reported and handled by the Discourse team. You can view the Discourse security policy [here](https://github.com/discourse/discourse/security/policy).
## Reporting a Vulnerability
If you find a security vulnerability that is specific to this plugin, please report it to development@pavilion.tech. Security issues always take precedence over all other work. All commits specific to security are prefixed with SECURITY.

Datei anzeigen

@ -1,30 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminController < ::Admin::AdminController
before_action :ensure_admin
def index
subcription = CustomWizard::Subscription.new
render_json_dump(
subscribed: subcription.subscribed?,
subscription_type: subcription.type,
subscription_attributes: CustomWizard::Subscription.attributes,
subscription_client_installed: CustomWizard::Subscription.client_installed?
)
end
private
def find_wizard
params.require(:wizard_id)
@wizard = CustomWizard::Wizard.create(params[:wizard_id].underscore)
raise Discourse::InvalidParameters.new(:wizard_id) unless @wizard
end
def custom_field_list
serialize_data(CustomWizard::CustomField.full_list, CustomWizard::CustomFieldSerializer)
end
def render_error(message)
render json: failed_json.merge(error: message)
end
end

Datei anzeigen

@ -1,66 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController
def index
render_json_dump(
custom_fields: custom_field_list
)
end
def update
errors = []
field_id = nil
field_data = {}
if saved_field = CustomWizard::CustomField.find(field_params[:id].to_i)
CustomWizard::CustomField::ATTRS.each do |attr|
field_data[attr] = saved_field.send(attr)
end
field_id = saved_field.id
end
CustomWizard::CustomField::ATTRS.each do |attr|
field_data[attr] = field_params[attr]
end
field = CustomWizard::CustomField.new(field_id, field_data)
PluginStoreRow.transaction do
unless field.save
field_errors = field.errors.any? ?
field.errors.full_messages.join("\n\n") :
I18n.t("wizard.custom_field.error.save_default", name: field.name)
errors << field_errors
raise ActiveRecord::Rollback.new
end
end
if errors.any?
render json: failed_json.merge(messages: errors)
else
render json: success_json
end
end
def destroy
params.require(:name)
if CustomWizard::CustomField.destroy(params[:name])
render json: success_json
else
render json: failed_json
end
end
private
def field_params
params.required(:custom_field)
.permit(
:id,
:name,
:klass,
:type,
serializers: []
)
end
end

Datei anzeigen

@ -1,44 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminLogsController < CustomWizard::AdminController
before_action :find_wizard, except: [:index]
def index
render json: ActiveModel::ArraySerializer.new(
CustomWizard::Wizard.list(current_user),
each_serializer: CustomWizard::BasicWizardSerializer
)
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

Datei anzeigen

@ -1,125 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminManagerController < CustomWizard::AdminController
skip_before_action :check_xhr, only: [:export]
before_action :get_wizard_ids, except: [:import]
def export
templates = []
@wizard_ids.each do |wizard_id|
if template = CustomWizard::Template.find(wizard_id)
templates.push(template)
end
end
if templates.empty?
return render_error(I18n.t('wizard.export.error.invalid_wizards'))
end
basename = SiteSetting.title.parameterize || 'discourse'
time = Time.now.to_i
filename = "#{basename}-wizards-#{time}.json"
send_data templates.to_json,
type: "application/json",
disposition: 'attachment',
filename: filename
end
def import
file = File.read(params['file'].tempfile)
if file.nil?
return render_error(I18n.t('wizard.export.error.no_file'))
end
file_size = file.size
max_file_size = 512 * 1024
if max_file_size < file_size
return render_error(I18n.t('wizard.import.error.file_large'))
end
begin
template_json = JSON.parse(file)
rescue JSON::ParserError
return render_error(I18n.t('wizard.import.error.invalid_json'))
end
imported = []
failures = []
templates = template_json.is_a?(Array) ? template_json : [template_json]
templates.each do |raw_template|
template = CustomWizard::Template.new(raw_template)
template.save(skip_jobs: true, create: true)
if template.errors.any?
failures.push(
id: template.data['id'],
messages: template.errors.full_messages.join(', ')
)
else
imported.push(
id: template.data['id'],
name: template.data['name']
)
end
end
render json: success_json.merge(
imported: imported,
failures: failures
)
end
def destroy
destroyed = []
failures = []
@wizard_ids.each do |wizard_id|
template = CustomWizard::Template.find(wizard_id)
if template && CustomWizard::Template.remove(wizard_id)
destroyed.push(
id: wizard_id,
name: template['name']
)
else
failures.push(
id: wizard_id,
messages: I18n.t("wizard.destroy.error.#{template ? 'default' : 'no_template'}")
)
end
end
render json: success_json.merge(
destroyed: destroyed,
failures: failures
)
end
private
def get_wizard_ids
if params['wizard_ids'].blank?
return render_error(I18n.t('wizard.export.error.select_one'))
end
wizard_ids = []
params['wizard_ids'].each do |wizard_id|
begin
wizard_ids.push(wizard_id.underscore)
rescue
#
end
end
if wizard_ids.empty?
return render_error(I18n.t('wizard.export.error.invalid_wizards'))
end
@wizard_ids = wizard_ids
end
end

Datei anzeigen

@ -1,41 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController
skip_before_action :preload_json, :check_xhr, only: [:download]
before_action :find_wizard, except: [:index]
def index
render json: ActiveModel::ArraySerializer.new(
CustomWizard::Wizard.list(current_user),
each_serializer: CustomWizard::BasicWizardSerializer
)
end
def show
render_json_dump(
wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false),
submissions: ActiveModel::ArraySerializer.new(
submission_list.submissions,
each_serializer: CustomWizard::SubmissionSerializer
),
total: submission_list.total
)
end
def download
content = ActiveModel::ArraySerializer.new(
CustomWizard::Submission.list(@wizard).submissions,
each_serializer: CustomWizard::SubmissionSerializer
)
send_data content.to_json,
filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json",
content_type: "application/json",
disposition: "attachment"
end
protected
def submission_list
CustomWizard::Submission.list(@wizard, page: params[:page].to_i)
end
end

Datei anzeigen

@ -1,173 +0,0 @@
# frozen_string_literal: true
class CustomWizard::AdminWizardController < CustomWizard::AdminController
before_action :find_wizard, only: [:show, :remove]
def index
render_json_dump(
wizard_list: ActiveModel::ArraySerializer.new(
CustomWizard::Wizard.list(current_user),
each_serializer: CustomWizard::BasicWizardSerializer
),
field_types: CustomWizard::Field.types,
realtime_validations: CustomWizard::RealtimeValidation.types,
custom_fields: custom_field_list
)
end
def show
params.require(:wizard_id)
if data = CustomWizard::Template.find(params[:wizard_id].underscore)
render json: data.as_json
else
render json: { none: true }
end
end
def remove
if CustomWizard::Template.remove(@wizard.id)
render json: success_json
else
render json: failed_json
end
end
def save
template = CustomWizard::Template.new(save_wizard_params.to_h)
wizard_id = template.save(create: params[:create])
if template.errors.any?
render json: failed_json.merge(backend_validation_error: template.errors.full_messages.join("\n\n"))
else
render json: success_json.merge(wizard_id: wizard_id)
end
end
private
def mapped_params
[
:type,
:connector,
:output,
:output_type,
:output_connector,
pairs: [
:index,
:key,
:key_type,
:value,
:value_type,
:connector,
value: [],
key: [],
],
output: [],
]
end
def save_wizard_params
params.require(:wizard).permit(
:id,
:name,
:background,
:save_submissions,
:multiple_submissions,
:after_signup,
:after_time,
:after_time_scheduled,
:required,
:prompt_completion,
:restart_on_revisit,
:resume_on_revisit,
:theme_id,
permitted: mapped_params,
steps: [
:id,
:index,
:title,
:key,
:banner,
:banner_upload_id,
:raw_description,
:required_data_message,
:force_final,
required_data: mapped_params,
permitted_params: mapped_params,
condition: mapped_params,
fields: [
:id,
:index,
:label,
:image,
:image_upload_id,
:description,
:required,
:key,
:type,
:min_length,
:max_length,
:char_counter,
:file_types,
:format,
:limit,
:property,
:preview_template,
:placeholder,
:can_create_tag,
prefill: mapped_params,
content: mapped_params,
condition: mapped_params,
index: mapped_params,
validations: {},
tag_groups: [],
]
],
actions: [
:id,
:run_after,
:type,
:code,
:skip_redirect,
:suppress_notifications,
:post,
:post_builder,
:post_template,
:notification_level,
:api,
:api_endpoint,
:api_body,
:wizard_user,
title: mapped_params,
category: mapped_params,
tags: mapped_params,
custom_fields: mapped_params,
visible: mapped_params,
required: mapped_params,
recipient: mapped_params,
categories: mapped_params,
mute_remainder: mapped_params,
profile_updates: mapped_params,
group: mapped_params,
url: mapped_params,
name: mapped_params,
slug: mapped_params,
color: mapped_params,
text_color: mapped_params,
parent_category_id: mapped_params,
permissions: mapped_params,
full_name: mapped_params,
bio_raw: mapped_params,
usernames: mapped_params,
owner_usernames: mapped_params,
grant_trust_level: mapped_params,
mentionable_level: mapped_params,
messageable_level: mapped_params,
visibility_level: mapped_params,
members_visibility_level: mapped_params,
add_event: mapped_params,
add_location: mapped_params
]
)
end
end

Datei anzeigen

@ -1,18 +0,0 @@
# frozen_string_literal: true
class CustomWizard::RealtimeValidationsController < ::ApplicationController
def validate
klass_str = "CustomWizard::RealtimeValidation::#{validation_params[:type].camelize}"
result = klass_str.constantize.new(current_user).perform(validation_params)
render_serialized(result.items, "#{klass_str}Serializer".constantize, result.serializer_opts)
end
private
def validation_params
params.require(:type)
settings = ::CustomWizard::RealtimeValidation.types[params[:type].to_sym]
params.require(settings[:required_params]) if settings[:required_params].present?
params
end
end

Datei anzeigen

@ -1,113 +0,0 @@
# frozen_string_literal: true
class CustomWizard::StepsController < ::CustomWizard::WizardClientController
before_action :ensure_can_update
def update
update = update_params.to_h
update[:fields] = {}
if params[:fields]
field_ids = @builder.wizard.field_ids
params[:fields].each do |k, v|
update[:fields][k] = v if field_ids.include? k
end
end
@builder.build
updater = @builder.wizard.create_updater(update[:step_id], update[:fields])
updater.update
@result = updater.result
if updater.success?
wizard_id = update_params[:wizard_id]
builder = CustomWizard::Builder.new(wizard_id, current_user, guest_id)
@wizard = builder.build(force: true)
current_step = @wizard.find_step(update[:step_id])
current_submission = @wizard.current_submission
result = {}
if current_step.conditional_final_step && !current_step.last_step
current_step.force_final = true
end
if current_step.final?
builder.template.actions.each do |action_template|
if action_template['run_after'] === 'wizard_completion'
action_result = CustomWizard::Action.new(
action: action_template,
wizard: @wizard,
submission: current_submission
).perform
if action_result.success?
current_submission = action_result.submission
end
end
end
current_submission.save
if redirect = get_redirect
updater.result[:redirect_on_complete] = redirect
end
@wizard.cleanup_on_complete!
result[:final] = true
else
current_submission.save
result[:final] = false
result[:next_step_id] = current_step.next.id
end
result.merge!(updater.result) if updater.result.present?
result[:refresh_required] = true if updater.refresh_required?
result[:wizard] = ::CustomWizard::WizardSerializer.new(
@wizard,
scope: Guardian.new(current_user),
root: false
).as_json
render json: result
else
errors = []
updater.errors.messages.each do |field, msg|
errors << { field: field, description: msg.join(',') }
end
render json: { errors: errors }, status: 422
end
end
private
def ensure_can_update
raise Discourse::InvalidParameters.new(:wizard_id) if @builder.template.nil?
raise Discourse::InvalidAccess.new if !@builder.wizard || !@builder.wizard.can_access?
@step_template = @builder.template.steps.select do |s|
s['id'] == update_params[:step_id]
end.first
raise Discourse::InvalidParameters.new(:step_id) if !@step_template
raise Discourse::InvalidAccess.new if !@builder.check_condition(@step_template)
end
def update_params
@update_params || begin
params.require(:step_id)
params.require(:wizard_id)
params.permit(:wizard_id, :step_id).transform_values { |v| v.underscore }
end
end
def get_redirect
return @result[:redirect_on_next] if @result[:redirect_on_next].present?
submission = @wizard.current_submission
return nil unless submission.present?
## route_to set by actions, redirect_on_complete set by actions, redirect_to set at wizard entry
submission.route_to || submission.redirect_on_complete || submission.redirect_to
end
end

Datei anzeigen

@ -1,39 +0,0 @@
# frozen_string_literal: true
class CustomWizard::WizardController < ::CustomWizard::WizardClientController
def show
if wizard.present?
render json: CustomWizard::WizardSerializer.new(wizard, scope: guardian, root: false).as_json, status: 200
else
render json: { error: I18n.t('wizard.none') }
end
end
def skip
params.require(:wizard_id)
if wizard.required && !wizard.completed? && wizard.permitted?
return render json: { error: I18n.t('wizard.no_skip') }
end
result = { success: 'OK' }
if current_user && wizard.can_access?
if redirect_to = wizard.current_submission&.redirect_to
result.merge!(redirect_to: redirect_to)
end
wizard.cleanup_on_skip!
end
render json: result
end
protected
def wizard
@wizard ||= begin
return nil unless @builder.present?
@builder.build({ reset: params[:reset] }, params)
end
end
end

Datei anzeigen

@ -1,23 +0,0 @@
# frozen_string_literal: true
class CustomWizard::WizardClientController < ::ApplicationController
before_action :ensure_plugin_enabled
before_action :set_builder
private
def ensure_plugin_enabled
unless SiteSetting.custom_wizard_enabled
redirect_to path("/")
end
end
def guest_id
return nil if current_user.present?
cookies[:custom_wizard_guest_id] ||= CustomWizard::Wizard.generate_guest_id
cookies[:custom_wizard_guest_id]
end
def set_builder
@builder = CustomWizard::Builder.new(params[:wizard_id].underscore, current_user, guest_id)
end
end

Datei anzeigen

@ -1,24 +0,0 @@
# frozen_string_literal: true
module Jobs
class SetAfterTimeWizard < ::Jobs::Base
def execute(args)
if SiteSetting.custom_wizard_enabled
wizard = CustomWizard::Wizard.create(args[:wizard_id])
if wizard && wizard.after_time
user_ids = []
User.human_users.each do |user|
if CustomWizard::Wizard.set_user_redirect(wizard.id, user)
user_ids.push(user.id)
end
end
CustomWizard::Template.clear_cache_keys
MessageBus.publish "/redirect_to_wizard", wizard.id, user_ids: user_ids
end
end
end
end
end

Datei anzeigen

@ -1,4 +0,0 @@
# frozen_string_literal: true
class CustomWizard::BasicWizardSerializer < ::ApplicationSerializer
attributes :id, :name
end

Datei anzeigen

@ -1,4 +0,0 @@
# frozen_string_literal: true
class CustomWizard::CustomFieldSerializer < ApplicationSerializer
attributes :id, :klass, :name, :type, :serializers
end

Datei anzeigen

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

Datei anzeigen

@ -1,3 +0,0 @@
# frozen_string_literal: true
class ::CustomWizard::RealtimeValidation::SimilarTopicsSerializer < ::SimilarTopicSerializer
end

Datei anzeigen

@ -1,35 +0,0 @@
# frozen_string_literal: true
class CustomWizard::SubmissionSerializer < ApplicationSerializer
attributes :id,
:fields,
:submitted_at,
:user
def include_user?
object.wizard.user.present?
end
def user
::BasicUserSerializer.new(object.wizard.user, root: false).as_json
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

Datei anzeigen

@ -1,138 +0,0 @@
# frozen_string_literal: true
class CustomWizard::FieldSerializer < ::ApplicationSerializer
attributes :id,
:index,
:type,
:required,
:value,
:label,
:placeholder,
:description,
:image,
:file_types,
:format,
:limit,
:property,
:content,
:tag_groups,
:can_create_tag,
:validations,
:max_length,
:char_counter,
:preview_template
def id
object.id
end
def index
object.index
end
def type
object.type
end
def required
object.required
end
def value
object.value
end
def label
I18n.t("#{i18n_key}.label", default: object.label, base_url: Discourse.base_url)
end
def include_label?
label.present?
end
def description
I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url)
end
def include_description?
description.present?
end
def placeholder
I18n.t("#{i18n_key}.placeholder", default: object.placeholder)
end
def include_placeholder?
placeholder.present?
end
def image
object.image
end
def include_image?
object.image.present?
end
def file_types
object.file_types
end
def format
object.format
end
def limit
object.limit
end
def property
object.property
end
def content
object.content
end
def tag_groups
object.tag_groups
end
def can_create_tag
object.can_create_tag
end
def validations
validations = {}
object.validations&.each do |type, props|
next unless props["status"]
validations[props["position"]] ||= {}
validations[props["position"]][type] = props.merge CustomWizard::RealtimeValidation.types[type.to_sym]
end
validations
end
def max_length
object.max_length
end
def char_counter
object.char_counter
end
def preview_template
object.preview_template
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

Datei anzeigen

@ -1,46 +0,0 @@
# frozen_string_literal: true
class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer
attributes :start,
:background,
:submission_last_updated_at,
:theme_id,
:completed,
:required,
:permitted,
:resume_on_revisit
has_many :steps, serializer: ::CustomWizard::StepSerializer, embed: :objects
has_one :user, serializer: ::BasicUserSerializer, embed: :objects
def completed
object.completed?
end
def include_completed?
object.completed? &&
(!object.respond_to?(:multiple_submissions) || !object.multiple_submissions) &&
!scope.is_admin?
end
def permitted
object.permitted?
end
def start
object.start
end
def include_start?
include_steps? && object.start.present?
end
def submission_last_updated_at
object.current_submission.updated_at
end
def include_steps?
!include_completed?
end
end

Datei anzeigen

@ -1,83 +0,0 @@
# frozen_string_literal: true
class CustomWizard::StepSerializer < ::ApplicationSerializer
attributes :id,
:index,
:next,
:previous,
:description,
:title,
:banner,
:permitted,
:permitted_message,
:final
has_many :fields, serializer: ::CustomWizard::FieldSerializer, embed: :objects
def id
object.id
end
def index
object.index
end
def next
object.next.id if object.next.present?
end
def include_next?
object.next.present?
end
def previous
object.previous.id if object.previous.present?
end
def include_previous?
object.previous.present?
end
def title
I18n.t("#{i18n_key}.title", default: object.title, base_url: Discourse.base_url)
end
def include_title?
title.present?
end
def description
I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url)
end
def include_description?
description.present?
end
def banner
object.banner
end
def include_banner?
object.banner.present?
end
def permitted
object.permitted
end
def permitted_message
object.permitted_message
end
def final
object.final?
end
protected
def i18n_key
@i18n_key ||= "#{object.wizard.id}.#{object.id}".underscore
end
end

Datei anzeigen

@ -1,28 +0,0 @@
<!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

@ -1,126 +0,0 @@
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { alias, equal, or } from "@ember/object/computed";
import I18n from "I18n";
export default Component.extend({
tagName: "tr",
topicSerializers: ["topic_view", "topic_list_item"],
postSerializers: ["post"],
groupSerializers: ["basic_group"],
categorySerializers: ["basic_category"],
showInputs: or("field.new", "field.edit"),
classNames: ["custom-field-input"],
loading: or("saving", "destroying"),
destroyDisabled: alias("loading"),
closeDisabled: alias("loading"),
isExternal: equal("field.id", "external"),
didInsertElement() {
this.set("originalField", JSON.parse(JSON.stringify(this.field)));
},
@discourseComputed("field.klass")
serializerContent(klass) {
const serializers = this.get(`${klass}Serializers`);
if (serializers) {
return serializers.reduce((result, key) => {
result.push({
id: key,
name: I18n.t(`admin.wizard.custom_field.serializers.${key}`),
});
return result;
}, []);
}
},
@observes("field.klass")
clearSerializersWhenClassChanges() {
this.set("field.serializers", null);
},
compareArrays(array1, array2) {
return (
array1.length === array2.length &&
array1.every((value, index) => {
return value === array2[index];
})
);
},
@discourseComputed(
"saving",
"isExternal",
"field.name",
"field.klass",
"field.type",
"field.serializers"
)
saveDisabled(saving, isExternal) {
if (saving || isExternal) {
return true;
}
const originalField = this.originalField;
if (!originalField) {
return false;
}
return ["name", "klass", "type", "serializers"].every((attr) => {
let current = this.get(attr);
let original = originalField[attr];
if (!current) {
return false;
}
if (attr === "serializers") {
return this.compareArrays(current, original);
} else {
return current === original;
}
});
},
actions: {
edit() {
this.set("field.edit", true);
},
close() {
if (this.field.edit) {
this.set("field.edit", false);
}
},
destroy() {
this.set("destroying", true);
this.removeField(this.field);
},
save() {
this.set("saving", true);
const field = this.field;
let data = {
id: field.id,
klass: field.klass,
type: field.type,
serializers: field.serializers,
name: field.name,
};
this.saveField(data).then((result) => {
this.set("saving", false);
if (result.success) {
this.set("field.edit", false);
} else {
this.set("saveIcon", "times");
}
setTimeout(() => this.set("saveIcon", null), 10000);
});
},
},
});

Datei anzeigen

@ -1,146 +0,0 @@
import {
default as computed,
observes,
} from "discourse-common/utils/decorators";
import { renderAvatar } from "discourse/helpers/user-avatar";
import userSearch from "discourse/lib/user-search";
import I18n from "I18n";
import Handlebars from "handlebars";
import { isEmpty } from "@ember/utils";
import TextField from "discourse/components/text-field";
const template = function (params) {
const options = params.options;
let html = "<div class='autocomplete'>";
if (options.users) {
html += "<ul>";
options.users.forEach((u) => {
html += `<li><a href title="${u.name}">`;
html += renderAvatar(u, { imageSize: "tiny" });
html += `<span class='username'>${u.username}</span>`;
if (u.name) {
html += `<span class='name'>${u.name}</span>`;
}
html += `</a></li>`;
});
html += "</ul>";
}
html += "</div>";
return new Handlebars.SafeString(html).string;
};
export default TextField.extend({
attributeBindings: ["autofocus", "maxLength"],
autocorrect: false,
autocapitalize: false,
name: "user-selector",
id: "custom-member-selector",
@computed("placeholderKey")
placeholder(placeholderKey) {
return placeholderKey ? I18n.t(placeholderKey) : "";
},
@observes("usernames")
_update() {
if (this.get("canReceiveUpdates") === "true") {
this.didInsertElement({ updateData: true });
}
},
didInsertElement(opts) {
this._super();
let self = this,
selected = [],
groups = [],
includeMentionableGroups =
this.get("includeMentionableGroups") === "true",
includeMessageableGroups =
this.get("includeMessageableGroups") === "true",
includeGroups = this.get("includeGroups") === "true",
allowedUsers = this.get("allowedUsers") === "true";
function excludedUsernames() {
// hack works around some issues with allowAny eventing
const usernames = self.get("single") ? [] : selected;
return usernames;
}
$(this.element)
.val(this.get("usernames"))
.autocomplete({
template,
disabled: this.get("disabled"),
single: this.get("single"),
allowAny: this.get("allowAny"),
updateData: opts && opts.updateData ? opts.updateData : false,
dataSource(term) {
const termRegex = /[^a-zA-Z0-9_\-\.@\+]/;
let results = userSearch({
term: term.replace(termRegex, ""),
topicId: self.get("topicId"),
exclude: excludedUsernames(),
includeGroups,
allowedUsers,
includeMentionableGroups,
includeMessageableGroups,
});
return results;
},
transformComplete(v) {
if (v.username || v.name) {
if (!v.username) {
groups.push(v.name);
}
return v.username || v.name;
} else {
let excludes = excludedUsernames();
return v.usernames.filter(function (item) {
return excludes.indexOf(item) === -1;
});
}
},
onChangeItems(items) {
let hasGroups = false;
items = items.map(function (i) {
if (groups.indexOf(i) > -1) {
hasGroups = true;
}
return i.username ? i.username : i;
});
self.set("usernames", items.join(","));
self.set("hasGroups", hasGroups);
selected = items;
if (self.get("onChangeCallback")) {
self.sendAction("onChangeCallback");
}
},
reverseTransform(i) {
return { username: i };
},
});
},
willDestroyElement() {
this._super();
$(this.element).autocomplete("destroy");
},
// THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT
@observes("usernames")
_clearInput: function () {
if (arguments.length > 1) {
if (isEmpty(this.get("usernames"))) {
$(this.element).parent().find("a").click();
}
}
},
});

Datei anzeigen

@ -1,18 +0,0 @@
import CategorySelector from "select-kit/components/category-selector";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers";
export default CategorySelector.extend({
classNames: ["category-selector", "wizard-category-selector"],
content: computed(
"categories.[]",
"blacklist.[]",
"whitelist.[]",
function () {
return this._super().filter((category) => {
const whitelist = makeArray(this.whitelist);
return !whitelist.length || whitelist.indexOf(category.id) > -1;
});
}
),
});

Datei anzeigen

@ -1,213 +0,0 @@
import ComposerEditor from "discourse/components/composer-editor";
import {
bind,
default as discourseComputed,
on,
} from "discourse-common/utils/decorators";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { scheduleOnce } from "@ember/runloop";
import { caretPosition, inCodeBlock } from "discourse/lib/utilities";
import highlightSyntax from "discourse/lib/highlight-syntax";
import { alias } from "@ember/object/computed";
import Site from "discourse/models/site";
import { uploadIcon } from "discourse/lib/uploads";
import { dasherize } from "@ember/string";
import InsertHyperlink from "discourse/components/modal/insert-hyperlink";
import { inject as service } from "@ember/service";
const IMAGE_MARKDOWN_REGEX =
/!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g;
export default ComposerEditor.extend({
modal: service(),
classNameBindings: ["fieldClass"],
allowUpload: true,
showLink: false,
topic: null,
showToolbar: true,
focusTarget: "reply",
canWhisper: false,
lastValidatedAt: "lastValidatedAt",
popupMenuOptions: [],
draftStatus: "null",
replyPlaceholder: alias("field.translatedPlaceholder"),
wizardEventFieldId: null,
composerEventPrefix: "wizard-editor",
@on("didInsertElement")
_composerEditorInit() {
const $input = $(this.element.querySelector(".d-editor-input"));
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
dataSource: (term) => this._userSearchTerm.call(this, term),
key: "@",
transformComplete: (v) => v.username || v.name,
afterComplete: (value) => {
this.composer.set("reply", value);
scheduleOnce("afterRender", () => $input.blur().focus());
},
triggerRule: (textarea) =>
!inCodeBlock(textarea.value, caretPosition(textarea)),
});
}
const siteSettings = this.siteSettings;
if (siteSettings.mentionables_enabled) {
Site.currentProp("mentionable_items", this.wizard.mentionable_items);
const { SEPARATOR } = requirejs(
"discourse/plugins/discourse-mentionables/discourse/lib/discourse-markdown/mentionable-items"
);
const { searchMentionableItem } = requirejs(
"discourse/plugins/discourse-mentionables/discourse/lib/mentionable-item-search"
);
$input.autocomplete({
template: findRawTemplate("javascripts/mentionable-item-autocomplete"),
key: SEPARATOR,
afterComplete: (value) => {
this.composer.set("reply", value);
scheduleOnce("afterRender", () => $input.blur().focus());
},
transformComplete: (item) => item.model.slug,
dataSource: (term) =>
term.match(/\s/) ? null : searchMentionableItem(term, siteSettings),
triggerRule: (textarea) =>
!inCodeBlock(textarea.value, caretPosition(textarea)),
});
}
$input.on("scroll", this._throttledSyncEditorAndPreviewScroll);
this._bindUploadTarget();
const field = this.field;
this.editorInputClass = `.${dasherize(field.type)}-${dasherize(
field.id
)} .d-editor-input`;
this._uppyInstance.on("file-added", () => {
this.session.set("wizardEventFieldId", field.id);
});
},
@discourseComputed("field.id")
fileUploadElementId(fieldId) {
return `file-uploader-${dasherize(fieldId)}`;
},
@discourseComputed
allowedFileTypes() {
return this.siteSettings.authorized_extensions
.split("|")
.map((ext) => "." + ext)
.join(",");
},
@discourseComputed()
uploadIcon() {
return uploadIcon(false, this.siteSettings);
},
@bind
_handleImageDeleteButtonClick(event) {
if (!event.target.classList.contains("delete-image-button")) {
return;
}
const index = parseInt(
event.target.closest(".button-wrapper").dataset.imageIndex,
10
);
const matchingPlaceholder =
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
this.session.set("wizardEventFieldId", this.field.id);
this.appEvents.trigger(
"composer:replace-text",
matchingPlaceholder[index],
"",
{ regex: IMAGE_MARKDOWN_REGEX, index }
);
},
actions: {
extraButtons(toolbar) {
const component = this;
if (this.allowUpload && this.uploadIcon) {
toolbar.addButton({
id: "upload",
group: "insertions",
icon: this.uploadIcon,
title: "upload",
sendAction: (event) => component.send("showUploadModal", event),
});
}
toolbar.addButton({
id: "link",
group: "insertions",
shortcut: "K",
trimLeading: true,
unshift: true,
sendAction: (event) => component.send("showLinkModal", event),
});
if (this.siteSettings.mentionables_enabled) {
const { SEPARATOR } = requirejs(
"discourse/plugins/discourse-mentionables/discourse/lib/discourse-markdown/mentionable-items"
);
toolbar.addButton({
id: "insert-mentionable",
group: "extras",
icon: this.siteSettings.mentionables_composer_button_icon,
title: "mentionables.composer.insert.title",
perform: () => {
this.appEvents.trigger("wizard-editor:insert-text", {
fieldId: this.field.id,
text: SEPARATOR,
});
const $textarea = $(
document.querySelector(
`.composer-field.${this.field.id} textarea.d-editor-input`
)
);
$textarea.trigger("keyup.autocomplete");
},
});
}
},
previewUpdated(preview) {
highlightSyntax(preview, this.siteSettings, this.session);
if (this.siteSettings.mentionables_enabled) {
const { linkSeenMentionableItems } = requirejs(
"discourse/plugins/discourse-mentionables/discourse/lib/mentionable-items-preview-styling"
);
linkSeenMentionableItems(preview, this.siteSettings);
}
this._super(...arguments);
},
showLinkModal(toolbarEvent) {
let linkText = "";
this._lastSel = toolbarEvent.selected;
if (this._lastSel) {
linkText = this._lastSel.value;
}
this.modal.show(InsertHyperlink, {
model: { linkText, toolbarEvent },
});
},
showUploadModal() {
this.session.set("wizardEventFieldId", this.field.id);
document.getElementById(this.fileUploadElementId).click();
},
},
});

Datei anzeigen

@ -1,17 +0,0 @@
import DateInput from "discourse/components/date-input";
import discourseComputed from "discourse-common/utils/decorators";
export default DateInput.extend({
useNativePicker: false,
classNameBindings: ["fieldClass"],
@discourseComputed()
placeholder() {
return this.format;
},
_opts() {
return {
format: this.format || "LL",
};
},
});

Datei anzeigen

@ -1,16 +0,0 @@
import DateTimeInput from "discourse/components/date-time-input";
import discourseComputed from "discourse-common/utils/decorators";
export default DateTimeInput.extend({
classNameBindings: ["fieldClass"],
@discourseComputed("timeFirst", "tabindex")
timeTabindex(timeFirst, tabindex) {
return timeFirst ? tabindex : tabindex + 1;
},
@discourseComputed("timeFirst", "tabindex")
dateTabindex(timeFirst, tabindex) {
return timeFirst ? tabindex + 1 : tabindex;
},
});

Datei anzeigen

@ -1,42 +0,0 @@
import { observes } from "discourse-common/utils/decorators";
import Category from "discourse/models/category";
import Component from "@ember/component";
export default Component.extend({
didInsertElement() {
const property = this.field.property || "id";
const value = this.field.value;
if (value) {
this.set(
"categories",
[...value].reduce((result, v) => {
let val =
property === "id" ? Category.findById(v) : Category.findBySlug(v);
if (val) {
result.push(val);
}
return result;
}, [])
);
}
},
@observes("categories")
setValue() {
const categories = (this.categories || []).filter((c) => !!c);
const property = this.field.property || "id";
if (categories.length) {
this.set(
"field.value",
categories.reduce((result, c) => {
if (c && c[property]) {
result.push(c[property]);
}
return result;
}, [])
);
}
},
});

Datei anzeigen

@ -1,3 +0,0 @@
import Component from "@ember/component";
export default Component.extend({});

Datei anzeigen

@ -1,49 +0,0 @@
import Component from "@ember/component";
import { loadOneboxes } from "discourse/lib/load-oneboxes";
import { schedule } from "@ember/runloop";
import discourseDebounce from "discourse-common/lib/debounce";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
import { ajax } from "discourse/lib/ajax";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
@on("init")
updatePreview() {
if (this.isDestroyed) {
return;
}
schedule("afterRender", () => {
if (this._state !== "inDOM" || !this.element) {
return;
}
const $preview = $(this.element);
if ($preview.length === 0) {
return;
}
this.previewUpdated($preview);
});
},
previewUpdated($preview) {
// Paint oneboxes
const paintFunc = () => {
loadOneboxes(
$preview[0],
ajax,
null,
null,
this.siteSettings.max_oneboxes_per_post,
true // refresh on every load
);
};
discourseDebounce(this, paintFunc, 450);
// Short upload urls need resolution
resolveAllShortUrls(ajax, this.siteSettings, $preview[0]);
},
});

Datei anzeigen

@ -1,48 +0,0 @@
import {
default as computed,
observes,
} from "discourse-common/utils/decorators";
import EmberObject from "@ember/object";
import Component from "@ember/component";
export default Component.extend({
showPreview: false,
classNameBindings: [
":wizard-field-composer",
"showPreview:show-preview:hide-preview",
],
didInsertElement() {
this.set(
"composer",
EmberObject.create({
loading: false,
reply: this.get("field.value") || "",
})
);
},
@observes("composer.reply")
setField() {
this.set("field.value", this.get("composer.reply"));
},
@computed("showPreview")
togglePreviewLabel(showPreview) {
return showPreview
? "wizard_composer.hide_preview"
: "wizard_composer.show_preview";
},
actions: {
togglePreview() {
this.toggleProperty("showPreview");
},
groupsMentioned() {},
afterRefresh() {},
cannotSeeMention() {},
importQuote() {},
showUploadSelector() {},
},
});

Datei anzeigen

@ -1,15 +0,0 @@
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
@observes("dateTime")
setValue() {
this.set("field.value", this.dateTime.format(this.field.format));
},
actions: {
onChange(value) {
this.set("dateTime", moment(value));
},
},
});

Datei anzeigen

@ -1,15 +0,0 @@
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
@observes("date")
setValue() {
this.set("field.value", this.date.format(this.field.format));
},
actions: {
onChange(value) {
this.set("date", moment(value));
},
},
});

Datei anzeigen

@ -1,13 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
keyPress(e) {
e.stopPropagation();
},
actions: {
onChangeValue(value) {
this.set("field.value", value);
},
},
});

Datei anzeigen

@ -1,3 +0,0 @@
import Component from "@ember/component";
export default Component.extend({});

Datei anzeigen

@ -1,3 +0,0 @@
import Component from "@ember/component";
export default Component.extend({});

Datei anzeigen

@ -1,3 +0,0 @@
import Component from "@ember/component";
export default Component.extend({});

Datei anzeigen

@ -1,7 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
keyPress(e) {
e.stopPropagation();
},
});

Datei anzeigen

@ -1,7 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
keyPress(e) {
e.stopPropagation();
},
});

Datei anzeigen

@ -1,23 +0,0 @@
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: ["fieldClass"],
@observes("time")
setValue() {
this.set("field.value", this.time.format(this.field.format));
},
actions: {
onChange(value) {
this.set(
"time",
moment({
hours: value.hours,
minutes: value.minutes,
})
);
},
},
});

Datei anzeigen

@ -1,27 +0,0 @@
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import Component from "@ember/component";
import { computed } from "@ember/object";
export default Component.extend(UppyUploadMixin, {
classNames: ["wizard-field-upload"],
classNameBindings: ["isImage", "fieldClass"],
uploading: false,
type: computed(function () {
return `wizard_${this.field.id}`;
}),
id: computed(function () {
return `wizard_field_upload_${this.field.id}`;
}),
isImage: computed("field.value.extension", function () {
return (
this.field.value &&
this.siteSettings.wizard_recognised_image_upload_formats
.split("|")
.includes(this.field.value.extension)
);
}),
uploadDone(upload) {
this.set("field.value", upload);
},
});

Datei anzeigen

@ -1,3 +0,0 @@
import Component from "@ember/component";
export default Component.extend({});

Datei anzeigen

@ -1,5 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
classNameBindings: ["fieldClass"],
});

Datei anzeigen

@ -1,41 +0,0 @@
import Component from "@ember/component";
import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
import { cookAsync } from "discourse/lib/text";
export default Component.extend({
classNameBindings: [
":wizard-field",
"typeClasses",
"field.invalid",
"field.id",
],
didReceiveAttrs() {
this._super(...arguments);
cookAsync(this.field.translatedDescription).then((cookedDescription) => {
this.set("cookedDescription", cookedDescription);
});
},
@discourseComputed("field.type", "field.id")
typeClasses: (type, id) =>
`${dasherize(type)}-field ${dasherize(type)}-${dasherize(id)}`,
@discourseComputed("field.id")
fieldClass: (id) => `field-${dasherize(id)} wizard-focusable`,
@discourseComputed("field.type", "field.id")
inputComponentName(type, id) {
if (["text_only"].includes(type)) {
return false;
}
return dasherize(type === "component" ? id : `custom-wizard-field-${type}`);
},
@discourseComputed("field.type")
textType(fieldType) {
return ["text", "textarea"].includes(fieldType);
},
});

Datei anzeigen

@ -1,19 +0,0 @@
import ComboBox from "select-kit/components/combo-box";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers";
export default ComboBox.extend({
content: computed("groups.[]", "field.content.[]", function () {
const whitelist = makeArray(this.field.content);
return this.groups
.filter((group) => {
return !whitelist.length || whitelist.indexOf(group.id) > -1;
})
.map((g) => {
return {
id: g.id,
name: g.full_name ? g.full_name : g.name,
};
});
}),
});

Datei anzeigen

@ -1,29 +0,0 @@
import CustomWizard from "../models/custom-wizard";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { dasherize } from "@ember/string";
import getURL from "discourse-common/lib/get-url";
export default Component.extend({
classNameBindings: [":wizard-no-access", "reasonClass"],
@discourseComputed("reason")
reasonClass(reason) {
return dasherize(reason);
},
@discourseComputed
siteName() {
return this.siteSettings.title || "";
},
actions: {
skip() {
if (this.currentUser) {
CustomWizard.skip(this.get("wizardId"));
} else {
window.location = getURL("/");
}
},
},
});

Datei anzeigen

@ -1,38 +0,0 @@
import Component from "@ember/component";
import { bind } from "@ember/runloop";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["wizard-similar-topics"],
showTopics: true,
didInsertElement() {
$(document).on("click", bind(this, this.documentClick));
},
willDestroyElement() {
$(document).off("click", bind(this, this.documentClick));
},
documentClick(e) {
if (this._state === "destroying") {
return;
}
let $target = $(e.target);
if (!$target.hasClass("show-topics")) {
this.set("showTopics", false);
}
},
@observes("topics")
toggleShowWhenTopicsChange() {
this.set("showTopics", true);
},
actions: {
toggleShowTopics() {
this.set("showTopics", true);
},
},
});

Datei anzeigen

@ -1,9 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [":wizard-step-form", "customStepClass"],
@discourseComputed("step.id")
customStepClass: (stepId) => `wizard-step-${stepId}`,
});

Datei anzeigen

@ -1,227 +0,0 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import getUrl from "discourse-common/lib/get-url";
import { htmlSafe } from "@ember/template";
import { schedule } from "@ember/runloop";
import { cookAsync } from "discourse/lib/text";
import CustomWizard, {
updateCachedWizard,
} from "discourse/plugins/discourse-custom-wizard/discourse/models/custom-wizard";
import { alias, not } from "@ember/object/computed";
import discourseLater from "discourse-common/lib/later";
const alreadyWarned = {};
export default Component.extend({
classNameBindings: [":wizard-step", "step.id"],
saving: null,
init() {
this._super(...arguments);
this.set("stylingDropdown", {});
},
didReceiveAttrs() {
this._super(...arguments);
cookAsync(this.step.translatedTitle).then((cookedTitle) => {
this.set("cookedTitle", cookedTitle);
});
cookAsync(this.step.translatedDescription).then((cookedDescription) => {
this.set("cookedDescription", cookedDescription);
});
},
didInsertElement() {
this._super(...arguments);
this.autoFocus();
},
@discourseComputed("step.index", "wizard.required")
showQuitButton: (index, required) => index === 0 && !required,
showNextButton: not("step.final"),
showDoneButton: alias("step.final"),
@discourseComputed(
"step.index",
"step.displayIndex",
"wizard.totalSteps",
"wizard.completed"
)
showFinishButton: (index, displayIndex, total, completed) => {
return index !== 0 && displayIndex !== total && completed;
},
@discourseComputed("step.index")
showBackButton: (index) => index > 0,
@discourseComputed("step.banner")
bannerImage(src) {
if (!src) {
return;
}
return getUrl(src);
},
@discourseComputed("step.id")
bannerAndDescriptionClass(id) {
return `wizard-banner-and-description wizard-banner-and-description-${id}`;
},
@discourseComputed("step.fields.[]")
primaryButtonIndex(fields) {
return fields.length + 1;
},
@discourseComputed("step.fields.[]")
secondaryButtonIndex(fields) {
return fields.length + 2;
},
@observes("step.id")
_stepChanged() {
this.set("saving", false);
this.autoFocus();
},
@observes("step.message")
_handleMessage: function () {
const message = this.get("step.message");
this.showMessage(message);
},
@discourseComputed("step.index", "wizard.totalSteps")
barStyle(displayIndex, totalSteps) {
let ratio = parseFloat(displayIndex) / parseFloat(totalSteps - 1);
if (ratio < 0) {
ratio = 0;
}
if (ratio > 1) {
ratio = 1;
}
return htmlSafe(`width: ${ratio * 200}px`);
},
@discourseComputed("step.fields")
includeSidebar(fields) {
return !!fields.findBy("show_in_sidebar");
},
autoFocus() {
discourseLater(() => {
schedule("afterRender", () => {
if ($(".invalid .wizard-focusable").length) {
this.animateInvalidFields();
}
});
});
},
animateInvalidFields() {
schedule("afterRender", () => {
let $invalid = $(".invalid .wizard-focusable");
if ($invalid.length) {
$([document.documentElement, document.body]).animate(
{
scrollTop: $invalid.offset().top - 200,
},
400
);
}
});
},
advance() {
this.set("saving", true);
this.get("step")
.save()
.then((response) => {
updateCachedWizard(CustomWizard.build(response["wizard"]));
if (response["final"]) {
CustomWizard.finished(response);
} else {
this.goNext(response);
}
})
.catch(() => this.animateInvalidFields())
.finally(() => this.set("saving", false));
},
actions: {
quit() {
this.get("wizard").skip();
},
done() {
this.send("nextStep");
},
showMessage(message) {
this.sendAction(message);
},
stylingDropdownChanged(id, value) {
this.set("stylingDropdown", { id, value });
},
exitEarly() {
const step = this.step;
step.validate();
if (step.get("valid")) {
this.set("saving", true);
step
.save()
.then(() => this.send("quit"))
.finally(() => this.set("saving", false));
} else {
this.autoFocus();
}
},
backStep() {
if (this.saving) {
return;
}
this.goBack();
},
nextStep() {
if (this.saving) {
return;
}
const step = this.step;
const result = step.validate();
if (result.warnings.length) {
const unwarned = result.warnings.filter((w) => !alreadyWarned[w]);
if (unwarned.length) {
unwarned.forEach((w) => (alreadyWarned[w] = true));
return window.bootbox.confirm(
unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.advance();
}
}
);
}
}
if (step.get("valid")) {
this.advance();
} else {
this.autoFocus();
}
},
},
});

Datei anzeigen

@ -1,15 +0,0 @@
import TagChooser from "select-kit/components/tag-chooser";
export default TagChooser.extend({
searchTags(url, data, callback) {
if (this.tagGroups) {
let tagGroupsString = this.tagGroups.join(",");
data.filterForInput = {
name: "custom-wizard-tag-chooser",
groups: tagGroupsString,
};
}
return this._super(url, data, callback);
},
});

Datei anzeigen

@ -1,11 +0,0 @@
import TagChooser from "select-kit/components/tag-chooser";
import { makeArray } from "discourse-common/lib/helpers";
export default TagChooser.extend({
_transformJson(context, json) {
return this._super(context, json).filter((tag) => {
const whitelist = makeArray(context.whitelist);
return !whitelist.length || whitelist.indexOf(tag.id) > 1;
});
},
});

Datei anzeigen

@ -1,44 +0,0 @@
import computed from "discourse-common/utils/decorators";
import { isLTR, isRTL, siteDir } from "discourse/lib/text-direction";
import I18n from "I18n";
import TextField from "discourse/components/text-field";
export default TextField.extend({
attributeBindings: [
"autocorrect",
"autocapitalize",
"autofocus",
"maxLength",
"dir",
],
@computed
dir() {
if (this.siteSettings.support_mixed_text_direction) {
let val = this.value;
if (val) {
return isRTL(val) ? "rtl" : "ltr";
} else {
return siteDir();
}
}
},
keyUp() {
if (this.siteSettings.support_mixed_text_direction) {
let val = this.value;
if (isRTL(val)) {
this.set("dir", "rtl");
} else if (isLTR(val)) {
this.set("dir", "ltr");
} else {
this.set("dir", siteDir());
}
}
},
@computed("placeholderKey")
placeholder(placeholderKey) {
return placeholderKey ? I18n.t(placeholderKey) : "";
},
});

Datei anzeigen

@ -1,3 +0,0 @@
import TimeInput from "discourse/components/time-input";
export default TimeInput.extend({});

Datei anzeigen

@ -1,9 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
actions: {
perform() {
this.appEvents.trigger("custom-wizard:validate");
},
},
});

Datei anzeigen

@ -1,34 +0,0 @@
<DModal @closeModal={{@closeModal}} @title={{this.title}}>
{{#if loading}}
<LoadingSpinner 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}}
<div class="modal-footer">
<DButton
class="btn-primary"
@label="directory.edit_columns.save"
@action={{action "save"}}
/>
<DButton
class="btn-secondary reset-to-default"
@label="directory.edit_columns.reset_to_default"
@action={{action "resetToDefault"}}
/>
</div>
</DModal>

Datei anzeigen

@ -1,15 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import I18n from "I18n";
export default class AdminWizardsColumnComponent extends Component {
title = I18n.t("admin.wizard.edit_columns");
@action save() {
this.args.closeModal();
}
@action resetToDefault() {
this.args.model.reset();
}
}

Datei anzeigen

@ -1,20 +0,0 @@
<DModal
@closeModal={{@closeModal}}
class="next-session-time-modal"
@title={{this.title}}
>
<DateTimeInput
@date={{this.bufferedDateTime}}
@onChange={{action "dateTimeChanged"}}
@showTime="true"
@clearable="true"
/>
<div class="modal-footer">
<DButton
@action={{action "submit"}}
class="btn-primary"
@label="admin.wizard.after_time_modal.done"
@disabled={{this.submitDisabled}}
/>
</div>
</DModal>

Datei anzeigen

@ -1,30 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import I18n from "I18n";
export default class NextSessionScheduledComponent extends Component {
@tracked bufferedDateTime;
title = I18n.t("admin.wizard.after_time_modal.title");
constructor() {
super(...arguments);
this.bufferedDateTime = this.args.model.dateTime
? moment(this.args.model.dateTime)
: moment(Date.now());
}
get submitDisabled() {
return moment().isAfter(this.bufferedDateTime);
}
@action submit() {
const dateTime = this.bufferedDateTime;
this.args.model.update(moment(dateTime).utc().toISOString());
this.args.closeModal();
}
@action dateTimeChanged(dateTime) {
this.bufferedDateTime = dateTime;
}
}

Datei anzeigen

@ -1,160 +0,0 @@
import WizardFieldValidator from "discourse/plugins/discourse-custom-wizard/discourse/components/validator";
import { deepMerge } from "discourse-common/lib/object";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { cancel, later } from "@ember/runloop";
import { A } from "@ember/array";
import EmberObject, { computed } from "@ember/object";
import { and, equal, notEmpty } from "@ember/object/computed";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import { dasherize } from "@ember/string";
export default WizardFieldValidator.extend({
classNames: ["similar-topics-validator"],
similarTopics: null,
hasInput: notEmpty("field.value"),
hasSimilarTopics: notEmpty("similarTopics"),
hasNotSearched: equal("similarTopics", null),
noSimilarTopics: computed("similarTopics", function () {
return this.similarTopics !== null && this.similarTopics.length === 0;
}),
showSimilarTopics: computed("typing", "hasSimilarTopics", function () {
return this.hasSimilarTopics && !this.typing;
}),
showNoSimilarTopics: computed("typing", "noSimilarTopics", function () {
return this.noSimilarTopics && !this.typing;
}),
hasValidationCategories: notEmpty("validationCategories"),
insufficientCharacters: computed("typing", "field.value", function () {
return this.hasInput && this.field.value.length < 5 && !this.typing;
}),
insufficientCharactersCategories: and(
"insufficientCharacters",
"hasValidationCategories"
),
@discourseComputed("validation.categories")
validationCategories(categoryIds) {
if (categoryIds) {
return categoryIds.map((id) => this.site.categoriesById[id]);
}
return A();
},
@discourseComputed("validationCategories")
catLinks(categories) {
return categories.map((category) => categoryBadgeHTML(category)).join("");
},
@discourseComputed(
"loading",
"showSimilarTopics",
"showNoSimilarTopics",
"insufficientCharacters",
"insufficientCharactersCategories"
)
currentState(
loading,
showSimilarTopics,
showNoSimilarTopics,
insufficientCharacters,
insufficientCharactersCategories
) {
switch (true) {
case loading:
return "loading";
case showSimilarTopics:
return "results";
case showNoSimilarTopics:
return "no_results";
case insufficientCharactersCategories:
return "insufficient_characters_categories";
case insufficientCharacters:
return "insufficient_characters";
default:
return false;
}
},
@discourseComputed("currentState")
currentStateClass(currentState) {
if (currentState) {
return `similar-topics-${dasherize(currentState)}`;
}
return "similar-topics";
},
@discourseComputed("currentState")
currentStateKey(currentState) {
if (currentState) {
return `realtime_validations.similar_topics.${currentState}`;
}
return false;
},
validate() {},
@observes("field.value")
customValidate() {
const field = this.field;
if (!field.value) {
this.set("similarTopics", null);
return;
}
const value = field.value;
this.set("typing", true);
const lastKeyUp = new Date();
this._lastKeyUp = lastKeyUp;
// One second from now, check to see if the last key was hit when
// we recorded it. If it was, the user paused typing.
cancel(this._lastKeyTimeout);
this._lastKeyTimeout = later(() => {
if (lastKeyUp !== this._lastKeyUp) {
return;
}
this.set("typing", false);
if (value && value.length < 5) {
this.set("similarTopics", null);
return;
}
this.updateSimilarTopics();
}, 1000);
},
updateSimilarTopics() {
this.set("similarTopics", null);
this.set("updating", true);
this.backendValidate({
title: this.get("field.value"),
categories: this.get("validation.categories"),
time_unit: this.get("validation.time_unit"),
time_n_value: this.get("validation.time_n_value"),
})
.then((result) => {
const similarTopics = A(
deepMerge(result["topics"], result["similar_topics"])
);
similarTopics.forEach(function (topic, index) {
similarTopics[index] = EmberObject.create(topic);
});
this.set("similarTopics", similarTopics);
})
.finally(() => this.set("updating", false));
},
actions: {
closeMessage() {
this.set("showMessage", false);
},
},
});

Datei anzeigen

@ -1,42 +0,0 @@
import Component from "@ember/component";
import { equal } from "@ember/object/computed";
import { ajax, getToken } from "discourse/lib/ajax";
export default Component.extend({
classNames: ["validator"],
classNameBindings: ["isValid", "isInvalid"],
validMessageKey: null,
invalidMessageKey: null,
isValid: null,
isInvalid: equal("isValid", false),
init() {
this._super(...arguments);
if (this.get("validation.backend")) {
// set a function that can be called as often as it need to
// from the derived component
this.backendValidate = (params) => {
return ajax("/realtime-validations", {
data: {
type: this.get("type"),
authenticity_token: getToken(),
...params,
},
});
};
}
},
didInsertElement() {
this.appEvents.on("custom-wizard:validate", this, this.checkIsValid);
},
willDestroyElement() {
this.appEvents.off("custom-wizard:validate", this, this.checkIsValid);
},
checkIsValid() {
this.set("isValid", this.validate());
},
});

Datei anzeigen

@ -1,104 +1,104 @@
import { default as discourseComputed } from "discourse-common/utils/decorators";
import { empty, equal, or } from "@ember/object/computed";
import { notificationLevels, selectKitContent } from "../lib/wizard";
import { computed } from "@ember/object";
import UndoChanges from "../mixins/undo-changes";
import Component from "@ember/component";
import I18n from "I18n";
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
export default Component.extend(UndoChanges, {
componentType: "action",
classNameBindings: [":wizard-custom-action", "visible"],
visible: computed("currentActionId", function () {
return this.action.id === this.currentActionId;
}),
createTopic: equal("action.type", "create_topic"),
updateProfile: equal("action.type", "update_profile"),
watchCategories: equal("action.type", "watch_categories"),
watchTags: equal("action.type", "watch_tags"),
sendMessage: equal("action.type", "send_message"),
openComposer: equal("action.type", "open_composer"),
sendToApi: equal("action.type", "send_to_api"),
addToGroup: equal("action.type", "add_to_group"),
routeTo: equal("action.type", "route_to"),
createCategory: equal("action.type", "create_category"),
createGroup: equal("action.type", "create_group"),
apiEmpty: empty("action.api"),
groupPropertyTypes: selectKitContent(["id", "name"]),
hasCustomFields: or(
"basicTopicFields",
"updateProfile",
"createGroup",
"createCategory"
),
basicTopicFields: or("createTopic", "sendMessage", "openComposer"),
publicTopicFields: or("createTopic", "openComposer"),
showPostAdvanced: or("createTopic", "sendMessage"),
availableNotificationLevels: notificationLevels.map((type) => {
return {
id: type,
name: I18n.t(`admin.wizard.action.watch_x.notification_level.${type}`),
};
}),
const ACTION_TYPES = [
{ id: 'create_topic', name: 'Create Topic' },
{ id: 'update_profile', name: 'Update Profile' },
{ id: 'send_message', name: 'Send Message' },
{ id: 'send_to_api', name: 'Send to API' },
{ id: 'add_to_group', name: 'Add to Group' },
{ id: 'route_to', name: 'Route To' },
{ id: 'open_composer', name: 'Open Composer' }
];
messageUrl: "https://discourse.pluginmanager.org/t/action-settings",
const PROFILE_FIELDS = [
'name',
'user_avatar',
'date_of_birth',
'title',
'locale',
'location',
'website',
'bio_raw',
'profile_background',
'card_background',
'theme_id'
];
@discourseComputed("action.type")
messageKey(type) {
let key = "type";
if (type) {
key = "edit";
}
return key;
export default Ember.Component.extend({
classNames: 'wizard-custom-action',
types: ACTION_TYPES,
profileFields: PROFILE_FIELDS,
createTopic: Ember.computed.equal('action.type', 'create_topic'),
updateProfile: Ember.computed.equal('action.type', 'update_profile'),
sendMessage: Ember.computed.equal('action.type', 'send_message'),
sendToApi: Ember.computed.equal('action.type', 'send_to_api'),
apiEmpty: Ember.computed.empty('action.api'),
addToGroup: Ember.computed.equal('action.type', 'add_to_group'),
routeTo: Ember.computed.equal('action.type', 'route_to'),
disableId: Ember.computed.not('action.isNew'),
@computed('action.type')
basicTopicFields(actionType) {
return ['create_topic', 'send_message', 'open_composer'].indexOf(actionType) > -1;
},
@discourseComputed("action.type")
customFieldsContext(type) {
return `action.${type}`;
@computed('action.type')
publicTopicFields(actionType) {
return ['create_topic', 'open_composer'].indexOf(actionType) > -1;
},
@discourseComputed("wizard.steps")
runAfterContent(steps) {
let content = steps.map(function (step) {
return {
id: step.id,
name: step.title || step.id,
};
});
content.unshift({
id: "wizard_completion",
name: I18n.t("admin.wizard.action.run_after.wizard_completion"),
});
return content;
@computed('action.type')
newTopicFields(actionType) {
return ['create_topic', 'send_message'].indexOf(actionType) > -1;
},
@discourseComputed("apis")
@computed('availableFields')
builderWizardFields(fields) {
return fields.map((f) => ` w{${f.id}}`);
},
@computed('availableFields')
categoryFields(fields) {
return fields.filter(f => f.type == 'category');
},
@computed('availableFields')
tagFields(fields) {
return fields.filter(f => f.type == 'tag');
},
@computed()
builderUserFields() {
const noTheme = PROFILE_FIELDS.filter((f) => f !== 'theme_id');
const fields = noTheme.concat(['email', 'username']);
return fields.map((f) => ` u{${f}}`);
},
@observes('action.custom_category_wizard_field')
toggleCustomCategoryUserField() {
const wizard = this.get('action.custom_category_wizard_field');
if (wizard) this.set('action.custom_category_user_field', false);
},
@observes('action.custom_category_user_field')
toggleCustomCategoryWizardField() {
const user = this.get('action.custom_category_user_field');
if (user) this.set('action.custom_category_wizard_field', false);
},
@computed('wizard.apis')
availableApis(apis) {
return apis.map((a) => {
return apis.map(a => {
return {
id: a.name,
name: a.title,
name: a.title
};
});
},
@discourseComputed("apis", "action.api")
@computed('wizard.apis', 'action.api')
availableEndpoints(apis, api) {
if (!api) {
return [];
if (!api) return [];
return apis.find(a => a.name === api).endpoints;
}
return apis.find((a) => a.name === api).endpoints;
},
@discourseComputed("fieldTypes")
hasEventField(fieldTypes) {
return fieldTypes.map((ft) => ft.id).includes("event");
},
@discourseComputed("fieldTypes")
hasLocationField(fieldTypes) {
return fieldTypes.map((ft) => ft.id).includes("location");
},
});

Datei anzeigen

@ -1,159 +1,46 @@
import { default as discourseComputed } from "discourse-common/utils/decorators";
import { equal, or } from "@ember/object/computed";
import { computed } from "@ember/object";
import { selectKitContent } from "../lib/wizard";
import UndoChanges from "../mixins/undo-changes";
import Component from "@ember/component";
import wizardSchema from "../lib/wizard-schema";
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
export default Component.extend(UndoChanges, {
componentType: "field",
classNameBindings: [":wizard-custom-field", "visible"],
visible: computed("currentFieldId", function () {
return this.field.id === this.currentFieldId;
}),
isDropdown: equal("field.type", "dropdown"),
isUpload: equal("field.type", "upload"),
isCategory: equal("field.type", "category"),
isGroup: equal("field.type", "group"),
isTag: equal("field.type", "tag"),
isText: equal("field.type", "text"),
isTextarea: equal("field.type", "textarea"),
isUrl: equal("field.type", "url"),
isComposer: equal("field.type", "composer"),
showPrefill: or("isText", "isCategory", "isTag", "isGroup", "isDropdown"),
showContent: or("isCategory", "isTag", "isGroup", "isDropdown"),
showLimit: or("isCategory", "isTag"),
isTextType: or("isText", "isTextarea", "isComposer"),
isComposerPreview: equal("field.type", "composer_preview"),
categoryPropertyTypes: selectKitContent(["id", "slug"]),
messageUrl: "https://discourse.pluginmanager.org/t/field-settings",
export default Ember.Component.extend({
classNames: 'wizard-custom-field',
isDropdown: Ember.computed.equal('field.type', 'dropdown'),
isUpload: Ember.computed.equal('field.type', 'upload'),
isCategory: Ember.computed.equal('field.type', 'category'),
disableId: Ember.computed.not('field.isNew'),
choicesTypes: ['translation', 'preset', 'custom'],
choicesTranslation: Ember.computed.equal('field.choices_type', 'translation'),
choicesPreset: Ember.computed.equal('field.choices_type', 'preset'),
choicesCustom: Ember.computed.equal('field.choices_type', 'custom'),
categoryPropertyTypes: ['id', 'slug'],
@discourseComputed("field.type")
validations(type) {
const applicableToField = [];
@computed('field.type')
isInput: (type) => type === 'text' || type === 'textarea',
for (let validation in wizardSchema.field.validations) {
if (wizardSchema.field.validations[validation]["types"].includes(type)) {
applicableToField.push(validation);
@computed('field.type')
isCategoryOrTag: (type) => type === 'tag' || type === 'category',
@computed()
presetChoices() {
let presets = [
{
id: 'categories',
name: I18n.t('admin.wizard.field.choices_preset.categories')
},{
id: 'groups',
name: I18n.t('admin.wizard.field.choices_preset.groups')
},{
id: 'tags',
name: I18n.t('admin.wizard.field.choices_preset.tags')
}
];
return presets;
},
@on('didInsertElement')
@observes('isUpload')
setupFileType() {
if (this.get('isUpload') && !this.get('field.file_types')) {
this.set('field.file_types', '.jpg,.png');
}
}
return applicableToField;
},
@discourseComputed("field.type")
isDateTime(type) {
return ["date_time", "date", "time"].indexOf(type) > -1;
},
@discourseComputed("field.type")
messageKey(type) {
let key = "type";
if (type) {
key = "edit";
}
return key;
},
setupTypeOutput(fieldType, options) {
const selectionType = {
category: "category",
tag: "tag",
group: "group",
}[fieldType];
if (selectionType) {
options[`${selectionType}Selection`] = "output";
options.outputDefaultSelection = selectionType;
}
return options;
},
@discourseComputed("field.type")
contentOptions(fieldType) {
let options = {
wizardFieldSelection: true,
textSelection: "key,value",
userFieldSelection: "key,value",
context: "field",
};
options = this.setupTypeOutput(fieldType, options);
if (this.isDropdown) {
options.wizardFieldSelection = "key,value";
options.userFieldOptionsSelection = "output";
options.textSelection = "key,value";
options.inputTypes = "association,conditional,assignment";
options.pairConnector = "association";
options.keyPlaceholder = "admin.wizard.key";
options.valuePlaceholder = "admin.wizard.value";
}
return options;
},
@discourseComputed("field.type")
prefillOptions(fieldType) {
let options = {
wizardFieldSelection: true,
textSelection: true,
userFieldSelection: "key,value",
context: "field",
};
return this.setupTypeOutput(fieldType, options);
},
@discourseComputed("step.index")
fieldConditionOptions(stepIndex) {
const options = {
inputTypes: "validation",
context: "field",
textSelection: "value",
userFieldSelection: true,
groupSelection: true,
};
if (stepIndex > 0) {
options.wizardFieldSelection = true;
options.wizardActionSelection = true;
}
return options;
},
@discourseComputed("step.index")
fieldIndexOptions(stepIndex) {
const options = {
context: "field",
userFieldSelection: true,
groupSelection: true,
};
if (stepIndex > 0) {
options.wizardFieldSelection = true;
options.wizardActionSelection = true;
}
return options;
},
actions: {
imageUploadDone(upload) {
this.setProperties({
"field.image": upload.url,
"field.image_upload_id": upload.id,
});
},
imageUploadDeleted() {
this.setProperties({
"field.image": null,
"field.image_upload_id": null,
});
},
},
});

Datei anzeigen

@ -0,0 +1,34 @@
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { getOwner } from 'discourse-common/lib/get-owner';
export default Ember.Component.extend({
classNames: 'custom-input',
noneKey: 'admin.wizard.select_field',
noneValue: 'admin.wizard.none',
connectorNone: 'admin.wizard.none',
inputKey: 'admin.wizard.key',
customDisabled: Ember.computed.alias('input.user_field'),
@computed('input.value_custom', 'input.user_field')
valueDisabled(custom, user) {
return Boolean(custom || user);
},
@on('init')
setupUserFields() {
const allowUserField = this.get('allowUserField');
if (allowUserField) {
const store = getOwner(this).lookup('store:main');
store.findAll('user-field').then((result) => {
if (result && result.content && result.content.length) {
this.set('userFields', result.content.map((f) => {
return {
id: `user_field_${f.id}`,
name: f.name
};
}));
}
});
}
}
});

Datei anzeigen

@ -0,0 +1,17 @@
export default Ember.Component.extend({
classNames: 'custom-inputs',
valuePlaceholder: 'admin.wizard.value',
actions: {
add() {
if (!this.get('inputs')) {
this.set('inputs', Ember.A());
}
this.get('inputs').pushObject(Ember.Object.create());
},
remove(input) {
this.get('inputs').removeObject(input);
}
}
});

Datei anzeigen

@ -1,40 +1,76 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { observes, default as computed } from 'ember-addons/ember-computed-decorators';
export default Component.extend({
classNames: "wizard-custom-step",
export default Ember.Component.extend({
classNames: 'wizard-custom-step',
currentField: null,
currentAction: null,
disableId: Ember.computed.not('step.isNew'),
@discourseComputed("step.index")
stepConditionOptions(stepIndex) {
const options = {
inputTypes: "validation",
context: "step",
textSelection: "value",
userFieldSelection: true,
groupSelection: true,
};
@observes('step')
resetCurrentObjects() {
const fields = this.get('step.fields');
const actions = this.get('step.actions');
this.setProperties({
currentField: fields.length ? fields[0] : null,
currentAction: actions.length ? actions[0] : null
});
},
if (stepIndex > 0) {
options["wizardFieldSelection"] = true;
options["wizardActionSelection"] = true;
@computed('availableFields', 'wizard.steps')
requiredContent(availableFields, steps) {
let content = availableFields;
let actions = [];
steps.forEach(s => {
actions.push(...s.actions);
});
actions.forEach(a => {
if (a.type === 'route_to' && a.code) {
content.push(Ember.Object.create({
id: a.code,
label: "code (Route To)"
}));
}
});
return content;
},
@computed
requiredConnectorContent() {
const label = (id) => I18n.t(`admin.wizard.step.required_data.connector.${id}`);
return [
{
id: 'equals',
label: label('equals')
}
];
},
@computed('step.id', 'wizard.save_submissions')
availableFields(currentStepId, saveSubmissions) {
const allSteps = this.get('wizard.steps');
let steps = allSteps;
let fields = [];
if (!saveSubmissions) {
steps = [allSteps.findBy('id', currentStepId)];
}
return options;
},
actions: {
bannerUploadDone(upload) {
this.setProperties({
"step.banner": upload.url,
"step.banner_upload_id": upload.id,
steps.forEach((s) => {
if (s.fields && s.fields.length > 0) {
let stepFields = s.fields.map((f) => {
return Ember.Object.create({
id: f.id,
label: `${f.id} (${s.id})`,
type: f.type
});
},
bannerUploadDeleted() {
this.setProperties({
"step.banner": null,
"step.banner_upload_id": null,
});
},
fields.push(...stepFields);
}
});
return fields;
},
});

Datei anzeigen

@ -0,0 +1,42 @@
export default Ember.Component.extend({
classNames: ['container', 'export'],
selected: Ember.A(),
actions: {
checkChanged(event) {
this.set('exportMessage', '');
let selected = this.get('selected');
if (event.target.checked) {
selected.addObject(event.target.id);
} else if (!event.target.checked) {
selected.removeObject(event.target.id);
}
this.set('selected', selected);
},
export() {
const wizards = this.get('selected');
if (!wizards.length) {
this.set('exportMessage', I18n.t("admin.wizard.transfer.export.none_selected"));
} else {
this.set('exportMessage', '');
let url = Discourse.BaseUrl;
let route = '/admin/wizards/transfer/export';
url += route + '?';
wizards.forEach((wizard) => {
let step = 'wizards[]=' + wizard;
step += '&';
url += step;
});
location.href = url;
}
}
}
});

Datei anzeigen

@ -0,0 +1,81 @@
import { ajax } from 'discourse/lib/ajax';
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: ['container', 'import'],
hasLogs: Ember.computed.notEmpty('logs'),
@computed('successIds', 'failureIds')
logs(successIds, failureIds) {
let logs = [];
if (successIds) {
logs.push(...successIds.map(id => {
return { id, type: 'success' };
}));
}
if (failureIds) {
logs.push(...failureIds.map(id => {
return { id, type: 'failure' };
}));
}
return logs;
},
actions: {
setFilePath(event) {
this.set('importMessage', '');
// 512 kb is the max file size
let maxFileSize = 512 * 1024;
if (event.target.files[0] === undefined) {
this.set('filePath', null);
return;
}
if (maxFileSize < event.target.files[0].size) {
this.setProperties({
importMessage: I18n.t('admin.wizard.transfer.import.file_size_error'),
filePath: null
});
$('#file-url').val('');
} else {
this.set('filePath', event.target.files[0]);
}
},
import() {
const filePath = this.get('filePath');
let $formData = new FormData();
if (filePath) {
$formData.append('file', filePath);
ajax('/admin/wizards/transfer/import', {
type: 'POST',
data: $formData,
processData: false,
contentType: false,
}).then(result => {
if (result.error) {
this.set('importMessage', result.error);
} else {
this.setProperties({
successIds: result.success,
failureIds: result.failed,
fileName: $('#file-url')[0].files[0].name
});
}
this.set('filePath', null);
$('#file-url').val('');
});
} else {
this.set('importMessage', I18n.t("admin.wizard.transfer.import.no_file"));
}
}
}
});

Datei anzeigen

@ -1,150 +1,86 @@
import discourseComputed from "discourse-common/utils/decorators";
import { generateName } from "../lib/wizard";
import {
setWizardDefaults,
default as wizardSchema,
} from "../lib/wizard-schema";
import { notEmpty } from "@ember/object/computed";
import EmberObject from "@ember/object";
import Component from "@ember/component";
import { A } from "@ember/array";
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
export default Component.extend({
classNameBindings: [":wizard-links", "itemType"],
items: A(),
anyLinks: notEmpty("links"),
export default Ember.Component.extend({
classNames: 'wizard-links',
items: Ember.A(),
updateItemOrder(itemId, newIndex) {
const items = this.items;
const item = items.findBy("id", itemId);
items.removeObject(item);
item.set("index", newIndex);
items.insertAt(newIndex, item);
@on('didInsertElement')
@observes('links.@each')
didInsertElement() {
Ember.run.scheduleOnce('afterRender', () => {
this.applySortable();
});
},
@discourseComputed("itemType")
header: (itemType) => `admin.wizard.${itemType}.header`,
applySortable() {
this.$("ul").sortable({tolerance: 'pointer'}).on('sortupdate', (e, ui) => {
const itemId = ui.item.data('id');
const index = ui.item.index();
Ember.run.bind(this, this.updateItemOrder(itemId, index));
});
},
@discourseComputed(
"current",
"items.@each.id",
"items.@each.type",
"items.@each.label",
"items.@each.title"
)
links(current, items) {
if (!items) {
return;
}
updateItemOrder(itemId, newIndex) {
const items = this.get('items');
const item = items.findBy('id', itemId);
items.removeObject(item);
items.insertAt(newIndex, item);
Ember.run.scheduleOnce('afterRender', this, () => this.applySortable());
},
return items.map((item, index) => {
@computed('type')
header: (type) => `admin.wizard.${type}.header`,
@computed('items.@each.id', 'current')
links(items, current) {
if (!items) return;
return items.map((item) => {
if (item) {
let link = {
id: item.id,
const id = item.get('id');
const type = this.get('type');
const label = type === 'action' ? id : (item.get('label') || item.get('title') || id);
let link = { id, label };
let classes = 'btn';
if (current && item.get('id') === current.get('id')) {
classes += ' btn-primary';
};
let label = item.label || item.title || item.id;
if (this.generateLabels && item.type) {
label = generateName(item.type);
}
link.label = `${label} (${item.id})`;
let classes = "btn";
if (current && item.id === current.id) {
classes += " btn-primary";
}
link.classes = classes;
link.index = index;
if (index === 0) {
link.first = true;
}
if (index === items.length - 1) {
link.last = true;
}
link['classes'] = classes;
return link;
}
});
},
getNextIndex() {
const items = this.items;
if (!items || items.length === 0) {
return 0;
}
const numbers = items
.map((i) => Number(i.id.split("_").pop()))
.sort((a, b) => a - b);
return numbers[numbers.length - 1];
},
actions: {
add() {
const items = this.get("items");
const itemType = this.itemType;
let params = setWizardDefaults({}, itemType);
const items = this.get('items');
const type = this.get('type');
const newId = `${type}_${items.length + 1}`;
let params = { id: newId, isNew: true };
params.isNew = true;
params.index = this.getNextIndex();
if (type === 'step') {
params['fields'] = Ember.A();
params['actions'] = Ember.A();
};
let id = `${itemType}_${params.index + 1}`;
if (itemType === "field") {
id = `${this.parentId}_${id}`;
}
params.id = id;
let objectArrays = wizardSchema[itemType].objectArrays;
if (objectArrays) {
Object.keys(objectArrays).forEach((objectType) => {
params[objectArrays[objectType].property] = A();
});
}
const newItem = EmberObject.create(params);
const newItem = Ember.Object.create(params);
items.pushObject(newItem);
this.set("current", newItem);
},
back(item) {
this.updateItemOrder(item.id, item.index - 1);
},
forward(item) {
this.updateItemOrder(item.id, item.index + 1);
this.set('current', newItem);
this.sendAction('isNew');
},
change(itemId) {
this.set("current", this.items.findBy("id", itemId));
const items = this.get('items');
this.set('current', items.findBy('id', itemId));
},
remove(itemId) {
const items = this.items;
let item;
let index;
items.forEach((it, ind) => {
if (it.id === itemId) {
item = it;
index = ind;
const items = this.get('items');
items.removeObject(items.findBy('id', itemId));
this.set('current', items[items.length - 1]);
}
});
let nextIndex;
if (this.current.id === itemId) {
nextIndex = index < items.length - 2 ? index + 1 : index - 1;
}
items.removeObject(item);
if (nextIndex) {
this.set("current", items[nextIndex]);
}
},
},
});

Datei anzeigen

@ -1,38 +0,0 @@
import Component from "@ember/component";
import { gt } from "@ember/object/computed";
import { computed } from "@ember/object";
import { defaultConnector } from "../lib/wizard-mapper";
import { later } from "@ember/runloop";
import I18n from "I18n";
export default Component.extend({
classNameBindings: [
":mapper-connector",
":mapper-block",
"hasMultiple::single",
],
hasMultiple: gt("connectors.length", 1),
connectorLabel: computed(function () {
let key = this.connector;
let path = this.inputTypes ? `input.${key}.name` : `connector.${key}`;
return I18n.t(`admin.wizard.${path}`);
}),
didReceiveAttrs() {
if (!this.connector) {
later(() => {
this.set(
"connector",
defaultConnector(this.connectorType, this.inputType, this.options)
);
});
}
},
actions: {
changeConnector(value) {
this.set("connector", value);
this.onUpdate("connector", this.connectorType);
},
},
});

Datei anzeigen

@ -1,78 +0,0 @@
import { computed, set } from "@ember/object";
import { alias, equal, not, or } from "@ember/object/computed";
import {
connectorContent,
defaultConnector,
defaultSelectionType,
inputTypesContent,
newPair,
} from "../lib/wizard-mapper";
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
import { A } from "@ember/array";
export default Component.extend({
classNameBindings: [":mapper-input", "inputType"],
inputType: alias("input.type"),
isConditional: equal("inputType", "conditional"),
isAssignment: equal("inputType", "assignment"),
isAssociation: equal("inputType", "association"),
isValidation: equal("inputType", "validation"),
hasOutput: or("isConditional", "isAssignment"),
hasPairs: or("isConditional", "isAssociation", "isValidation"),
canAddPair: not("isAssignment"),
connectors: computed(function () {
return connectorContent("output", this.input.type, this.options);
}),
inputTypes: computed(function () {
return inputTypesContent(this.options);
}),
@observes("input.type")
setupType() {
if (this.hasPairs && (!this.input.pairs || this.input.pairs.length < 1)) {
this.send("addPair");
}
if (this.hasOutput) {
this.set("input.output", null);
if (!this.input.outputConnector) {
const options = this.options;
this.set("input.output_type", defaultSelectionType("output", options));
this.set(
"input.output_connector",
defaultConnector("output", this.inputType, options)
);
}
}
},
actions: {
addPair() {
if (!this.input.pairs) {
this.set("input.pairs", A());
}
const pairs = this.input.pairs;
const pairCount = pairs.length + 1;
pairs.forEach((p) => set(p, "pairCount", pairCount));
pairs.pushObject(
newPair(
this.input.type,
Object.assign({}, this.options, { index: pairs.length, pairCount })
)
);
},
removePair(pair) {
const pairs = this.input.pairs;
const pairCount = pairs.length - 1;
pairs.forEach((p) => set(p, "pairCount", pairCount));
pairs.removeObject(pair);
},
},
});

Datei anzeigen

@ -1,16 +0,0 @@
import { connectorContent } from "../lib/wizard-mapper";
import { alias, gt } from "@ember/object/computed";
import { computed } from "@ember/object";
import Component from "@ember/component";
export default Component.extend({
classNameBindings: [":mapper-pair", "hasConnector::no-connector"],
firstPair: gt("pair.index", 0),
showRemove: alias("firstPair"),
showJoin: computed("pair.pairCount", function () {
return this.pair.index < this.pair.pairCount - 1;
}),
connectors: computed(function () {
return connectorContent("pair", this.inputType, this.options);
}),
});

Datei anzeigen

@ -1,16 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
tagName: "a",
classNameBindings: ["active"],
@discourseComputed("item.type", "activeType")
active(type, activeType) {
return type === activeType;
},
click() {
this.toggle(this.item.type);
},
});

Datei anzeigen

@ -1,410 +0,0 @@
import { alias, gt, or } from "@ember/object/computed";
import { computed } from "@ember/object";
import {
default as discourseComputed,
observes,
} from "discourse-common/utils/decorators";
import { getOwner } from "discourse-common/lib/get-owner";
import { defaultSelectionType, selectionTypes } from "../lib/wizard-mapper";
import {
generateName,
sentenceCase,
snakeCase,
userProperties,
} from "../lib/wizard";
import Component from "@ember/component";
import { bind, later } from "@ember/runloop";
import I18n from "I18n";
import Subscription from "../mixins/subscription";
const customFieldActionMap = {
topic: ["create_topic", "send_message"],
post: ["create_topic", "send_message"],
category: ["create_category"],
group: ["create_group"],
user: ["update_profile"],
};
const values = ["present", "true", "false"];
export default Component.extend(Subscription, {
classNameBindings: [":mapper-selector", "activeType"],
showText: computed("activeType", function () {
return this.showInput("text");
}),
showWizardField: computed("activeType", function () {
return this.showInput("wizardField");
}),
showWizardAction: computed("activeType", function () {
return this.showInput("wizardAction");
}),
showUserField: computed("activeType", function () {
return this.showInput("userField");
}),
showUserFieldOptions: computed("activeType", function () {
return this.showInput("userFieldOptions");
}),
showCategory: computed("activeType", function () {
return this.showInput("category");
}),
showTag: computed("activeType", function () {
return this.showInput("tag");
}),
showGroup: computed("activeType", function () {
return this.showInput("group");
}),
showUser: computed("activeType", function () {
return this.showInput("user");
}),
showList: computed("activeType", function () {
return this.showInput("list");
}),
showCustomField: computed("activeType", function () {
return this.showInput("customField");
}),
showValue: computed("activeType", function () {
return this.showInput("value");
}),
textEnabled: computed("options.textSelection", "inputType", function () {
return this.optionEnabled("textSelection");
}),
wizardFieldEnabled: computed(
"options.wizardFieldSelection",
"inputType",
function () {
return this.optionEnabled("wizardFieldSelection");
}
),
wizardActionEnabled: computed(
"options.wizardActionSelection",
"inputType",
function () {
return this.optionEnabled("wizardActionSelection");
}
),
customFieldEnabled: computed(
"options.customFieldSelection",
"inputType",
function () {
return this.optionEnabled("customFieldSelection");
}
),
userFieldEnabled: computed(
"options.userFieldSelection",
"inputType",
function () {
return this.optionEnabled("userFieldSelection");
}
),
userFieldOptionsEnabled: computed(
"options.userFieldOptionsSelection",
"inputType",
function () {
return this.optionEnabled("userFieldOptionsSelection");
}
),
categoryEnabled: computed(
"options.categorySelection",
"inputType",
function () {
return this.optionEnabled("categorySelection");
}
),
tagEnabled: computed("options.tagSelection", "inputType", function () {
return this.optionEnabled("tagSelection");
}),
groupEnabled: computed("options.groupSelection", "inputType", function () {
return this.optionEnabled("groupSelection");
}),
guestGroup: computed("options.guestGroup", "inputType", function () {
return this.optionEnabled("guestGroup");
}),
userEnabled: computed("options.userSelection", "inputType", function () {
return this.optionEnabled("userSelection");
}),
listEnabled: computed("options.listSelection", "inputType", function () {
return this.optionEnabled("listSelection");
}),
valueEnabled: computed("connector", function () {
return this.connector === "is";
}),
@discourseComputed("site.groups", "guestGroup", "subscriptionType")
groups(groups, guestGroup, subscriptionType) {
let result = groups;
if (!guestGroup) {
return result;
}
if (["standard", "business"].includes(subscriptionType)) {
let guestIndex;
result.forEach((r, index) => {
if (r.id === 0) {
r.name = I18n.t("admin.wizard.selector.label.users");
guestIndex = index;
}
});
result.splice(guestIndex, 0, {
id: -1,
name: I18n.t("admin.wizard.selector.label.guests"),
});
}
return result;
},
categories: alias("site.categories"),
showComboBox: or(
"showWizardField",
"showWizardAction",
"showUserField",
"showUserFieldOptions",
"showCustomField",
"showValue"
),
showMultiSelect: or("showCategory", "showGroup"),
hasTypes: gt("selectorTypes.length", 1),
showTypes: false,
didInsertElement() {
if (
!this.activeType ||
(this.activeType && !this[`${this.activeType}Enabled`])
) {
later(() => this.resetActiveType());
}
$(document).on("click", bind(this, this.documentClick));
},
willDestroyElement() {
$(document).off("click", bind(this, this.documentClick));
},
documentClick(e) {
if (this._state === "destroying") {
return;
}
let $target = $(e.target);
if (!$target.parents(".type-selector").length && this.showTypes) {
this.set("showTypes", false);
}
},
@discourseComputed("connector")
selectorTypes() {
return selectionTypes
.filter((type) => this[`${type}Enabled`])
.map((type) => ({ type, label: this.typeLabel(type) }));
},
@discourseComputed("activeType")
activeTypeLabel(activeType) {
return this.typeLabel(activeType);
},
typeLabel(type) {
return type
? I18n.t(`admin.wizard.selector.label.${snakeCase(type)}`)
: null;
},
comboBoxAllowAny: or("showWizardField", "showWizardAction"),
@discourseComputed
showController() {
return getOwner(this).lookup("controller:admin-wizards-wizard-show");
},
@discourseComputed(
"activeType",
"showController.wizardFields.[]",
"showController.wizard.actions.[]",
"showController.userFields.[]",
"showController.currentField.id",
"showController.currentAction.id",
"showController.customFields"
)
comboBoxContent(
activeType,
wizardFields,
wizardActions,
userFields,
currentFieldId,
currentActionId,
customFields
) {
let content;
let context;
let contextType;
if (this.options.context) {
let contextAttrs = this.options.context.split(".");
context = contextAttrs[0];
contextType = contextAttrs[1];
}
if (activeType === "wizardField") {
content = wizardFields;
if (context === "field") {
content = content.filter((field) => field.id !== currentFieldId);
}
}
if (activeType === "wizardAction") {
content = wizardActions.map((a) => ({
id: a.id,
label: `${generateName(a.type)} (${a.id})`,
type: a.type,
}));
if (context === "action") {
content = content.filter((a) => a.id !== currentActionId);
}
}
if (activeType === "userField") {
content = userProperties
.map((f) => ({
id: f,
name: generateName(f),
}))
.concat(userFields || []);
if (
context === "action" &&
this.inputType === "association" &&
this.selectorType === "key"
) {
const excludedFields = ["username", "email", "trust_level"];
content = content.filter(
(userField) => excludedFields.indexOf(userField.id) === -1
);
}
}
if (activeType === "userFieldOptions") {
content = userFields;
}
if (activeType === "customField") {
content = customFields
.filter((f) => {
return (
f.type !== "json" &&
customFieldActionMap[f.klass].includes(contextType)
);
})
.map((f) => ({
id: f.name,
name: `${sentenceCase(f.klass)} ${f.name} (${f.type})`,
}));
}
if (activeType === "value") {
content = values.map((value) => ({
id: value,
name: value,
}));
}
return content;
},
@discourseComputed("activeType")
multiSelectContent(activeType) {
return {
category: this.categories,
group: this.groups,
list: "",
}[activeType];
},
@discourseComputed("activeType", "inputType")
placeholderKey(activeType) {
if (
activeType === "text" &&
this.options[`${this.selectorType}Placeholder`]
) {
return this.options[`${this.selectorType}Placeholder`];
} else {
return `admin.wizard.selector.placeholder.${snakeCase(activeType)}`;
}
},
@discourseComputed("activeType")
multiSelectOptions(activeType) {
let result = {
none: this.placeholderKey,
};
if (activeType === "list") {
result.allowAny = true;
}
return result;
},
optionEnabled(type) {
const options = this.options;
if (!options) {
return false;
}
const option = options[type];
if (option === true) {
return true;
}
if (typeof option !== "string") {
return false;
}
return option.split(",").filter((o) => {
return [this.selectorType, this.inputType].indexOf(o) !== -1;
}).length;
},
showInput(type) {
return this.activeType === type && this[`${type}Enabled`];
},
changeValue(value) {
this.set("value", value);
this.onUpdate("selector", this.activeType);
},
@observes("inputType")
resetActiveType() {
this.set(
"activeType",
defaultSelectionType(this.selectorType, this.options, this.connector)
);
},
actions: {
toggleType(type) {
this.set("activeType", type);
this.set("showTypes", false);
this.set("value", null);
this.onUpdate("selector");
},
toggleTypes() {
this.toggleProperty("showTypes");
},
changeValue(value) {
this.changeValue(value);
},
changeInputValue(event) {
this.changeValue(event.target.value);
},
changeUserValue(value) {
this.changeValue(value);
},
},
});

Datei anzeigen

@ -1,86 +0,0 @@
import { newInput, selectionTypes } from "../lib/wizard-mapper";
import discourseComputed from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import { A } from "@ember/array";
export default Component.extend({
classNames: "wizard-mapper",
didReceiveAttrs() {
if (this.inputs && this.inputs.constructor !== Array) {
later(() => this.set("inputs", null));
}
},
@discourseComputed("inputs.@each.type")
canAdd(inputs) {
return (
!inputs ||
inputs.constructor !== Array ||
inputs.every((i) => {
return ["assignment", "association"].indexOf(i.type) === -1;
})
);
},
@discourseComputed("options.@each.inputType")
inputOptions(options) {
let result = {
inputTypes: options.inputTypes || "assignment,conditional",
inputConnector: options.inputConnector || "or",
pairConnector: options.pairConnector || null,
outputConnector: options.outputConnector || null,
context: options.context || null,
guestGroup: options.guestGroup || false,
};
let inputTypes = ["key", "value", "output"];
inputTypes.forEach((type) => {
result[`${type}Placeholder`] = options[`${type}Placeholder`] || null;
result[`${type}DefaultSelection`] =
options[`${type}DefaultSelection`] || null;
});
selectionTypes.forEach((type) => {
if (options[`${type}Selection`] !== undefined) {
result[`${type}Selection`] = options[`${type}Selection`];
} else {
result[`${type}Selection`] = type === "text" ? true : false;
}
});
return result;
},
onUpdate() {},
actions: {
add() {
if (!this.get("inputs")) {
this.set("inputs", A());
}
this.get("inputs").pushObject(
newInput(this.inputOptions, this.inputs.length)
);
this.onUpdate(this.property, "input");
},
remove(input) {
const inputs = this.inputs;
inputs.removeObject(input);
if (inputs.length) {
inputs[0].set("connector", null);
}
this.onUpdate(this.property, "input");
},
inputUpdated(component, type) {
this.onUpdate(this.property, component, type);
},
},
});

Datei anzeigen

@ -1,33 +0,0 @@
import { default as discourseComputed } from "discourse-common/utils/decorators";
import { not, notEmpty } from "@ember/object/computed";
import Component from "@ember/component";
import I18n from "I18n";
const icons = {
error: "times-circle",
success: "check-circle",
warn: "exclamation-circle",
info: "info-circle",
};
export default Component.extend({
classNameBindings: [":wizard-message", "type", "loading"],
showDocumentation: not("loading"),
showIcon: not("loading"),
hasItems: notEmpty("items"),
@discourseComputed("type")
icon(type) {
return icons[type] || "info-circle";
},
@discourseComputed("key", "component", "opts")
message(key, component, opts) {
return I18n.t(`admin.wizard.message.${component}.${key}`, opts || {});
},
@discourseComputed("component")
documentation(component) {
return I18n.t(`admin.wizard.message.${component}.documentation`);
},
});

Datei anzeigen

@ -1,58 +0,0 @@
import Component from "@ember/component";
import EmberObject from "@ember/object";
import { cloneJSON } from "discourse-common/lib/object";
import Category from "discourse/models/category";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
classNames: ["realtime-validations", "setting", "full", "subscription"],
@discourseComputed
timeUnits() {
return ["days", "weeks", "months", "years"].map((unit) => {
return {
id: unit,
name: I18n.t(`admin.wizard.field.validations.time_units.${unit}`),
};
});
},
init() {
this._super(...arguments);
if (!this.validations) {
return;
}
if (!this.field.validations) {
const validations = {};
this.validations.forEach((validation) => {
validations[validation] = {};
});
this.set("field.validations", EmberObject.create(validations));
}
const validationBuffer = cloneJSON(this.get("field.validations"));
let bufferCategories;
if (
validationBuffer.similar_topics &&
(bufferCategories = validationBuffer.similar_topics.categories)
) {
const categories = Category.findByIds(bufferCategories);
validationBuffer.similar_topics.categories = categories;
}
this.set("validationBuffer", validationBuffer);
},
actions: {
updateValidationCategories(type, validation, categories) {
this.set(`validationBuffer.${type}.categories`, categories);
this.set(
`field.validations.${type}.categories`,
categories.map((category) => category.id)
);
},
},
});

Datei anzeigen

@ -1,30 +0,0 @@
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

@ -1,26 +0,0 @@
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" : "times";
},
@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

@ -1,36 +0,0 @@
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

@ -1,96 +0,0 @@
import SingleSelectComponent from "select-kit/components/single-select";
import Subscription from "../mixins/subscription";
import { filterValues } 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", "wizard.allowGuests")
content(feature, attribute) {
return filterValues(this.wizard, 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

@ -1,17 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,139 +0,0 @@
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", "isSubmittedAt")
hasValue(value, isUser, isSubmittedAt) {
if (isUser || isSubmittedAt) {
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,68 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import { userProperties } from "../lib/wizard";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import I18n from "I18n";
const excludedUserProperties = ["profile_background", "card_background"];
export default Component.extend({
classNames: "wizard-text-editor",
barEnabled: true,
previewEnabled: true,
fieldsEnabled: true,
hasWizardFields: notEmpty("wizardFieldList"),
hasWizardActions: notEmpty("wizardActionList"),
didReceiveAttrs() {
this._super(...arguments);
if (!this.barEnabled) {
scheduleOnce("afterRender", () => {
$(this.element).find(".d-editor-button-bar").addClass("hidden");
});
}
},
@discourseComputed("forcePreview")
previewLabel(forcePreview) {
return I18n.t("admin.wizard.editor.preview", {
action: I18n.t(`admin.wizard.editor.${forcePreview ? "hide" : "show"}`),
});
},
@discourseComputed("showPopover")
popoverLabel(showPopover) {
return I18n.t("admin.wizard.editor.popover", {
action: I18n.t(`admin.wizard.editor.${showPopover ? "hide" : "show"}`),
});
},
@discourseComputed()
userPropertyList() {
return userProperties
.filter((f) => !excludedUserProperties.includes(f))
.map((f) => ` u{${f}}`);
},
@discourseComputed("wizardFields")
wizardFieldList(wizardFields) {
return (wizardFields || []).map((f) => ` w{${f.id}}`);
},
@discourseComputed("wizardActions")
wizardActionList(wizardActions) {
return (wizardActions || []).map((a) => ` w{${a.id}}`);
},
actions: {
togglePreview() {
this.toggleProperty("forcePreview");
},
togglePopover() {
this.toggleProperty("showPopover");
},
},
});

Datei anzeigen

@ -1,12 +0,0 @@
import ValueList from "admin/components/value-list";
export default ValueList.extend({
_saveValues() {
if (this.inputType === "array") {
this.onChange(this.collection);
return;
}
this.onChange(this.collection.join(this.inputDelimiter || "\n"));
},
});

Datei anzeigen

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

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