0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-10 04:12:53 +01:00
Dieser Commit ist enthalten in:
Angus McLeod 2020-04-06 18:36:38 +10:00
Ursprung 04d7fc1c59
Commit 6570e4b74b
21 geänderte Dateien mit 246 neuen und 134 gelöschten Zeilen

Datei anzeigen

@ -41,7 +41,8 @@ export default Component.extend({
if (this.isDropdown) { if (this.isDropdown) {
options.wizardFieldSelection = 'key,value'; options.wizardFieldSelection = 'key,value';
options.listSelection = 'assignment'; options.listSelection = 'assignment';
options.inputTypes = 'pair,assignment'; options.inputTypes = 'association,assignment';
options.singular = true;
options.pairConnector = 'association'; options.pairConnector = 'association';
options.keyPlaceholder = 'admin.wizard.key'; options.keyPlaceholder = 'admin.wizard.key';
options.valuePlaceholder = 'admin.wizard.value'; options.valuePlaceholder = 'admin.wizard.value';

Datei anzeigen

@ -1,10 +1,10 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { lt } from '@ember/object/computed'; import { gt } from '@ember/object/computed';
import { computed } from "@ember/object"; import { computed } from "@ember/object";
export default Component.extend({ export default Component.extend({
classNameBindings: [':mapper-connector', ':mapper-block', 'single'], classNameBindings: [':mapper-connector', ':mapper-block', 'hasMultiple::single'],
single: lt('connectors.length', 2), hasMultiple: gt('connectors.length', 1),
connectorLabel: computed(function() { connectorLabel: computed(function() {
let key = this.connector; let key = this.connector;
let path = this.inputTypes ? `input.${key}.name` : `connector.${key}`; let path = this.inputTypes ? `input.${key}.name` : `connector.${key}`;

Datei anzeigen

@ -5,13 +5,14 @@ import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators"; import { observes } from "discourse-common/utils/decorators";
export default Component.extend({ export default Component.extend({
classNameBindings: [':mapper-input', 'type'], classNameBindings: [':mapper-input', 'inputType'],
inputType: alias('input.type'), inputType: alias('input.type'),
isConditional: equal('inputType', 'conditional'), isConditional: equal('inputType', 'conditional'),
isAssignment: equal('inputType', 'assignment'), isAssignment: equal('inputType', 'assignment'),
isPair: equal('inputType', 'pair'), isAssociation: equal('inputType', 'association'),
isValidation: equal('inputType', 'validation'),
hasOutput: or('isConditional', 'isAssignment'), hasOutput: or('isConditional', 'isAssignment'),
hasPairs: or('isConditional', 'isPair'), hasPairs: or('isConditional', 'isAssociation', 'isValidation'),
connectors: computed(function() { return connectorContent('output', this.input.type, this.options) }), connectors: computed(function() { return connectorContent('output', this.input.type, this.options) }),
inputTypes: computed(function() { return inputTypesContent(this.options) }), inputTypes: computed(function() { return inputTypesContent(this.options) }),

Datei anzeigen

@ -1,6 +1,6 @@
import { alias, or, gt } from "@ember/object/computed"; import { alias, or, gt } from "@ember/object/computed";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { default as discourseComputed, observes } from "discourse-common/utils/decorators"; import { default as discourseComputed, observes, on } from "discourse-common/utils/decorators";
import { getOwner } from 'discourse-common/lib/get-owner'; import { getOwner } from 'discourse-common/lib/get-owner';
import { defaultSelectionType, selectionTypes } from '../lib/wizard-mapper'; import { defaultSelectionType, selectionTypes } from '../lib/wizard-mapper';
import { snakeCase, selectKitContent } from '../lib/wizard'; import { snakeCase, selectKitContent } from '../lib/wizard';
@ -72,7 +72,7 @@ export default Component.extend({
return showTypes ? 'chevron-down' : 'chevron-right'; return showTypes ? 'chevron-down' : 'chevron-right';
}, },
@observes('options.@each', 'inputType') @observes('inputType')
resetActiveType() { resetActiveType() {
this.set('activeType', defaultSelectionType(this.selectorType, this.options)); this.set('activeType', defaultSelectionType(this.selectorType, this.options));
}, },

Datei anzeigen

@ -1,22 +1,24 @@
import { getOwner } from 'discourse-common/lib/get-owner'; import { getOwner } from 'discourse-common/lib/get-owner';
import { on } from 'discourse-common/utils/decorators';
import { newInput, selectionTypes } from '../lib/wizard-mapper'; import { newInput, selectionTypes } from '../lib/wizard-mapper';
import { default as discourseComputed, observes } from 'discourse-common/utils/decorators'; import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators';
import { gt } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { A } from "@ember/array"; import { A } from "@ember/array";
export default Component.extend({ export default Component.extend({
classNames: 'wizard-mapper', classNames: 'wizard-mapper',
hasInput: gt('inputs.length', 0),
@discourseComputed('inputs.[]', 'options.singular') @discourseComputed('options.singular', 'hasInput')
canAdd(inputs, singular) { canAdd(singular, hasInput) {
return !singular || !inputs || inputs.length < 1; return !singular || !hasInput;
}, },
@discourseComputed('options.@each') @discourseComputed('options.@each.inputType')
inputOptions(options) { inputOptions(options) {
let result = { let result = {
inputTypes: options.inputTypes || 'conditional,assignment', inputTypes: options.inputTypes || 'conditional,assignment',
inputConnector: options.inputConnector || 'or',
pairConnector: options.pairConnector || null, pairConnector: options.pairConnector || null,
outputConnector: options.outputConnector || null, outputConnector: options.outputConnector || null,
context: options.context || null context: options.context || null
@ -45,7 +47,9 @@ export default Component.extend({
this.set('inputs', A()); this.set('inputs', A());
} }
this.get('inputs').pushObject(newInput(this.inputOptions)); this.get('inputs').pushObject(
newInput(this.inputOptions, this.inputs.length)
);
}, },
remove(input) { remove(input) {

Datei anzeigen

@ -50,7 +50,7 @@ function buildMappedJson(inputs) {
if (present(inpt.output)) { if (present(inpt.output)) {
input.output = inpt.output; input.output = inpt.output;
input.output_type = snakeCase(inpt.output_type); input.output_type = snakeCase(inpt.output_type);
input.connector = inpt.connector; input.output_connector = inpt.output_connector;
} }
if (present(inpt.pairs)) { if (present(inpt.pairs)) {
@ -74,8 +74,7 @@ function buildMappedJson(inputs) {
} }
if ((input.type === 'assignment' && present(input.output)) || if ((input.type === 'assignment' && present(input.output)) ||
(input.type === 'conditional' && present(input.pairs)) || present(input.pairs)) {
(input.type === 'pair' && present(input.pairs))) {
result.push(input); result.push(input);
} }
@ -146,56 +145,60 @@ function buildStepJson(object) {
}; };
} }
function mappedProperty(property, value) { function castCase(property, value) {
return property.indexOf('_type') > -1 ? camelCase(value) : value; return property.indexOf('_type') > -1 ? camelCase(value) : value;
} }
function buildProperty(json, property, type) {
if (mapped(property, type) && present(json[property])) {
let inputs = [];
json[property].forEach(inputJson => {
let input = {}
Object.keys(inputJson).forEach(inputKey => {
if (inputKey === 'pairs') {
let pairs = [];
let pairCount = inputJson.pairs.length;
inputJson.pairs.forEach(pairJson => {
let pair = {};
Object.keys(pairJson).forEach(pairKey => {
pair[pairKey] = castCase(pairKey, pairJson[pairKey]);
});
pair.pairCount = pairCount;
pairs.push(
EmberObject.create(pair)
);
});
input.pairs = pairs;
} else {
input[inputKey] = castCase(inputKey, inputJson[inputKey]);
}
});
inputs.push(
EmberObject.create(input)
);
});
return A(inputs);
} else {
return json[property];
}
}
function buildObject(json, type) { function buildObject(json, type) {
let params = { let params = {
isNew: false isNew: false
} }
Object.keys(json).forEach(prop => { Object.keys(json).forEach(prop => {
if (mapped(prop, type) && present(json[prop])) { params[prop] = buildProperty(json, prop, type)
let inputs = [];
json[prop].forEach(inputJson => {
let input = {}
Object.keys(inputJson).forEach(inputKey => {
if (inputKey === 'pairs') {
let pairs = [];
let pairCount = inputJson.pairs.length;
inputJson.pairs.forEach(pairJson => {
let pair = {};
Object.keys(pairJson).forEach(pairKey => {
pair[pairKey] = mappedProperty(pairKey, pairJson[pairKey]);
});
pair.pairCount = pairCount;
pairs.push(
EmberObject.create(pair)
);
});
input.pairs = pairs;
} else {
input[inputKey] = mappedProperty(inputKey, inputJson[inputKey]);
}
});
inputs.push(
EmberObject.create(input)
);
});
params[prop] = A(inputs);
} else {
params[prop] = json[prop];
}
}); });
return EmberObject.create(params); return EmberObject.create(params);
@ -228,7 +231,7 @@ function buildProperties(json) {
props.existingId = true; props.existingId = true;
properties.wizard.forEach((p) => { properties.wizard.forEach((p) => {
props[p] = json[p]; props[p] = buildProperty(json, p, 'wizard');
if (wizardHasAdvanced(p, json[p])) { if (wizardHasAdvanced(p, json[p])) {
props.showAdvanced = true; props.showAdvanced = true;
@ -242,7 +245,7 @@ function buildProperties(json) {
}; };
properties.step.forEach((p) => { properties.step.forEach((p) => {
stepParams[p] = stepJson[p]; stepParams[p] = buildProperty(stepJson, p, 'wizard');;
if (stepHasAdvanced(p, stepJson[p])) { if (stepHasAdvanced(p, stepJson[p])) {
stepParams.showAdvanced = true; stepParams.showAdvanced = true;

Datei anzeigen

@ -34,16 +34,19 @@ const connectors = {
'greater', 'greater',
'less', 'less',
'greater_or_equal', 'greater_or_equal',
'less_or_equal' 'less_or_equal',
'regex'
] ]
} }
function defaultConnector(connectorType, inputType, opts = {}) { function defaultConnector(connectorType, inputType, opts = {}) {
if (opts[`${connectorType}Connector`]) return opts[`${connectorType}Connector`]; if (opts[`${connectorType}Connector`]) return opts[`${connectorType}Connector`];
if (inputType === 'assignment' && connectorType === 'output') return 'set';
if (inputType === 'conditional' && connectorType === 'output') return 'then'; if (inputType === 'conditional' && connectorType === 'output') return 'then';
if (inputType === 'conditional' && connectorType === 'pair') return 'equal'; if (inputType === 'conditional' && connectorType === 'pair') return 'equal';
if (inputType === 'pair') return 'equal'; if (inputType === 'assignment' && connectorType === 'output') return 'set';
if (inputType === 'association' && connectorType === 'pair') return 'association';
if (inputType === 'validation' && connectorType === 'pair') return 'equal';
return 'equal';
} }
function connectorContent(connectorType, inputType, opts) { function connectorContent(connectorType, inputType, opts) {
@ -114,7 +117,7 @@ function newPair(inputType, options = {}) {
return EmberObject.create(params); return EmberObject.create(params);
} }
function newInput(options = {}) { function newInput(options = {}, count) {
const inputType = defaultInputType(options); const inputType = defaultInputType(options);
let params = { let params = {
@ -132,12 +135,16 @@ function newInput(options = {}) {
) )
} }
if (count > 0) {
params.connector = options.inputConnector;
}
if (['conditional', 'assignment'].indexOf(inputType) > -1 || if (['conditional', 'assignment'].indexOf(inputType) > -1 ||
options.outputDefaultSelection || options.outputDefaultSelection ||
options.outputConnector) { options.outputConnector) {
params['output_type'] = defaultSelectionType('output', options); params['output_type'] = defaultSelectionType('output', options);
params['connector'] = defaultConnector('output', inputType, options); params['output_connector'] = defaultConnector('output', inputType, options);
} }
return EmberObject.create(params); return EmberObject.create(params);

Datei anzeigen

@ -111,14 +111,14 @@
{{wizard-mapper {{wizard-mapper
inputs=model.permitted inputs=model.permitted
options=(hash options=(hash
singular=true
context='wizard' context='wizard'
inputTypes='assignment' inputTypes='assignment,validation'
groupSelection='output' groupSelection='output'
textSelection='key,value' userFieldSelection='key'
textSelection='value'
inputConnector='and'
)}} )}}
</div> </div>
</div> </div>
{{wizard-advanced-toggle showAdvanced=model.showAdvanced}} {{wizard-advanced-toggle showAdvanced=model.showAdvanced}}

Datei anzeigen

@ -248,7 +248,7 @@
{{wizard-mapper {{wizard-mapper
inputs=action.custom_fields inputs=action.custom_fields
options=(hash options=(hash
pairConnector='set' inputTypes='association'
wizardFieldSelection='value' wizardFieldSelection='value'
userFieldSelection='value' userFieldSelection='value'
keyPlaceholder='admin.wizard.action.custom_fields.key' keyPlaceholder='admin.wizard.action.custom_fields.key'

Datei anzeigen

@ -48,6 +48,7 @@
{{wizard-mapper {{wizard-mapper
inputs=step.required_data inputs=step.required_data
options=(hash options=(hash
inputTypes='validation'
wizardFieldSelection='value' wizardFieldSelection='value'
userFieldSelection='value' userFieldSelection='value'
keyPlaceholder="admin.wizard.submission_key" keyPlaceholder="admin.wizard.submission_key"

Datei anzeigen

@ -1,10 +1,10 @@
{{#if single}} {{#if hasMultiple}}
<span class="connector-single">
{{connectorLabel}}
</span>
{{else}}
{{combo-box {{combo-box
value=connector value=connector
content=connectors content=connectors
onChange=(action (mut connector))}} onChange=(action (mut connector))}}
{{else}}
<span class="connector-single">
{{connectorLabel}}
</span>
{{/if}} {{/if}}

Datei anzeigen

@ -25,7 +25,7 @@
{{#if hasOutput}} {{#if hasOutput}}
{{#if hasPairs}} {{#if hasPairs}}
{{wizard-mapper-connector {{wizard-mapper-connector
connector=input.connector connector=input.output_connector
connectors=connectors}} connectors=connectors}}
{{/if}} {{/if}}

Datei anzeigen

@ -1,4 +1,8 @@
{{#each inputs as |input|}} {{#each inputs as |input|}}
{{#if input.connector}}
{{wizard-mapper-connector connector=input.connector}}
{{/if}}
{{wizard-mapper-input {{wizard-mapper-input
input=input input=input
options=inputOptions options=inputOptions

Datei anzeigen

@ -2,6 +2,5 @@
class=fieldClass class=fieldClass
value=field.value value=field.value
content=field.content content=field.content
none=(hash id="__none__" label=field.dropdown_none)
nameProperty="label" nameProperty="label"
tabindex="9"}} tabindex="9"}}

Datei anzeigen

@ -17,6 +17,12 @@
div.mapper-block:not(:last-of-type) { div.mapper-block:not(:last-of-type) {
margin-right: 10px; margin-right: 10px;
} }
> .mapper-connector.single {
width: min-content;
margin-bottom: 10px;
height: 20px
}
} }
[class~='mapper-input'] { [class~='mapper-input'] {
@ -156,6 +162,10 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
.mapper-connector {
min-width: 50px;
}
} }
.mapper-pair { .mapper-pair {

Datei anzeigen

@ -67,8 +67,10 @@ en:
output: 'then' output: 'then'
assignment: assignment:
name: 'set' name: 'set'
pair: association:
name: 'pair' name: 'map'
validation:
name: 'ensure'
selector: selector:
label: label:
@ -119,8 +121,6 @@ en:
description: "Description" description: "Description"
image: "Image" image: "Image"
image_placeholder: "Image url" image_placeholder: "Image url"
dropdown_none: "None"
dropdown_none_placeholder: "Label"
required: "Required" required: "Required"
required_label: "Field is Required" required_label: "Field is Required"
min_length: "Min Length" min_length: "Min Length"
@ -134,6 +134,8 @@ en:
content: "Content" content: "Content"
connector: connector:
and: "and"
or: "or"
then: "then" then: "then"
set: "set" set: "set"
equal: '=' equal: '='
@ -141,6 +143,7 @@ en:
less: '<' less: '<'
greater_or_equal: '>=' greater_or_equal: '>='
less_or_equal: '<=' less_or_equal: '<='
regex: '=~'
association: '→' association: '→'
action: action:

Datei anzeigen

@ -45,7 +45,7 @@ class CustomWizard::Action
inputs: action['recipient'], inputs: action['recipient'],
data: data, data: data,
user: user user: user
).output ).perform
if params[:title] && params[:raw] if params[:title] && params[:raw]
params[:archetype] = Archetype.private_message params[:archetype] = Archetype.private_message
@ -144,7 +144,7 @@ class CustomWizard::Action
opts: { opts: {
multiple: true multiple: true
} }
).output ).perform
groups = groups.flatten.reduce([]) do |result, g| groups = groups.flatten.reduce([]) do |result, g|
begin begin
@ -183,7 +183,7 @@ class CustomWizard::Action
inputs: action['category'], inputs: action['category'],
data: data, data: data,
user: user user: user
).output ).perform
if output.is_a?(Array) if output.is_a?(Array)
output.first output.first
@ -199,7 +199,7 @@ class CustomWizard::Action
inputs: action['tags'], inputs: action['tags'],
data: data, data: data,
user: user, user: user,
).output ).perform
if output.is_a?(Array) if output.is_a?(Array)
output.flatten output.flatten
@ -212,23 +212,23 @@ class CustomWizard::Action
def add_custom_fields(params = {}) def add_custom_fields(params = {})
if (custom_fields = action['custom_fields']).present? if (custom_fields = action['custom_fields']).present?
custom_fields.each do |field| field_map = CustomWizard::Mapper.new(
pair = field['pairs'].first inputs: custom_fields,
value = mapper.map_field(pair['key'], pair['key_type']) data: data,
key = mapper.map_field(pair['value'], pair['value_type']) user: user
).perform
if key && field_map.each do |field|
value.present? && keyArr = field[:key].split('.')
(keyArr = key.split('.')).length === 2 value = field[:value]
if keyArr.first === 'topic' if keyArr.length != 2 || keyArr.first === 'topic'
params[:topic_opts] ||= {} params[:topic_opts] ||= {}
params[:topic_opts][:custom_fields] ||= {} params[:topic_opts][:custom_fields] ||= {}
params[:topic_opts][:custom_fields][keyArr.last] = value params[:topic_opts][:custom_fields][keyArr.last] = value
elsif keyArr.first === 'post' elsif keyArr.first === 'post'
params[:custom_fields] ||= {} params[:custom_fields] ||= {}
params[:custom_fields][keyArr.last.to_sym] = value params[:custom_fields][keyArr.last.to_sym] = value
end
end end
end end
end end
@ -245,7 +245,7 @@ class CustomWizard::Action
inputs: action['title'], inputs: action['title'],
data: data, data: data,
user: user user: user
).output ).perform
params[:raw] = action['post_builder'] ? params[:raw] = action['post_builder'] ?
mapper.interpolate(action['post_template']) : mapper.interpolate(action['post_template']) :

Datei anzeigen

@ -235,12 +235,26 @@ class CustomWizard::Builder
@wizard.needs_groups = true @wizard.needs_groups = true
end end
if (content = field_template['content']).present? if (content_inputs = field_template['content']).present?
params[:content] = CustomWizard::Mapper.new( content = CustomWizard::Mapper.new(
inputs: content, inputs: content_inputs,
user: @wizard.user, user: @wizard.user,
data: @submissions.last data: @submissions.last,
).output opts: {
with_type: true
}
).perform
if content[:type] == 'association'
content[:result] = content[:result].map do |item|
{
id: item[:key],
name: item[:value]
}
end
end
params[:content] = content[:result]
end end
field = step.add_field(params) field = step.add_field(params)
@ -252,7 +266,7 @@ class CustomWizard::Builder
inputs: prefill, inputs: prefill,
user: @wizard.user, user: @wizard.user,
data: @submissions.last data: @submissions.last
).output ).perform
end end
end end

Datei anzeigen

@ -8,7 +8,8 @@ class CustomWizard::Mapper
greater: '>', greater: '>',
less: '<', less: '<',
greater_or_equal: '>=', greater_or_equal: '>=',
less_or_equal: '<=' less_or_equal: '<=',
regex: '=~'
} }
def initialize(params) def initialize(params)
@ -18,33 +19,63 @@ class CustomWizard::Mapper
@opts = params[:opts] || {} @opts = params[:opts] || {}
end end
def output def perform
multiple = @opts[:multiple] multiple = @opts[:multiple]
output = multiple ? [] : nil perform_result = multiple ? [] : nil
inputs.each do |input| inputs.each do |input|
if input['type'] === 'conditional' && validate_pairs(input['pairs']) input_type = input['type']
pairs = input['pairs']
if (input_type === 'conditional' && validate_pairs(pairs)) || input_type === 'assignment'
output = input['output']
output_type = input['output_type']
result = build_result(map_field(output, output_type), input_type)
if multiple if multiple
output.push(map_field(input['output'], input['output_type'])) perform_result.push(result)
else else
output = map_field(input['output'], input['output_type']) perform_result = result
break break
end end
end end
if input['type'] === 'assignment' if input_type === 'validation'
value = map_field(input['output'], input['output_type']) result = build_result(validate_pairs(pairs), input_type)
if @opts[:multiple] if multiple
output.push(value) perform_result.push(result)
else else
output = value perform_result = result
break
end
end
if input_type === 'association'
result = build_result(map_pairs(pairs), input_type)
if multiple
perform_result.push(result)
else
perform_result = result
break break
end end
end end
end end
output perform_result
end
def build_result(result, type)
if opts[:with_type]
{
type: type,
result: result
}
else
result
end
end end
def validate_pairs(pairs) def validate_pairs(pairs)
@ -52,10 +83,12 @@ class CustomWizard::Mapper
pairs.each do |pair| pairs.each do |pair|
key = map_field(pair['key'], pair['key_type']) key = map_field(pair['key'], pair['key_type'])
value = map_field(pair['value'], pair['value_type']) operator = map_operator(pair['connector'])
value = interpolate(map_field(pair['value'], pair['value_type']))
value = "/#{value}/" if pair['connector'] == 'regex'
begin begin
failed = true unless key.public_send(operator(pair['connector']), value) failed = true unless key.public_send(operator, value)
rescue NoMethodError rescue NoMethodError
# #
end end
@ -64,7 +97,25 @@ class CustomWizard::Mapper
!failed !failed
end end
def operator(connector) def map_pairs(pairs)
result = []
pairs.each do |pair|
key = map_field(pair['key'], pair['key_type'])
value = map_field(pair['value'], pair['value_type'])
if key && value
result.push(
key: key,
value: value
)
end
end
result
end
def map_operator(connector)
OPERATORS[connector.to_sym] || '==' OPERATORS[connector.to_sym] || '=='
end end

Datei anzeigen

@ -129,8 +129,27 @@ class CustomWizard::Wizard
def permitted? def permitted?
return false unless user return false unless user
return true if user.admin? || permitted.blank? return true if user.admin? || permitted.blank?
group_ids = permitted.first['output']
return GroupUser.exists?(group_id: group_ids, user_id: user.id) mapper = CustomWizard::Mapper.new(
inputs: permitted,
user: user,
opts: {
with_type: true,
multiple: true
}
).perform
return true if mapper.blank?
mapper.all? do |m|
if m.type === 'assignment'
GroupUser.exists?(group_id: m.result, user_id: user.id)
elsif m.type === 'validation'
mapper.result
else
true
end
end
end end
def reset def reset

Datei anzeigen

@ -2,8 +2,7 @@
class CustomWizardFieldSerializer < ::WizardFieldSerializer class CustomWizardFieldSerializer < ::WizardFieldSerializer
attributes :dropdown_none, attributes :image,
:image,
:file_types, :file_types,
:limit, :limit,
:property, :property,
@ -31,10 +30,6 @@ class CustomWizardFieldSerializer < ::WizardFieldSerializer
I18n.t("#{object.key || i18n_key}.placeholder", default: '') I18n.t("#{object.key || i18n_key}.placeholder", default: '')
end end
def dropdown_none
object.dropdown_none
end
def file_types def file_types
object.file_types object.file_types
end end