From 2caf940c2041008c42fda195182ec2fcb36b2bf4 Mon Sep 17 00:00:00 2001 From: Reuben Salagaras Date: Mon, 29 Dec 2014 22:59:38 +1030 Subject: [PATCH] Add "autocomplete" custom field type that can look up values in user-defined lists --- app/assets/javascripts/application.js.erb | 1 + .../crm_text_with_autocomplete.js.coffee | 17 +++++ .../admin/autocompletes_controller.rb | 65 +++++++++++++++++++ app/controllers/autocompletes_controller.rb | 28 ++++++++ app/helpers/admin/autocompletes_helper.rb | 10 +++ app/inputs/autocomplete_input.rb | 39 +++++++++++ app/models/autocomplete.rb | 14 ++++ app/models/fields/field.rb | 3 +- .../autocompletes/_autocomplete.html.haml | 8 +++ .../admin/autocompletes/_confirm.html.haml | 5 ++ app/views/admin/autocompletes/_edit.html.haml | 9 +++ app/views/admin/autocompletes/_new.html.haml | 10 +++ .../autocompletes/_top_section.html.haml | 10 +++ app/views/admin/autocompletes/confirm.js.haml | 7 ++ app/views/admin/autocompletes/create.js.haml | 11 ++++ app/views/admin/autocompletes/destroy.js.haml | 10 +++ app/views/admin/autocompletes/edit.js.haml | 20 ++++++ app/views/admin/autocompletes/index.html.haml | 17 +++++ app/views/admin/autocompletes/new.js.haml | 7 ++ app/views/admin/autocompletes/update.js.haml | 9 +++ .../_autocomplete_field.html.haml | 25 +++++++ config/locales/en-US_fat_free_crm.yml | 10 +++ config/routes.rb | 11 ++++ .../20141229005411_add_autocompletes.rb | 12 ++++ db/schema.rb | 7 +- 25 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/crm_text_with_autocomplete.js.coffee create mode 100644 app/controllers/admin/autocompletes_controller.rb create mode 100644 app/controllers/autocompletes_controller.rb create mode 100644 app/helpers/admin/autocompletes_helper.rb create mode 100644 app/inputs/autocomplete_input.rb create mode 100644 app/models/autocomplete.rb create mode 100644 app/views/admin/autocompletes/_autocomplete.html.haml create mode 100644 app/views/admin/autocompletes/_confirm.html.haml create mode 100644 app/views/admin/autocompletes/_edit.html.haml create mode 100644 app/views/admin/autocompletes/_new.html.haml create mode 100644 app/views/admin/autocompletes/_top_section.html.haml create mode 100644 app/views/admin/autocompletes/confirm.js.haml create mode 100644 app/views/admin/autocompletes/create.js.haml create mode 100644 app/views/admin/autocompletes/destroy.js.haml create mode 100644 app/views/admin/autocompletes/edit.js.haml create mode 100644 app/views/admin/autocompletes/index.html.haml create mode 100644 app/views/admin/autocompletes/new.js.haml create mode 100644 app/views/admin/autocompletes/update.js.haml create mode 100644 app/views/admin/custom_fields/_autocomplete_field.html.haml create mode 100644 db/migrate/20141229005411_add_autocompletes.rb diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index c468db8668..c3bf3e9bc3 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -40,6 +40,7 @@ //= require timeago //= require pagination //= require ffcrm_merge +//= require crm_text_with_autocomplete //= require_self <% diff --git a/app/assets/javascripts/crm_text_with_autocomplete.js.coffee b/app/assets/javascripts/crm_text_with_autocomplete.js.coffee new file mode 100644 index 0000000000..0c3867bf57 --- /dev/null +++ b/app/assets/javascripts/crm_text_with_autocomplete.js.coffee @@ -0,0 +1,17 @@ +(($) -> + + window.crm ||= {} + + crm.text_with_autocomplete = (el_id) -> + unless $("#text_with_autocomplete_" + el_id)[0] + $('#' + el_id).autocomplete + minLength: 2 + source: (request, response) -> + $.ajax + url: $('#' + el_id).data('autocompleteurl') + dataType: "json" + data: + name: request.term + success: (data) -> + response(data) +) jQuery \ No newline at end of file diff --git a/app/controllers/admin/autocompletes_controller.rb b/app/controllers/admin/autocompletes_controller.rb new file mode 100644 index 0000000000..f98e7edaaa --- /dev/null +++ b/app/controllers/admin/autocompletes_controller.rb @@ -0,0 +1,65 @@ +# Copyright (c) 2008-2013 Michael Dvorkin and contributors. +# +# Fat Free CRM is freely distributable under the terms of MIT license. +# See MIT-LICENSE file or http://www.opensource.org/licenses/mit-license.php +#------------------------------------------------------------------------------ +class Admin::AutocompletesController < Admin::ApplicationController + before_filter "set_current_tab('admin/autocompletes')", :only => [ :index, :show ] + + load_resource + + # GET /admin/autocompletes + # GET /admin/autocompletes.xml HTML + #---------------------------------------------------------------------------- + def index + @autocompletes = Autocomplete.all + respond_with(@autocompletes) + end + + # GET /admin/autocompletes/new + # GET /admin/autocompletes/new.xml AJAX + #---------------------------------------------------------------------------- + def new + respond_with(@autocomplete) + end + + # GET /admin/autocompletes/1/edit AJAX + #---------------------------------------------------------------------------- + def edit + if params[:previous].to_s =~ /(\d+)\z/ + @previous = Autocomplete.find_by_id($1) || $1.to_i + end + end + + # POST /admin/autocompletestags + # POST /admin/autocompletes.xml AJAX + #---------------------------------------------------------------------------- + def create + @autocomplete.update_attributes(params[:autocomplete]) + + respond_with(@autocomplete) + end + + # PUT /admin/autocompletes/1 + # PUT /admin/autocompletes/1.xml AJAX + #---------------------------------------------------------------------------- + def update + @autocomplete.update_attributes(params[:autocomplete]) + + respond_with(@autocomplete) + end + + # DELETE /admin/autocompletes/1 + # DELETE /admin/autocompletes/1.xml AJAX + #---------------------------------------------------------------------------- + def destroy + @autocomplete.destroy + + respond_with(@autocomplete) + end + + # GET /admin/autocompletes/1/confirm AJAX + #---------------------------------------------------------------------------- + def confirm + end +end diff --git a/app/controllers/autocompletes_controller.rb b/app/controllers/autocompletes_controller.rb new file mode 100644 index 0000000000..d4ae31d85c --- /dev/null +++ b/app/controllers/autocompletes_controller.rb @@ -0,0 +1,28 @@ +class AutocompletesController < ApplicationController + def get_results + if params[:id] && params[:name] + if list = Autocomplete.find_by_name(params[:id]) + + #construct regex /.*(?=.*TERM1.*)(?=.*TERM2.*) ... .*/ + r = ".*" + search_terms = params[:name].split(" ").each do |t| + r += "(?=.*#{t}.*)" + end + r += ".*" + + result = list.terms.select{ |s| s =~ /#{r}/i } + + result.collect do |t| + { value: t } + end + + render json: result + + else + render json: "list not found", status: 404 + end + else + render json: "list and search term(s) required", status: 500 + end + end +end \ No newline at end of file diff --git a/app/helpers/admin/autocompletes_helper.rb b/app/helpers/admin/autocompletes_helper.rb new file mode 100644 index 0000000000..f3be5076f8 --- /dev/null +++ b/app/helpers/admin/autocompletes_helper.rb @@ -0,0 +1,10 @@ +# Copyright (c) 2008-2013 Michael Dvorkin and contributors. +# +# Fat Free CRM is freely distributable under the terms of MIT license. +# See MIT-LICENSE file or http://www.opensource.org/licenses/mit-license.php +#------------------------------------------------------------------------------ +module Admin::AutocompletesHelper + def link_to_confirm(autocomplete) + link_to(t(:delete) + "?", confirm_admin_autocomplete_path(autocomplete), :method => :get, :remote => true) + end +end diff --git a/app/inputs/autocomplete_input.rb b/app/inputs/autocomplete_input.rb new file mode 100644 index 0000000000..fb2ea1ecc2 --- /dev/null +++ b/app/inputs/autocomplete_input.rb @@ -0,0 +1,39 @@ +# Copyright (c) 2008-2013 Michael Dvorkin and contributors. +# +# Fat Free CRM is freely distributable under the terms of MIT license. +# See MIT-LICENSE file or http://www.opensource.org/licenses/mit-license.php +#------------------------------------------------------------------------------ +class AutocompleteInput < SimpleForm::Inputs::Base + def input + field = get_field + + script = template.content_tag(:script, type: 'text/javascript') do + template.cdata_section("crm.text_with_autocomplete('#{@builder.object.class.to_s.downcase}_#{attribute_name}');") + end + + input_html_options.merge!('data-autocompleteUrl' => "/autocompletes/#{field.collection_string}/get_results") + + @builder.text_field(attribute_name, input_html_options) + script + end + + + + + + private + + # Autocomplete latches onto the 'text_with_autocomplete' class. + #------------------------------------------------------------------------------ + def input_html_classes + super.push('text_with_autocomplete') + end + + # Returns the field as field1 + #------------------------------------------------------------------------------ + def get_field + @field1 ||= Field.where(:name => attribute_name).first + end + + + ActiveSupport.run_load_hooks(:fat_free_crm_autocomplete_input, self) +end diff --git a/app/models/autocomplete.rb b/app/models/autocomplete.rb new file mode 100644 index 0000000000..c8cda9f37c --- /dev/null +++ b/app/models/autocomplete.rb @@ -0,0 +1,14 @@ +class Autocomplete < ActiveRecord::Base + validates_presence_of :name + validates_presence_of :terms + validates_uniqueness_of :name + + serialize :terms, Array + + def terms_string=(value) + self.terms = value.lines.map(&:squish).reject(&:blank?) + end + def terms_string + terms.try(:join, "\n") + end +end \ No newline at end of file diff --git a/app/models/fields/field.rb b/app/models/fields/field.rb index 9975be71d4..d45545a699 100644 --- a/app/models/fields/field.rb +++ b/app/models/fields/field.rb @@ -53,7 +53,8 @@ class Field < ActiveRecord::Base 'datetime' => {:klass => 'CustomField', :type => 'timestamp'}, 'decimal' => {:klass => 'CustomField', :type => 'decimal', :column_options => {:precision => 15, :scale => 2} }, 'integer' => {:klass => 'CustomField', :type => 'integer'}, - 'float' => {:klass => 'CustomField', :type => 'float'} + 'float' => {:klass => 'CustomField', :type => 'float'}, + 'autocomplete'=> {:klass => 'CustomField', :type => 'string'} }.with_indifferent_access validates_presence_of :label, :message => "^Please enter a field label." diff --git a/app/views/admin/autocompletes/_autocomplete.html.haml b/app/views/admin/autocompletes/_autocomplete.html.haml new file mode 100644 index 0000000000..35b7966a73 --- /dev/null +++ b/app/views/admin/autocompletes/_autocomplete.html.haml @@ -0,0 +1,8 @@ +%li.highlight[autocomplete] + .tools + = link_to_edit(autocomplete, :url => edit_admin_autocomplete_path(autocomplete)) << " |" + = link_to_confirm(autocomplete) + + %span.black= autocomplete.name + %tt + %dt diff --git a/app/views/admin/autocompletes/_confirm.html.haml b/app/views/admin/autocompletes/_confirm.html.haml new file mode 100644 index 0000000000..3603f1aeb4 --- /dev/null +++ b/app/views/admin/autocompletes/_confirm.html.haml @@ -0,0 +1,5 @@ +.confirm[@autocomplete, :confirm] + #{t :autocomplete_confirm_delete} + #{t :delete} #{@autocomplete.name}? + = link_to_confirm_delete(@autocomplete) << " : " + = link_to_function(t(:no_button), "crm.flick('#{dom_id(@autocomplete, :confirm)}', 'remove')") diff --git a/app/views/admin/autocompletes/_edit.html.haml b/app/views/admin/autocompletes/_edit.html.haml new file mode 100644 index 0000000000..53d8cf823b --- /dev/null +++ b/app/views/admin/autocompletes/_edit.html.haml @@ -0,0 +1,9 @@ +.remote + = form_for([:admin, @autocomplete], :as => :autocomplete, :html => one_submit_only.merge(:class => "edit_autocomplete", :id => "edit_autocomplete_#{@autocomplete.id}"), :remote => true) do |f| + = link_to_close edit_admin_autocomplete_path(@autocomplete) + = f.error_messages + = render :partial => "top_section", :locals => { :f => f, :edit => true } + .buttonbar + = f.submit t(:save_autocomplete), :id => :autocomplete_submit + or + = link_to_cancel edit_admin_autocomplete_path(@autocomplete) diff --git a/app/views/admin/autocompletes/_new.html.haml b/app/views/admin/autocompletes/_new.html.haml new file mode 100644 index 0000000000..5d5997ab87 --- /dev/null +++ b/app/views/admin/autocompletes/_new.html.haml @@ -0,0 +1,10 @@ +- path = new_admin_autocomplete_path + += form_for([:admin, @autocomplete], :html => one_submit_only, :remote => true) do |f| + = link_to_close path + = f.error_messages + = render :partial => "top_section", :locals => { :f => f } + .buttonbar + = f.submit t(:create_autocomplete), :id => :autocomplete_submit + or + = link_to_cancel path diff --git a/app/views/admin/autocompletes/_top_section.html.haml b/app/views/admin/autocompletes/_top_section.html.haml new file mode 100644 index 0000000000..2aa5ab1890 --- /dev/null +++ b/app/views/admin/autocompletes/_top_section.html.haml @@ -0,0 +1,10 @@ +.section + %table + %tr + %td + .label.top.req= t(:name) + ":" + = f.text_field :name + %tr + %td + .label.top.req= t(:terms) + "(one per line):" + = f.text_area :terms_string \ No newline at end of file diff --git a/app/views/admin/autocompletes/confirm.js.haml b/app/views/admin/autocompletes/confirm.js.haml new file mode 100644 index 0000000000..a281826068 --- /dev/null +++ b/app/views/admin/autocompletes/confirm.js.haml @@ -0,0 +1,7 @@ +- id = dom_id(@autocomplete, :confirm) + +if ($('##{id}').size() > 0) { +crm.flick('#{id}', 'remove'); +} else { +$('##{dom_id(@autocomplete)}').prepend('#{ j (render :partial => "confirm") }'); +} \ No newline at end of file diff --git a/app/views/admin/autocompletes/create.js.haml b/app/views/admin/autocompletes/create.js.haml new file mode 100644 index 0000000000..e58b97c3cd --- /dev/null +++ b/app/views/admin/autocompletes/create.js.haml @@ -0,0 +1,11 @@ +- if @autocomplete.valid? + $('#create_autocomplete_arrow').html(crm.COLLAPSED); + $('#create_autocomplete_title').html('#{ j t(:autocompletes) }'); + $('#create_autocomplete').slideUp(250); + $('#autocompletes').prepend('#{ j (render :partial => "autocomplete", :collection => [ @autocomplete ]) }'); + $('##{dom_id(@autocomplete)}').effect("highlight", { duration:1500 }); + crm.flick('empty', 'remove'); +- else + $('#create_autocomplete').html('#{ j render(:partial => 'new') }'); + $('#create_autocomplete').effect("shake", { duration:250, distance: 6 }); + $('#autocomplete_name').focus(); diff --git a/app/views/admin/autocompletes/destroy.js.haml b/app/views/admin/autocompletes/destroy.js.haml new file mode 100644 index 0000000000..d3c47a3c1f --- /dev/null +++ b/app/views/admin/autocompletes/destroy.js.haml @@ -0,0 +1,10 @@ +- id = dom_id(@autocomplete) + +- if @autocomplete.destroyed? + $('##{id}').css('background-color', '#ffe4e1').slideUp(250); +- else + crm.flick("#{dom_id(@autocomplete, :confirm)}, 'remove')"); + $('##{id}').effect("shake", { duration:250, distance: 6 }); + $('#flash').html('#{ j flash[:warning] }'); + crm.flash('warning'); + - flash[:warning] = nil diff --git a/app/views/admin/autocompletes/edit.js.haml b/app/views/admin/autocompletes/edit.js.haml new file mode 100644 index 0000000000..5b2b4a3634 --- /dev/null +++ b/app/views/admin/autocompletes/edit.js.haml @@ -0,0 +1,20 @@ +- id = dom_id(@autocomplete) + +- if params[:cancel].true? # <----------------- Hide [Edit autocomplete] + $('##{id}').replaceWith('#{ j render(:partial => "autocomplete", :collection => [ @autocomplete ]) }'); + +- else # <---------------------------------------- Show [Edit autocomplete] form. + + - if @previous # Hide open [Edit autocomplete] form if any. + - if @previous.is_a?(Autocomplete) # Previous autocomplete still exists? + $('##{dom_id(@previous)}').replaceWith('#{ j render(:partial => "autocomplete", :collection => [ @previous ]) }'); + - else + crm.flick('autocomplete_#{@previous}', 'remove'); + + -# Disable onMouseOver for the list item. + -# Hide [Create autocomplete] form if any. + -# Show [Edit autocomplete] form. + crm.highlight_off('#{id}'); + crm.hide_form('create_autocomplete'); + $('##{id}').html('#{ j render(:partial => "edit") }'); + $('#autocomplete_name').focus(); diff --git a/app/views/admin/autocompletes/index.html.haml b/app/views/admin/autocompletes/index.html.haml new file mode 100644 index 0000000000..5c6350b05f --- /dev/null +++ b/app/views/admin/autocompletes/index.html.haml @@ -0,0 +1,17 @@ += styles_for :autocomplete + +.title_tools + = link_to_inline(:create_autocomplete, new_admin_autocomplete_path, :text => t(:create_autocomplete)) + +.title + %span#create_autocomplete_title #{t :autocompletes} + = image_tag("loading.gif", :size => :thumb, :id => "loading", :style => "display: none;") +.remote#create_autocomplete{ hidden } + +.list#autocompletes + - if @autocompletes.any? + = render :partial => "admin/autocompletes/autocomplete", :collection => @autocompletes + - else + = render "shared/empty" + +#export= render "shared/export" diff --git a/app/views/admin/autocompletes/new.js.haml b/app/views/admin/autocompletes/new.js.haml new file mode 100644 index 0000000000..b7de58b619 --- /dev/null +++ b/app/views/admin/autocompletes/new.js.haml @@ -0,0 +1,7 @@ +crm.flip_form('create_autocomplete'); + +- unless params[:cancel].true? + $('#create_autocomplete').html('#{ j render(:partial => "new") }'); + crm.set_title('create_autocomplete', '#{t(:create_autocomplete)}'); +- else + crm.set_title('create_autocomplete', '#{t(:autocompletes)}'); diff --git a/app/views/admin/autocompletes/update.js.haml b/app/views/admin/autocompletes/update.js.haml new file mode 100644 index 0000000000..a29ea01fa3 --- /dev/null +++ b/app/views/admin/autocompletes/update.js.haml @@ -0,0 +1,9 @@ +- id = dom_id(@autocomplete) + +- if @autocomplete.errors.empty? + $('##{id}').replaceWith('#{ j render(:partial => "autocomplete", :collection => [ @autocomplete ]) }'); + $('##{id}').effect('highlight', { duration: 1000 }); +- else + $('##{id}').html('#{ j render(:partial => "edit") }'); + $('##{id}').effect("shake", { duration:250, distance: 6 }); + $('#tag_autocompletename').focus(); diff --git a/app/views/admin/custom_fields/_autocomplete_field.html.haml b/app/views/admin/custom_fields/_autocomplete_field.html.haml new file mode 100644 index 0000000000..2336685105 --- /dev/null +++ b/app/views/admin/custom_fields/_autocomplete_field.html.haml @@ -0,0 +1,25 @@ +%div + .label.top.req + = "Autocomplete list name:" += f.text_field :collection_string, :class => 'field_collection_string', :size => 78 + +%table + %tr + %td + + %td= spacer + %td + .label.top + %span Required: + = f.check_box :required + %br + %span Disabled: + = f.check_box :disabled + %tr + %td + .label.top Hint: + = f.text_field :hint + %td= spacer + %td + .label.top Placeholder: + = f.text_field :placeholder diff --git a/config/locales/en-US_fat_free_crm.yml b/config/locales/en-US_fat_free_crm.yml index 186a4e65a5..537b9f525f 100644 --- a/config/locales/en-US_fat_free_crm.yml +++ b/config/locales/en-US_fat_free_crm.yml @@ -40,6 +40,7 @@ en-US: admin_tab_settings: Settings admin_tab_plugins: Plugins admin_tab_imports: Import + admin_tab_autocompletes: Autocomplete affiliate: Affiliate competitor: Competitor @@ -973,6 +974,13 @@ en-US: version: Version description: Description + # Views -> Admin -> Autocompletes + #---------------------------------------------------------------------------- + autocompletes: Autocomplete lists + autocomplete_small: autocomplete + create_autocomplete: Create Autocomplete + save_autocomplete: Save Autocomplete + # Simple Form translations #---------------------------------------------------------------------------- simple_form: @@ -1024,6 +1032,8 @@ en-US: title: Number (Integer) float: title: Number (Floating Point) + autocomplete: + title: Autocomplete list pair: start: Start end: End diff --git a/config/routes.rb b/config/routes.rb index 1795a36191..80049d69a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -281,6 +281,11 @@ end end + resources :autocompletes, only: [], :id => /[a-zA-Z0-9]+/ do + get :get_results, :on => :member + end + + namespace :admin do resources :groups @@ -326,6 +331,12 @@ get :confirm end end + + resources :autocompletes, :except => [:show] do + member do + get :confirm + end + end resources :fields, :as => :custom_fields resources :fields, :as => :core_fields diff --git a/db/migrate/20141229005411_add_autocompletes.rb b/db/migrate/20141229005411_add_autocompletes.rb new file mode 100644 index 0000000000..401901932e --- /dev/null +++ b/db/migrate/20141229005411_add_autocompletes.rb @@ -0,0 +1,12 @@ +class AddAutocompletes < ActiveRecord::Migration + def up + create_table :autocompletes, :force => true do |t| + t.string :name + t.text :terms, :limit => 16.megabytes - 1 + end + end + + def down + drop_table :autocompletes + end +end diff --git a/db/schema.rb b/db/schema.rb index 16dba6b939..84478c2cdf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20141208230747) do +ActiveRecord::Schema.define(:version => 20141229005411) do create_table "account_aliases", :force => true do |t| t.integer "account_id" @@ -124,6 +124,11 @@ add_index "attendances", ["contact_id"], :name => "index_attendances_on_contact_id" add_index "attendances", ["event_instance_id"], :name => "index_attendances_on_event_instance_id" + create_table "autocompletes", :force => true do |t| + t.string "name" + t.text "terms", :limit => 16777215 + end + create_table "avatars", :force => true do |t| t.integer "user_id" t.integer "entity_id"