mirror of
https://github.com/internetee/registry.git
synced 2025-07-01 16:53:37 +02:00
Nested form for domain
This commit is contained in:
parent
d6543f2148
commit
4fef7e94c8
8 changed files with 382 additions and 26 deletions
|
@ -6,6 +6,7 @@
|
|||
#= require nprogress-turbolinks
|
||||
#= require typeahead.bundle.min
|
||||
#= require autocomplete
|
||||
#= require jquery.nested_attributes
|
||||
#= require app
|
||||
|
||||
NProgress.configure
|
||||
|
|
|
@ -6,6 +6,8 @@ class Admin::DomainsController < ApplicationController
|
|||
owner_contact = Contact.find(params[:owner_contact_id]) if params[:owner_contact_id]
|
||||
@domain = Domain.new(owner_contact: owner_contact)
|
||||
params[:domain_owner_contact] = owner_contact
|
||||
|
||||
@domain.nameservers.build
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -59,7 +61,7 @@ class Admin::DomainsController < ApplicationController
|
|||
end
|
||||
|
||||
def domain_params
|
||||
params.require(:domain).permit(:name, :period, :period_unit, :registrar_id, :owner_contact_id)
|
||||
params.require(:domain).permit(:name, :period, :period_unit, :registrar_id, :owner_contact_id, nameservers_attributes: [:id, :hostname, :ipv4, :ipv6, :_destroy])
|
||||
end
|
||||
|
||||
def verify_deletion
|
||||
|
|
|
@ -14,7 +14,8 @@ class Domain < ActiveRecord::Base
|
|||
where(domain_contacts: { contact_type: DomainContact::ADMIN })
|
||||
end, through: :domain_contacts, source: :contact
|
||||
|
||||
has_many :nameservers, dependent: :delete_all, autosave: true
|
||||
has_many :nameservers, dependent: :delete_all
|
||||
accepts_nested_attributes_for :nameservers, allow_destroy: true
|
||||
|
||||
has_many :domain_statuses, dependent: :delete_all
|
||||
|
||||
|
|
|
@ -7,30 +7,33 @@
|
|||
%hr
|
||||
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :name
|
||||
= f.text_field(:name, class: 'form-control')
|
||||
.form-group
|
||||
= f.label :period
|
||||
.row
|
||||
.col-md-6
|
||||
= f.text_field(:period, class: 'form-control')
|
||||
.col-md-6
|
||||
= f.select :period_unit, options_for_select(['y', 'm', 'd'], @domain.period_unit), {}, {class: 'form-control'}
|
||||
.col-md-6
|
||||
.form-group.has-feedback
|
||||
= f.label :registrar
|
||||
= text_field_tag(:domain_registrar, params[:registrar], class: 'form-control js-registrar-typeahead', placeholder: t('shared.registrar_name'), autocomplete: 'off')
|
||||
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
|
||||
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
|
||||
= f.hidden_field(:registrar_id, class: 'js-registrar-id')
|
||||
.form-group.has-feedback
|
||||
= f.label :owner_contact
|
||||
= text_field_tag(:domain_owner_contact, params[:domain_owner_contact], class: 'form-control js-contact-typeahead', placeholder: t('shared.contact_code'), autocomplete: 'off')
|
||||
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
|
||||
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
|
||||
= f.hidden_field(:owner_contact_id, class: 'js-contact-id')
|
||||
.col-md-12
|
||||
/ Nav tabs
|
||||
%ul.nav.nav-tabs{:role => "tablist", id: 'tabs'}
|
||||
%li.active
|
||||
%a{"data-toggle" => "tab", :href => "#general-tab", :role => "tab"} general-tab
|
||||
%li
|
||||
%a{"data-toggle" => "tab", :href => "#nameservers-tab", :role => "tab"} nameservers-tab
|
||||
%li
|
||||
%a{"data-toggle" => "tab", :href => "#tech-contacts-tab", :role => "tab"} tech-contacts-tab
|
||||
%li
|
||||
%a{"data-toggle" => "tab", :href => "#admin-contacts", :role => "tab"} admin-contacts-tab
|
||||
%li
|
||||
%a{"data-toggle" => "tab", :href => "#statuses-tab", :role => "tab"} statuses-tab
|
||||
/ Tab panes
|
||||
.tab-content{style:'margin-top: 20px;'}
|
||||
#general-tab.tab-pane.active
|
||||
= render 'admin/domains/form_partials/general', f: f
|
||||
#nameservers-tab.tab-pane
|
||||
= render 'admin/domains/form_partials/nameservers', f: f
|
||||
#tech-contacts-tab.tab-pane
|
||||
#admin-contacts-tab.tab-pane ...
|
||||
#statuses-tab.tab-pane ...
|
||||
.row
|
||||
.col-md-12.text-right
|
||||
= button_tag(t('shared.save'), class: 'btn btn-primary')
|
||||
|
||||
:javascript
|
||||
$(function () {
|
||||
$('#tabs a:first').tab('show')
|
||||
})
|
||||
|
|
25
app/views/admin/domains/form_partials/_general.haml
Normal file
25
app/views/admin/domains/form_partials/_general.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :name
|
||||
= f.text_field(:name, class: 'form-control')
|
||||
.form-group
|
||||
= f.label :period
|
||||
.row
|
||||
.col-md-6
|
||||
= f.text_field(:period, class: 'form-control')
|
||||
.col-md-6
|
||||
= f.select :period_unit, options_for_select(['y', 'm', 'd'], @domain.period_unit), {}, {class: 'form-control'}
|
||||
.col-md-6
|
||||
.form-group.has-feedback
|
||||
= f.label :registrar
|
||||
= text_field_tag(:domain_registrar, params[:registrar], class: 'form-control js-registrar-typeahead', placeholder: t('shared.registrar_name'), autocomplete: 'off')
|
||||
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
|
||||
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
|
||||
= f.hidden_field(:registrar_id, class: 'js-registrar-id')
|
||||
.form-group.has-feedback
|
||||
= f.label :owner_contact
|
||||
= text_field_tag(:domain_owner_contact, params[:domain_owner_contact], class: 'form-control js-contact-typeahead', placeholder: t('shared.contact_code'), autocomplete: 'off')
|
||||
%span.glyphicon.glyphicon-ok.form-control-feedback.js-typeahead-ok.hidden
|
||||
%span.glyphicon.glyphicon-remove.form-control-feedback.js-typeahead-remove
|
||||
= f.hidden_field(:owner_contact_id, class: 'js-contact-id')
|
30
app/views/admin/domains/form_partials/_nameservers.haml
Normal file
30
app/views/admin/domains/form_partials/_nameservers.haml
Normal file
|
@ -0,0 +1,30 @@
|
|||
#nameservers
|
||||
= f.fields_for :nameservers do |ns_fields|
|
||||
.panel.panel-default
|
||||
.panel-heading.text-right
|
||||
= link_to(t('shared.add_another'), '#', class: 'btn btn-primary btn-xs add-nameserver')
|
||||
= link_to(t('shared.delete'), '#', class: 'btn btn-danger btn-xs destroy')
|
||||
.panel-body
|
||||
.errors
|
||||
= render 'admin/shared/errors', object: ns_fields.object
|
||||
.row
|
||||
.col-md-12
|
||||
.form-group
|
||||
= ns_fields.label :hostname
|
||||
= ns_fields.text_field :hostname, class: 'form-control'
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
= ns_fields.label :ipv4
|
||||
= ns_fields.text_field :ipv4, class: 'form-control'
|
||||
.col-md-6
|
||||
.form-group
|
||||
= ns_fields.label :ipv6
|
||||
= ns_fields.text_field :ipv6, class: 'form-control'
|
||||
:javascript
|
||||
$("#nameservers").nestedAttributes({
|
||||
bindAddTo: $(".add-nameserver"),
|
||||
afterAdd: function(item) {
|
||||
item.find('.errors').html('');
|
||||
}
|
||||
});
|
|
@ -259,3 +259,4 @@ en:
|
|||
failed_to_add_domain: 'Failed to add domain!'
|
||||
domain_added: 'Domain added!'
|
||||
new_contact: 'New contact'
|
||||
add_another: 'Add another'
|
||||
|
|
293
vendor/assets/javascripts/jquery.nested_attributes.coffee
vendored
Normal file
293
vendor/assets/javascripts/jquery.nested_attributes.coffee
vendored
Normal file
|
@ -0,0 +1,293 @@
|
|||
###
|
||||
Authors: Nick Giancola (@patbenatar), Brendan Loudermilk (@bloudermilk)
|
||||
Homepage: https://github.com/patbenatar/jquery-nested_attributes
|
||||
###
|
||||
$ = jQuery
|
||||
|
||||
methods =
|
||||
init: (options) ->
|
||||
$el = $(@)
|
||||
throw "Can't initialize more than one item at a time" if $el.length > 1
|
||||
if $el.data("nestedAttributes")
|
||||
throw "Can't initialize on this element more than once"
|
||||
instance = new NestedAttributes($el, options)
|
||||
$el.data("nestedAttributes", instance)
|
||||
return $el
|
||||
add: ->
|
||||
$el = $(@)
|
||||
unless $el.data("nestedAttributes")?
|
||||
throw "You are trying to call instance methods without initializing first"
|
||||
$el.data("nestedAttributes").addItem()
|
||||
return $el
|
||||
|
||||
$.fn.nestedAttributes = (method) ->
|
||||
if methods[method]?
|
||||
return methods[method].apply @, Array.prototype.slice.call(arguments, 1)
|
||||
else if typeof method == 'object' || !method
|
||||
return methods.init.apply(@, arguments)
|
||||
else
|
||||
$.error("Method #{method} does not exist on jQuery.nestedAttributes")
|
||||
|
||||
class NestedAttributes
|
||||
|
||||
RELEVANT_INPUTS_SELECTOR: ":input[name][name!=\"\"]"
|
||||
|
||||
settings:
|
||||
collectionName: false # If not provided, we will autodetect
|
||||
bindAddTo: false # Required
|
||||
removeOnLoadIf: false
|
||||
collectIdAttributes: true
|
||||
beforeAdd: false
|
||||
afterAdd: false
|
||||
beforeMove: false
|
||||
afterMove: false
|
||||
beforeDestroy: false
|
||||
afterDestroy: false
|
||||
destroySelector: '.destroy'
|
||||
deepClone: true
|
||||
$clone: null
|
||||
|
||||
######################
|
||||
## ##
|
||||
## Initialization ##
|
||||
## ##
|
||||
######################
|
||||
|
||||
constructor: ($el, options) ->
|
||||
|
||||
# This plugin gets called on the container
|
||||
@$container = $el
|
||||
|
||||
# Merge default options
|
||||
@options = $.extend({}, @settings, options)
|
||||
|
||||
# If the user provided a jQuery object to bind the "Add"
|
||||
# bind it now or forever hold your peace.
|
||||
@options.bindAddTo.click(@addClick) if @options.bindAddTo
|
||||
|
||||
# Cache all the items
|
||||
@$items = @$container.children()
|
||||
|
||||
# If the user didn't provide a collectionName, autodetect it
|
||||
unless @options.collectionName
|
||||
@autodetectCollectionName()
|
||||
|
||||
# Initialize existing items
|
||||
@$items.each (i, el) =>
|
||||
$item = $(el)
|
||||
|
||||
# If the user wants us to attempt to collect Rail's ID attributes, do it now
|
||||
# Using the default rails helpers, ID attributes will wind up right after their
|
||||
# propper containers in the form.
|
||||
if @options.collectIdAttributes and $item.is('input')
|
||||
# Move the _id field into its proper container
|
||||
$item.appendTo($item.prev())
|
||||
# Remove it from the $items collection
|
||||
@$items = @$items.not($item)
|
||||
else
|
||||
# Try to find and bind the destroy link if the user wanted one
|
||||
@bindDestroy($item)
|
||||
|
||||
# Now that we've collected ID attributes
|
||||
@hideIfAlreadyDestroyed $(item) for item in @$items
|
||||
|
||||
# Remove any items on load if the client implements a check and the check passes
|
||||
if @options.removeOnLoadIf
|
||||
@$items.each (i, el) =>
|
||||
$el = $(el)
|
||||
if $el.call(true, @options.removeOnLoadIf, i)
|
||||
$el.remove()
|
||||
|
||||
|
||||
########################
|
||||
## ##
|
||||
## Instance Methods ##
|
||||
## ##
|
||||
########################
|
||||
|
||||
autodetectCollectionName: ->
|
||||
pattern = /\[(.[^\]]*)_attributes\]/
|
||||
try
|
||||
match = pattern.exec(@$items.first().find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name'))[1]
|
||||
if match != null
|
||||
@options.collectionName = match
|
||||
else
|
||||
throw "Regex error"
|
||||
catch error
|
||||
console.log "Error detecting collection name", error
|
||||
|
||||
addClick: (event) =>
|
||||
|
||||
@addItem()
|
||||
|
||||
# Don't let the link do anything
|
||||
event.preventDefault()
|
||||
|
||||
addItem: ->
|
||||
# Piece together an item
|
||||
newIndex = @$items.length
|
||||
$newClone = @applyIndexToItem(@extractClone(), newIndex)
|
||||
|
||||
# Give the user a chance to make their own changes before we insert
|
||||
if (@options.beforeAdd)
|
||||
|
||||
# Stop the add process if the callback returns false
|
||||
return false if !@options.beforeAdd.call(undefined, $newClone, newIndex)
|
||||
|
||||
# Insert the new item after the last item
|
||||
@$container.append($newClone)
|
||||
|
||||
# Give the user a chance to make their own changes after insertion
|
||||
@options.afterAdd.call(undefined, $newClone, newIndex) if (@options.afterAdd)
|
||||
|
||||
# Add this item to the items list
|
||||
@refreshItems()
|
||||
|
||||
extractClone: ->
|
||||
|
||||
# Are we restoring from an already created clone?
|
||||
if @$restorableClone
|
||||
|
||||
$record = @$restorableClone
|
||||
|
||||
@$restorableClone = null
|
||||
|
||||
else
|
||||
$record = @options.$clone || @$items.first()
|
||||
|
||||
# Make a deep clone (bound events and data)
|
||||
$record = $record.clone(@options.deepClone)
|
||||
|
||||
@bindDestroy($record) if @options.$clone or !@options.deepClone
|
||||
|
||||
# Empty out the values of text inputs and selects
|
||||
$record.find(':text, textarea, select').val('')
|
||||
|
||||
# Reset checkboxes and radios
|
||||
$record.find(':checkbox, :radio').attr("checked", false)
|
||||
|
||||
# Empty out any hidden [id] or [_destroy] fields
|
||||
$record.find('input[name$="\\[id\\]"]').remove()
|
||||
$record.find('input[name$="\\[_destroy\\]"]').remove()
|
||||
|
||||
# Make sure it's not hidden as we return.
|
||||
# It would be hidden in the case where we're duplicating an
|
||||
# already removed item for its template.
|
||||
return $record.show()
|
||||
|
||||
applyIndexToItem: ($item, index) ->
|
||||
collectionName = @options.collectionName
|
||||
|
||||
$item.find(@RELEVANT_INPUTS_SELECTOR).each (i, el) =>
|
||||
|
||||
$el = $(el)
|
||||
|
||||
idRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
|
||||
idReplacement = "_#{collectionName}_attributes_#{index}_"
|
||||
nameRegExp = new RegExp("\\[#{collectionName}_attributes\\]\\[\\d+\\]")
|
||||
nameReplacement = "[#{collectionName}_attributes][#{index}]"
|
||||
|
||||
newID = $el.attr('id').replace(idRegExp, idReplacement) if $el.attr('id')
|
||||
newName = $el.attr('name').replace(nameRegExp, nameReplacement)
|
||||
|
||||
$el.attr
|
||||
id: newID
|
||||
name: newName
|
||||
|
||||
$item.find('label[for]').each (i, el) =>
|
||||
$el = $(el)
|
||||
try
|
||||
forRegExp = new RegExp("_#{collectionName}_attributes_\\d+_")
|
||||
forReplacement = "_#{collectionName}_attributes_#{index}_"
|
||||
newFor = $el.attr('for').replace(forRegExp, forReplacement)
|
||||
$el.attr('for', newFor)
|
||||
catch error
|
||||
console.log "Error updating label", error
|
||||
|
||||
return $item
|
||||
|
||||
hideIfAlreadyDestroyed: ($item) ->
|
||||
$destroyField = $item.find("[name$='[_destroy]']")
|
||||
if $destroyField.length && $destroyField.val() == "true"
|
||||
@destroy $item
|
||||
|
||||
# Hides a item from the user and marks it for deletion in the
|
||||
# DOM by setting _destroy to true if the record already exists. If it
|
||||
# is a new escalation, we simple delete the item
|
||||
destroyClick: (event) =>
|
||||
event.preventDefault()
|
||||
@destroy $(event.target).parentsUntil(@$container).last()
|
||||
|
||||
destroy: ($item) ->
|
||||
# If you're about to delete the last one,
|
||||
# cache a clone of it first so we have something to show
|
||||
# the next time user hits add
|
||||
@$restorableClone = @extractClone() unless @$items.length-1
|
||||
|
||||
index = @indexForItem($item)
|
||||
itemIsNew = $item.find('input[name$="\\[id\\]"]').length == 0
|
||||
|
||||
if (@options.beforeDestroy)
|
||||
|
||||
# Stop the destroy process if the callback returns false
|
||||
return false if !@options.beforeDestroy.call(undefined, $item, index, itemIsNew)
|
||||
|
||||
# Add a blank item row if none are visible after this deletion
|
||||
@addItem() unless @$items.filter(':visible').length-1
|
||||
|
||||
if itemIsNew
|
||||
|
||||
$item.remove()
|
||||
|
||||
else
|
||||
|
||||
# Hide the item
|
||||
$item.hide()
|
||||
|
||||
# Add the _destroy field
|
||||
otherFieldName = $item.find(':input[name]:first').attr('name')
|
||||
attributePosition = otherFieldName.lastIndexOf('[')
|
||||
destroyFieldName = "#{otherFieldName.substring(0, attributePosition)}[_destroy]"
|
||||
# First look for an existing _destroy field
|
||||
$destroyField = $item.find("input[name='#{destroyFieldName}']")
|
||||
# If it doesn't exist, create it
|
||||
if $destroyField.length == 0
|
||||
$destroyField = $("<input type=\"hidden\" name=\"#{destroyFieldName}\" />")
|
||||
$item.append($destroyField)
|
||||
$destroyField.val(true).change()
|
||||
|
||||
@options.afterDestroy.call($item, index, itemIsNew) if (@options.afterDestroy)
|
||||
|
||||
# Remove this item from the items list
|
||||
@refreshItems()
|
||||
|
||||
# Rename the remaining items
|
||||
@resetIndexes()
|
||||
|
||||
indexForItem: ($item) ->
|
||||
regExp = new RegExp("\\[#{@options.collectionName}_attributes\\]\\[\\d+\\]")
|
||||
name = $item.find("#{@RELEVANT_INPUTS_SELECTOR}:first").attr('name')
|
||||
return parseInt(name.match(regExp)[0].split('][')[1].slice(0, -1), 10)
|
||||
|
||||
refreshItems: ->
|
||||
@$items = @$container.children()
|
||||
|
||||
# Sets the proper association indices and labels to all items
|
||||
# Used when removing items
|
||||
resetIndexes: ->
|
||||
@$items.each (i, el) =>
|
||||
$el = $(el)
|
||||
|
||||
# Make sure this is actually a new position
|
||||
oldIndex = @indexForItem($el)
|
||||
return true if (i == oldIndex)
|
||||
|
||||
@options.beforeMove.call($el, i, oldIndex) if (@options.beforeMove)
|
||||
|
||||
# Change the number to the new index
|
||||
@applyIndexToItem($el, i)
|
||||
|
||||
@options.afterMove.call($el, i, oldIndex) if (@options.afterMove)
|
||||
|
||||
bindDestroy: ($item) ->
|
||||
$item.find(@options.destroySelector).click(@destroyClick) if (@options.destroySelector)
|
Loading…
Add table
Add a link
Reference in a new issue