implement the UI for the many_to_many relationship (WIP)
This commit is contained in:
parent
9e9fe49ccf
commit
2a911d912c
@ -12,6 +12,12 @@ class Locomotive.Models.ContentEntry extends Backbone.Model
|
|||||||
collection = new Locomotive.Models.ContentEntriesCollection(@get(name))
|
collection = new Locomotive.Models.ContentEntriesCollection(@get(name))
|
||||||
@set_attribute name, collection
|
@set_attribute name, collection
|
||||||
|
|
||||||
|
_.each @get('_many_to_many_fields'), (field) =>
|
||||||
|
name = field[0]
|
||||||
|
collection = new Locomotive.Models.ContentEntriesCollection(@get(name))
|
||||||
|
collection.comparator = (entry) -> entry.get('__position') || 0
|
||||||
|
@set_attribute name, collection
|
||||||
|
|
||||||
set_attribute: (attribute, value) ->
|
set_attribute: (attribute, value) ->
|
||||||
data = {}
|
data = {}
|
||||||
data[attribute] = value
|
data[attribute] = value
|
||||||
@ -40,6 +46,10 @@ class Locomotive.Models.ContentEntry extends Backbone.Model
|
|||||||
name = field[0]
|
name = field[0]
|
||||||
hash["#{name}_attributes"] = @get(name).toMinJSON()
|
hash["#{name}_attributes"] = @get(name).toMinJSON()
|
||||||
|
|
||||||
|
_.each @get('_many_to_many_fields'), (field) => # include the many_to_many relationships
|
||||||
|
name = field[0]; setter_name = field[1]
|
||||||
|
hash[setter_name] = @get(name).sort().map (entry) => entry.id
|
||||||
|
|
||||||
class Locomotive.Models.ContentEntriesCollection extends Backbone.Collection
|
class Locomotive.Models.ContentEntriesCollection extends Backbone.Collection
|
||||||
|
|
||||||
model: Locomotive.Models.ContentEntry
|
model: Locomotive.Models.ContentEntry
|
||||||
|
@ -17,7 +17,7 @@ class Locomotive.Models.CustomField extends Backbone.Model
|
|||||||
['class_name', 'inverse_of', 'ui_enabled']
|
['class_name', 'inverse_of', 'ui_enabled']
|
||||||
|
|
||||||
is_relationship_type: ->
|
is_relationship_type: ->
|
||||||
_.include(['belongs_to', 'has_many'], @get('type'))
|
_.include(['belongs_to', 'has_many', 'many_to_many'], @get('type'))
|
||||||
|
|
||||||
toJSONForSave: ->
|
toJSONForSave: ->
|
||||||
_.tap {}, (hash) =>
|
_.tap {}, (hash) =>
|
||||||
|
@ -10,6 +10,8 @@ class Locomotive.Views.ContentEntries.FormView extends Locomotive.Views.Shared.F
|
|||||||
|
|
||||||
_has_many_field_views: []
|
_has_many_field_views: []
|
||||||
|
|
||||||
|
_many_to_many_field_views: []
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'submit': 'save'
|
'submit': 'save'
|
||||||
|
|
||||||
@ -33,6 +35,8 @@ class Locomotive.Views.ContentEntries.FormView extends Locomotive.Views.Shared.F
|
|||||||
|
|
||||||
@enable_has_many_fields()
|
@enable_has_many_fields()
|
||||||
|
|
||||||
|
@enable_many_to_many_fields()
|
||||||
|
|
||||||
@slugify_label_field()
|
@slugify_label_field()
|
||||||
|
|
||||||
return @
|
return @
|
||||||
@ -67,7 +71,16 @@ class Locomotive.Views.ContentEntries.FormView extends Locomotive.Views.Shared.F
|
|||||||
|
|
||||||
@_has_many_field_views.push(view)
|
@_has_many_field_views.push(view)
|
||||||
|
|
||||||
@$("##{@model.paramRoot}_#{name}_input").append(view.render().el)
|
@$("##{@model.paramRoot}_#{name}_input label").after(view.render().el)
|
||||||
|
|
||||||
|
enable_many_to_many_fields: ->
|
||||||
|
_.each @model.get('_many_to_many_fields'), (field) =>
|
||||||
|
name = field[0]
|
||||||
|
view = new Locomotive.Views.Shared.Fields.ManyToManyView model: @model, name: name, all_entries: @options["all_#{name}_entries"]
|
||||||
|
|
||||||
|
@_many_to_many_field_views.push(view)
|
||||||
|
|
||||||
|
@$("##{@model.paramRoot}_#{name}_input label").after(view.render().el)
|
||||||
|
|
||||||
slugify_label_field: ->
|
slugify_label_field: ->
|
||||||
@$('li.input.highlighted > input[type=text]').slugify(target: @$('#content_entry__slug'))
|
@$('li.input.highlighted > input[type=text]').slugify(target: @$('#content_entry__slug'))
|
||||||
@ -88,6 +101,7 @@ class Locomotive.Views.ContentEntries.FormView extends Locomotive.Views.Shared.F
|
|||||||
remove: ->
|
remove: ->
|
||||||
_.each @_file_field_views, (view) => view.remove()
|
_.each @_file_field_views, (view) => view.remove()
|
||||||
_.each @_has_many_field_views, (view) => view.remove()
|
_.each @_has_many_field_views, (view) => view.remove()
|
||||||
|
_.each @_many_to_many_field_views, (view) => view.remove()
|
||||||
super
|
super
|
||||||
|
|
||||||
tinyMCE_settings: ->
|
tinyMCE_settings: ->
|
||||||
|
@ -13,8 +13,6 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
|
|||||||
initialize: ->
|
initialize: ->
|
||||||
@inverse_of_list = @options.parent_view.options.inverse_of_list
|
@inverse_of_list = @options.parent_view.options.inverse_of_list
|
||||||
|
|
||||||
console.log(@model.get('inverse_of'))
|
|
||||||
|
|
||||||
@model.bind 'change', (custom_field) =>
|
@model.bind 'change', (custom_field) =>
|
||||||
@switch_to_type() if @model.hasChanged('type')
|
@switch_to_type() if @model.hasChanged('type')
|
||||||
@fetch_inverse_of_list() if @model.hasChanged('class_name') && @model.get('class_name')?
|
@fetch_inverse_of_list() if @model.hasChanged('class_name') && @model.get('class_name')?
|
||||||
@ -51,7 +49,7 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
|
|||||||
when 'belongs_to'
|
when 'belongs_to'
|
||||||
@$('li.input.localized').hide()
|
@$('li.input.localized').hide()
|
||||||
@$('li.input.class-name').show()
|
@$('li.input.class-name').show()
|
||||||
when 'has_many'
|
when 'has_many', 'many_to_many'
|
||||||
@$('li.input.localized').hide()
|
@$('li.input.localized').hide()
|
||||||
@$('li.input.class-name').show()
|
@$('li.input.class-name').show()
|
||||||
@$('li.input.inverse-of').show()
|
@$('li.input.inverse-of').show()
|
||||||
@ -60,7 +58,7 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
|
|||||||
fetch_inverse_of_list: ->
|
fetch_inverse_of_list: ->
|
||||||
@$('li.input.inverse-of select option').remove()
|
@$('li.input.inverse-of select option').remove()
|
||||||
|
|
||||||
_.each @inverse_of_list, (data) =>
|
_.each @inverse_of_list[@model.get('type')], (data) =>
|
||||||
if data.class_name == @model.get('class_name')
|
if data.class_name == @model.get('class_name')
|
||||||
option = new Option(data.label, data.name, data.class_name == @model.get('inverse_of') || @inverse_of_list.length == 1)
|
option = new Option(data.label, data.name, data.class_name == @model.get('inverse_of') || @inverse_of_list.length == 1)
|
||||||
@$('li.input.inverse-of select').append(option)
|
@$('li.input.inverse-of select').append(option)
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
Locomotive.Views.Shared ||= {}
|
||||||
|
Locomotive.Views.Shared.Fields ||= {}
|
||||||
|
|
||||||
|
class Locomotive.Views.Shared.Fields.ManyToManyView extends Backbone.View
|
||||||
|
|
||||||
|
tagName: 'div'
|
||||||
|
|
||||||
|
className: 'list'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .new-entry a.add': 'add_entry'
|
||||||
|
'keypress .new-entry select': 'add_entry'
|
||||||
|
'click ul span.actions a.remove': 'remove_entry'
|
||||||
|
|
||||||
|
template: ->
|
||||||
|
ich["#{@options.name}_list"]
|
||||||
|
|
||||||
|
entry_template: ->
|
||||||
|
ich["#{@options.name}_entry"]
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
_.bindAll(@, 'refresh_position_entries')
|
||||||
|
|
||||||
|
@collection = @model.get(@options.name)
|
||||||
|
@all_entries = @options.all_entries
|
||||||
|
|
||||||
|
window.collection = @collection
|
||||||
|
window.bar = @all_entries
|
||||||
|
window.model = @model
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
$(@el).html(@template()())
|
||||||
|
|
||||||
|
@insert_entries()
|
||||||
|
|
||||||
|
@make_entries_sortable()
|
||||||
|
|
||||||
|
@refresh_select_field()
|
||||||
|
|
||||||
|
return @
|
||||||
|
|
||||||
|
insert_entries: ->
|
||||||
|
if @collection.length > 0
|
||||||
|
@collection.each (entry) => @insert_entry(entry)
|
||||||
|
else
|
||||||
|
@$('.empty').show()
|
||||||
|
|
||||||
|
insert_entry: (entry) ->
|
||||||
|
unless @collection.get(entry.get('_id'))?
|
||||||
|
@collection.add(entry)
|
||||||
|
|
||||||
|
@$('.empty').hide()
|
||||||
|
entry_html = $(@entry_template()(label: entry.get('_label')))
|
||||||
|
entry_html.data('data-entry-id', entry.id)
|
||||||
|
@$('> ul').append(entry_html)
|
||||||
|
|
||||||
|
make_entries_sortable: ->
|
||||||
|
@sortable_list = @$('> ul').sortable
|
||||||
|
handle: '.handle'
|
||||||
|
items: 'li'
|
||||||
|
axis: 'y'
|
||||||
|
update: @refresh_position_entries
|
||||||
|
|
||||||
|
refresh_position_entries: ->
|
||||||
|
@$('> ul > li').each (index, entry_html) =>
|
||||||
|
id = $(entry_html).data('data-entry-id')
|
||||||
|
entry = @collection.get(id)
|
||||||
|
entry.set_attribute "__position", index
|
||||||
|
|
||||||
|
add_entry: (event) ->
|
||||||
|
event.stopPropagation() & event.preventDefault()
|
||||||
|
|
||||||
|
entry_id = @$('.new-entry select').val()
|
||||||
|
entry = @get_entry_from_id(entry_id)
|
||||||
|
|
||||||
|
return unless entry?
|
||||||
|
|
||||||
|
@insert_entry(entry)
|
||||||
|
@refresh_select_field()
|
||||||
|
|
||||||
|
remove_entry: (event) ->
|
||||||
|
event.stopPropagation() & event.preventDefault()
|
||||||
|
|
||||||
|
if confirm($(event.target).attr('data-confirm'))
|
||||||
|
entry = @get_entry_from_element($(event.target))
|
||||||
|
@collection.remove(entry)
|
||||||
|
|
||||||
|
$(event.target).closest('li').remove()
|
||||||
|
@$('.empty').show() if @$('> ul > li').size() == 0
|
||||||
|
|
||||||
|
@refresh_position_entries() & @refresh_select_field()
|
||||||
|
|
||||||
|
refresh_select_field: ->
|
||||||
|
@$('.new-entry select optgroup, .new-entry select option').remove()
|
||||||
|
|
||||||
|
_.each @all_entries, (entry_or_group) =>
|
||||||
|
if _.isArray(entry_or_group.entries)
|
||||||
|
group_html = $('<optgroup/>').attr('label', entry_or_group.name)
|
||||||
|
|
||||||
|
_.each entry_or_group.entries, (entry) =>
|
||||||
|
unless @collection.get(entry._id)?
|
||||||
|
option = new Option(entry._label, entry._id, false)
|
||||||
|
group_html.append(option)
|
||||||
|
|
||||||
|
@$('.new-entry select').append(group_html)
|
||||||
|
else
|
||||||
|
unless @collection.get(entry_or_group._id)?
|
||||||
|
option = new Option(entry_or_group._label, entry_or_group._id, false)
|
||||||
|
@$('.new-entry select').append(option)
|
||||||
|
|
||||||
|
get_entry_from_element: (element) ->
|
||||||
|
entry_html = $(element).closest('li')
|
||||||
|
id = $(entry_html).data('data-entry-id')
|
||||||
|
@collection.get(id)
|
||||||
|
|
||||||
|
get_entry_from_id: (id) ->
|
||||||
|
entry = null
|
||||||
|
|
||||||
|
_.each @all_entries, (entry_or_group) =>
|
||||||
|
if _.isArray(entry_or_group.entries)
|
||||||
|
entry ||= _.detect(entry_or_group.entries, (_entry) => _entry._id == id)
|
||||||
|
else
|
||||||
|
entry = entry_or_group if entry_or_group._id == id
|
||||||
|
|
||||||
|
if entry?
|
||||||
|
new Locomotive.Models.ContentEntry(entry)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -570,6 +570,7 @@ form.formtastic {
|
|||||||
&.label {
|
&.label {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // ul .col
|
} // ul .col
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module Locomotive::CustomFieldsHelper
|
module Locomotive::CustomFieldsHelper
|
||||||
|
|
||||||
def options_for_custom_field_type
|
def options_for_custom_field_type
|
||||||
%w(string text select boolean date file belongs_to has_many).map do |type|
|
%w(string text select boolean date file belongs_to has_many many_to_many).map do |type|
|
||||||
[t("custom_fields.types.#{type}"), type]
|
[t("custom_fields.types.#{type}"), type]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -48,11 +48,12 @@ module Locomotive::CustomFieldsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def options_for_content_type_inverse_of
|
def options_for_content_type_inverse_of
|
||||||
[].tap do |list|
|
{}.tap do |hash|
|
||||||
current_site.content_types.where(:'entries_custom_fields.type' => 'belongs_to').each do |content_type|
|
current_site.content_types.where(:'entries_custom_fields.type'.in => %w(belongs_to many_to_many)).each do |content_type|
|
||||||
content_type.entries_custom_fields.each do |field|
|
content_type.entries_custom_fields.each do |field|
|
||||||
if field.type == 'belongs_to'
|
if %w(belongs_to many_to_many).include?(field.type)
|
||||||
list << {
|
hash[field.type] ||= []
|
||||||
|
hash[field.type] << {
|
||||||
:label => field.label,
|
:label => field.label,
|
||||||
:name => field.name,
|
:name => field.name,
|
||||||
:class_name => content_type.klass_with_custom_fields(:entries).to_s
|
:class_name => content_type.klass_with_custom_fields(:entries).to_s
|
||||||
|
@ -150,11 +150,11 @@ module Locomotive
|
|||||||
content_type = Locomotive::ContentType.find($1)
|
content_type = Locomotive::ContentType.find($1)
|
||||||
|
|
||||||
if content_type.site_id != self.site_id
|
if content_type.site_id != self.site_id
|
||||||
field.errors.add :security
|
field.errors.add :class_name, :security
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# for now, does not allow external classes
|
# for now, does not allow external classes
|
||||||
field.errors.add :security
|
field.errors.add :class_name, :security
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ module Locomotive
|
|||||||
def safe_attributes
|
def safe_attributes
|
||||||
self.source.custom_fields_recipe['rules'].map do |rule|
|
self.source.custom_fields_recipe['rules'].map do |rule|
|
||||||
case rule['type'].to_sym
|
case rule['type'].to_sym
|
||||||
when :date then "formatted_#{rule['name']}"
|
when :date then "formatted_#{rule['name']}"
|
||||||
when :file then [rule['name'], "remove_#{rule['name']}"]
|
when :file then [rule['name'], "remove_#{rule['name']}"]
|
||||||
when :select, :belongs_to then ["#{rule['name']}_id", "position_in_#{rule['name']}"]
|
when :select, :belongs_to then ["#{rule['name']}_id", "position_in_#{rule['name']}"]
|
||||||
when :has_many then nil
|
when :has_many, :many_to_many then nil
|
||||||
else
|
else
|
||||||
rule['name']
|
rule['name']
|
||||||
end
|
end
|
||||||
@ -50,15 +50,19 @@ module Locomotive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def _file_fields
|
def _file_fields
|
||||||
self.source.custom_fields_recipe['rules'].find_all { |rule| rule['type'] == 'file' }.map { |rule| rule['name'] }
|
group_fields 'file'
|
||||||
end
|
end
|
||||||
|
|
||||||
def _has_many_fields
|
def _has_many_fields
|
||||||
self.source.custom_fields_recipe['rules'].find_all { |rule| rule['type'] == 'has_many' }.map { |rule| [rule['name'], rule['inverse_of']] }
|
group_fields('has_many') { |rule| [rule['name'], rule['inverse_of']] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def _many_to_many_fields
|
||||||
|
group_fields('many_to_many') { |rule| [rule['name'], "#{rule['name'].singularize}_ids"] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def included_methods
|
def included_methods
|
||||||
default_list = %w(_label _slug _position content_type_slug _file_fields _has_many_fields safe_attributes)
|
default_list = %w(_label _slug _position content_type_slug _file_fields _has_many_fields _many_to_many_fields safe_attributes)
|
||||||
default_list << 'errors' if !!self.options[:include_errors]
|
default_list << 'errors' if !!self.options[:include_errors]
|
||||||
super + self.custom_fields_methods + default_list
|
super + self.custom_fields_methods + default_list
|
||||||
end
|
end
|
||||||
@ -91,5 +95,13 @@ module Locomotive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_fields(type, &block)
|
||||||
|
unless block_given?
|
||||||
|
block = lambda { |rule| rule['name'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
self.source.custom_fields_recipe['rules'].find_all { |rule| rule['type'] == type }.map(&block)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -0,0 +1,41 @@
|
|||||||
|
- if field.ui_enabled?
|
||||||
|
- target_content_type = Locomotive::ContentType.class_name_to_content_type(field.class_name, current_site)
|
||||||
|
|
||||||
|
= f.input name.to_sym,
|
||||||
|
:label => field.label,
|
||||||
|
:hint => field.hint,
|
||||||
|
:as => :'Locomotive::Empty',
|
||||||
|
:wrapper_html => { :id => "content_entry_#{name}_input", :class => 'empty relationship input' }
|
||||||
|
|
||||||
|
- content_for :head do
|
||||||
|
|
||||||
|
%script{ :type => 'text/html', :id => "#{name}_list" }
|
||||||
|
|
||||||
|
%p.empty{ :style => 'display: none' }!= t('.empty')
|
||||||
|
|
||||||
|
%ul
|
||||||
|
|
||||||
|
%hr
|
||||||
|
|
||||||
|
.new-entry
|
||||||
|
|
||||||
|
= select_tag 'entry', []
|
||||||
|
|
||||||
|
%span.actions
|
||||||
|
= link_to t('locomotive.buttons.new_item'), '#', :class => 'add'
|
||||||
|
|
||||||
|
%script{ :type => 'text/html', :id => "#{name}_entry" }
|
||||||
|
%li
|
||||||
|
.handle.col
|
||||||
|
= image_tag 'locomotive/form/icons/drag.png'
|
||||||
|
|
||||||
|
.label.col
|
||||||
|
{{label}}
|
||||||
|
|
||||||
|
%span.actions
|
||||||
|
= link_to 'x', '#', :class => 'remove', :confirm => t('locomotive.messages.confirm')
|
||||||
|
|
||||||
|
|
||||||
|
- content_for :backbone_view_data do
|
||||||
|
:plain
|
||||||
|
, all_#{name}_entries: #{target_content_type.list_or_group_entries.to_json}
|
@ -82,6 +82,8 @@ en:
|
|||||||
has_many:
|
has_many:
|
||||||
empty: The list is empty
|
empty: The list is empty
|
||||||
new_entry: + Add a new entry
|
new_entry: + Add a new entry
|
||||||
|
many_to_many:
|
||||||
|
empty: The list is empty. Add an entry from the select box below.
|
||||||
|
|
||||||
form:
|
form:
|
||||||
required: Required
|
required: Required
|
||||||
|
1
doc/TODO
1
doc/TODO
@ -121,6 +121,7 @@ x script to migrate existing site
|
|||||||
x unable to remove a field
|
x unable to remove a field
|
||||||
x "back to admin" link does not work if inline editor disabled
|
x "back to admin" link does not work if inline editor disabled
|
||||||
x unable to delete memberships
|
x unable to delete memberships
|
||||||
|
- editable_elements does not display the first time they get created (and if there are no existing ones)
|
||||||
- display by categories does not work when localized
|
- display by categories does not work when localized
|
||||||
- disallow to click twice on the submit form button (spinner ?)
|
- disallow to click twice on the submit form button (spinner ?)
|
||||||
- message to notify people if their browser is too old
|
- message to notify people if their browser is too old
|
||||||
|
@ -74,9 +74,12 @@
|
|||||||
|
|
||||||
for (var prefix in data) {
|
for (var prefix in data) {
|
||||||
_buildParams(prefix, data[prefix], function(key, value) {
|
_buildParams(prefix, data[prefix], function(key, value) {
|
||||||
// console.log('append ' + key + ', ' + value);
|
// console.log('append ' + key + ', ' + value + ', is Array = ' + jQuery.isArray(value));
|
||||||
if (value != null)
|
if (jQuery.isArray(value) && value.length == 0)
|
||||||
formData.append(key, value);
|
formData.append(key + '[]', '');
|
||||||
|
else
|
||||||
|
if (value != null)
|
||||||
|
formData.append(key, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user