├── test ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── application_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── config │ │ ├── routes.rb │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── secrets.yml │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── config.ru │ ├── Rakefile │ └── README.rdoc ├── administa_test.rb ├── integration │ └── navigation_test.rb ├── controllers │ └── administa │ │ └── test_controller_test.rb └── test_helper.rb ├── app ├── assets │ ├── images │ │ └── administa │ │ │ └── .keep │ ├── javascripts │ │ └── administa │ │ │ ├── build │ │ │ ├── 448c34a56d699c29117adc64c43affeb.woff2 │ │ │ ├── 7799dece2c79854f63f09e7dfa528b88.jpg │ │ │ ├── e18bbf611f2a2e43afc071aa2f4e1512.ttf │ │ │ ├── f4769f9bdb7466be65088239c12046d1.eot │ │ │ └── fa2772327f55d8198301fdb8bcfc8158.woff │ │ │ └── application.js │ └── stylesheets │ │ └── administa │ │ └── application.css ├── helpers │ └── administa │ │ └── application_helper.rb ├── controllers │ └── administa │ │ ├── application_controller.rb │ │ ├── main_controller.rb │ │ ├── actions │ │ ├── new.rb │ │ ├── edit.rb │ │ ├── destroy.rb │ │ ├── show.rb │ │ ├── create.rb │ │ ├── update.rb │ │ └── index.rb │ │ ├── controller.rb │ │ ├── generic_controller.rb │ │ ├── error_handlers.rb │ │ └── base.rb └── views │ ├── administa │ └── main │ │ └── index.html.erb │ ├── application │ └── index.html.erb │ └── layouts │ └── administa │ └── application.html.erb ├── lib ├── administa │ ├── version.rb │ ├── engine.rb │ ├── model.rb │ ├── model │ │ ├── finder.rb │ │ ├── json.rb │ │ ├── relation.rb │ │ ├── attributes.rb │ │ └── options.rb │ ├── config.rb │ └── config │ │ ├── auth.rb │ │ ├── dynamic_controller.rb │ │ └── menu.rb ├── tasks │ └── administa_tasks.rake ├── generators │ └── administa │ │ ├── USAGE │ │ ├── templates │ │ └── controller.rb │ │ └── administa_generator.rb └── administa.rb ├── administa-demo.png ├── js ├── AppDispatcher.js ├── components │ ├── list │ │ ├── Header.react.js │ │ ├── Item.react.js │ │ ├── SearchBox.react.js │ │ ├── Pagination.react.js │ │ ├── List.react.js │ │ └── Selection.react.js │ ├── Footer.react.js │ ├── Main.react.js │ ├── ContentHeader.react.js │ ├── form │ │ ├── HasMany.react.js │ │ ├── HasOne.react.js │ │ ├── Through.react.js │ │ ├── BelongsTo.react.js │ │ ├── InputMixin.js │ │ ├── CollectionMixin.js │ │ ├── Input.react.js │ │ ├── Form.react.js │ │ └── Association.react.js │ ├── detail │ │ ├── Property.react.js │ │ └── Detail.react.js │ ├── Dialog.react.js │ ├── Dialogs.react.js │ ├── Header.react.js │ ├── Resource.react.js │ ├── Menu.react.js │ ├── PropertyMixin.js │ └── LinkMixin.js ├── actions │ ├── MenuActions.js │ ├── UserActions.js │ ├── DialogActions.js │ └── ResourceActions.js ├── Utils.js ├── Constants.js ├── plugin │ └── ForceReplaceSourceMapppingURLPlugin.js ├── stores │ ├── UserStore.js │ ├── MenuStore.js │ ├── AppStore.js │ ├── DialogStore.js │ └── ResourceStore.js ├── Loader.js └── app.js ├── config └── locales │ ├── administa.ja.yml │ └── administa.en.yml ├── .gitignore ├── webpack.config.release.js ├── bin └── rails ├── Gemfile ├── .eslintrc ├── README.md ├── administa.gemspec ├── Rakefile ├── MIT-LICENSE ├── package.json ├── webpack.config.js ├── Gemfile.lock └── css └── app.css /test/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/administa/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/administa/version.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | VERSION = "0.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /administa-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/administa-demo.png -------------------------------------------------------------------------------- /js/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | let Dispatcher = Flux.Dispatcher; 2 | 3 | export default new Dispatcher; 4 | -------------------------------------------------------------------------------- /app/helpers/administa/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module ApplicationHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | mount Administa::Engine => "/administa" 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /lib/tasks/administa_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :administa do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /app/controllers/administa/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class ApplicationController < ActionController::Base 3 | 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/administa_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdministaTest < ActiveSupport::TestCase 4 | test "truth" do 5 | assert_kind_of Module, Administa 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /lib/generators/administa/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate administa Thing 6 | 7 | This will create: 8 | what/will/it/create 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/administa/build/448c34a56d699c29117adc64c43affeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/448c34a56d699c29117adc64c43affeb.woff2 -------------------------------------------------------------------------------- /app/assets/javascripts/administa/build/7799dece2c79854f63f09e7dfa528b88.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/7799dece2c79854f63f09e7dfa528b88.jpg -------------------------------------------------------------------------------- /app/assets/javascripts/administa/build/e18bbf611f2a2e43afc071aa2f4e1512.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/e18bbf611f2a2e43afc071aa2f4e1512.ttf -------------------------------------------------------------------------------- /app/assets/javascripts/administa/build/f4769f9bdb7466be65088239c12046d1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/f4769f9bdb7466be65088239c12046d1.eot -------------------------------------------------------------------------------- /app/assets/javascripts/administa/build/fa2772327f55d8198301fdb8bcfc8158.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/fa2772327f55d8198301fdb8bcfc8158.woff -------------------------------------------------------------------------------- /config/locales/administa.ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | administa: 3 | flash: 4 | created: "%{name}(id: %{id}) が登録されました" 5 | updated: "%{name}(id: %{id}) が更新されました" 6 | deleted: "%{name}(id: %{id}) が削除されました" 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/controllers/administa/main_controller.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class MainController < Administa::ApplicationController 3 | include ::Administa::Base 4 | 5 | def index 6 | 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/integration/navigation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NavigationTest < ActionDispatch::IntegrationTest 4 | fixtures :all 5 | 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/generators/administa/templates/controller.rb: -------------------------------------------------------------------------------- 1 | class <%= "#{@options[:namespace].camelize}::#{class_name.pluralize}Controller" %> < <%= @options[:base_class] %> 2 | include Administa::Controller 3 | 4 | administa model: <%= class_name %> 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/administa/test_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Administa 4 | class TestControllerTest < ActionController::TestCase 5 | # test "the truth" do 6 | # assert true 7 | # end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/locales/administa.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | administa: 3 | flash: 4 | created: "%{name}(id: %{id}) was successfully created" 5 | updated: "%{name}(id: %{id}) was successfully updated" 6 | deleted: "%{name}(id: %{id}) was successfully deleted" 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /js/components/list/Header.react.js: -------------------------------------------------------------------------------- 1 | var PT = React.PropTypes 2 | 3 | export default React.createClass({ 4 | displayName: 'list/Header', 5 | 6 | propTypes: { 7 | label: PT.string 8 | }, 9 | 10 | render() { 11 | return { this.props.label } ; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /js/actions/MenuActions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../AppDispatcher'; 2 | import Constants from '../Constants'; 3 | 4 | export default { 5 | 6 | initialize(data) { 7 | AppDispatcher.dispatch({ 8 | type: Constants.MENU_INITIALIZED, 9 | data: data 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../AppDispatcher'; 2 | import Constants from '../Constants'; 3 | 4 | export default { 5 | 6 | initialize(data) { 7 | AppDispatcher.dispatch({ 8 | type: Constants.USER_INITIALIZED, 9 | data: data 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/administa/engine.rb: -------------------------------------------------------------------------------- 1 | 2 | module Administa 3 | class Engine < ::Rails::Engine 4 | 5 | isolate_namespace Administa 6 | 7 | config.after_initialize do 8 | if Rails.env.production? && (not Rails.groups.include?("assets")) 9 | Administa.config.initialize! 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | bower_components/ 10 | node_modules/ 11 | Gemfile.lock 12 | vendor/ 13 | .ruby-version 14 | test/lib/ 15 | 16 | app/assets/javascripts/administa/build/*.js.map 17 | -------------------------------------------------------------------------------- /js/components/Footer.react.js: -------------------------------------------------------------------------------- 1 | 2 | export default React.createClass({ 3 | render() { 4 | return( 5 | 10 | ); 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /js/components/Main.react.js: -------------------------------------------------------------------------------- 1 | import Header from 'components/Header.react'; 2 | import Menu from 'components/Menu.react'; 3 | import Footer from 'components/Footer.react'; 4 | 5 | export default React.createClass({ 6 | displayName: 'Main', 7 | render() { 8 | return ( 9 |
10 |
11 | ); 12 | } 13 | 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /app/views/administa/main/index.html.erb: -------------------------------------------------------------------------------- 1 |
6 | 7 | 14 | -------------------------------------------------------------------------------- /app/views/application/index.html.erb: -------------------------------------------------------------------------------- 1 |
6 | 7 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /webpack.config.release.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var config = require("./webpack.config.js"); 3 | 4 | config.plugins.push( 5 | new webpack.optimize.UglifyJsPlugin({ 6 | sourceMap: false, 7 | mangle: { 8 | except: ['$super', '$', 'jQuery', 'React', 'exports', 'require'] 9 | }, 10 | compress: { 11 | warnings: false, 12 | drop_console: true 13 | } 14 | }) 15 | ); 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/administa/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /js/actions/DialogActions.js: -------------------------------------------------------------------------------- 1 | 2 | import AppDispatcher from '../AppDispatcher'; 3 | import Constants from '../Constants'; 4 | 5 | export default { 6 | 7 | open(name, component, props) { 8 | AppDispatcher.dispatch({ 9 | type: Constants.DIALOG_OPENED, 10 | data: { 11 | name: name, 12 | component: component, 13 | props: props 14 | } 15 | }); 16 | }, 17 | 18 | close(name) { 19 | AppDispatcher.dispatch({ 20 | type: Constants.DIALOG_CLOSED, 21 | data: { 22 | name: name, 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/new.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module New 4 | 5 | def new 6 | 7 | @result = index_result 8 | 9 | resource = model.klass.new 10 | 11 | @result[:id] = nil 12 | @result[:resource] = model.as_json(resource, action: :create) 13 | 14 | respond_to do |format| 15 | format.html { render :index } 16 | format.json { render json: to_json(@result) } 17 | end 18 | rescue => e 19 | handle_exception(e) 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /js/components/ContentHeader.react.js: -------------------------------------------------------------------------------- 1 | export default React.createClass({ 2 | displayName: 'ContentHeader', 3 | 4 | render() { 5 | return ( 6 | 7 |
8 |

9 | Page Header 10 | Optional description 11 |

12 |
    13 |
  1. Level
  2. 14 |
  3. Here
  4. 15 |
16 |
17 | ); 18 | } 19 | }); 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/edit.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Edit 4 | 5 | def edit 6 | 7 | @result = index_result 8 | resource = model.find(params[:id], action: :edit) 9 | 10 | @result[:id] = resource.id 11 | @result[:resource] = model.as_json(resource, action: :edit) 12 | 13 | respond_to do |format| 14 | format.html { render :index } 15 | format.json { render json: to_json(@result) } 16 | end 17 | rescue => e 18 | handle_exception(e) 19 | end 20 | 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in administa.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | 16 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 1, 2 ], 4 | "quotes": [ 1, "double" ], 5 | "linebreak-style": [ 2, "unix" ], 6 | "semi": [ 2, "always" ], 7 | "comma-dangle": [2, "always-multiline"], 8 | "no-console": [1], 9 | 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "browser" : true 15 | }, 16 | "extends": "eslint:recommended", 17 | "ecmaFeatures": { 18 | "jsx": true, 19 | "modules": true, 20 | "experimentalObjectRestSpread": true 21 | }, 22 | "globals" :{ 23 | "React": true, 24 | "jQuery": true, 25 | }, 26 | "plugins": [ 27 | "react" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/destroy.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Destroy 4 | 5 | def destroy 6 | 7 | resource = model.find(params[:id], action: :edit) 8 | 9 | unless resource.destroy 10 | handle_validate_errors(resource.errors) 11 | end 12 | 13 | @result = index_result 14 | @result["flash"] = I18n.t("administa.flash.deleted", name: model.label, id: resource.id ) 15 | 16 | respond_to do |format| 17 | format.html { render :index } 18 | format.json { render json: to_json(@result) } 19 | end 20 | rescue => e 21 | handle_exception(e) 22 | end 23 | 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Administa 2 | 3 | The administration console framework/generator for Ruby on Rails. 4 | Supported rails versions are Rails3.2+ and Rails4. 5 | 6 | Administa is under heavy development, no tests, no documents ( ꒪⌓꒪) 7 | 8 | ## Demo 9 | 10 | [Demo Application](https://administa-demo.herokuapp.com/) 11 | 12 | [Demo Application Source Code](https://github.com/yuroyoro/administa_demo) 13 | 14 | ![](https://raw.githubusercontent.com/yuroyoro/administa/master/administa-demo.png) 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | gem 'administa' 21 | 22 | And then execute: 23 | 24 | $ bundle 25 | 26 | Or install it yourself as: 27 | 28 | $ gem install administa 29 | -------------------------------------------------------------------------------- /lib/administa.rb: -------------------------------------------------------------------------------- 1 | require "administa/engine" 2 | require "administa/config" 3 | require "administa/model" 4 | 5 | # 6 | # TODO 7 | # - animation入れる 8 | # - test書きたい 9 | # - ESLInt 10 | # 11 | # Done; 12 | # - 認証 13 | # - has_many対応 14 | # - has_many through対応 15 | # - validation 16 | # - 子associationをnewした後、再度ダイアログ開くと、選択されているassociationの情報が消える 17 | # - メッセージ国際化対応 18 | # - メニュー 19 | # - メニュー階層化 20 | # - file upload 21 | # - enum 22 | # - boolean 23 | # - genratorつける 24 | # - permalink 25 | # - datetime picker 26 | # - 削除 27 | # 28 | module Administa 29 | 30 | def self.config(&block) 31 | @config ||= Administa::Config.new 32 | if block_given? 33 | yield @config 34 | end 35 | 36 | @config 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /js/Utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | present(obj) { 3 | return obj && Object.keys(obj).length > 0; 4 | }, 5 | 6 | empty(obj) { 7 | return !obj || Object.keys(obj).length == 0; 8 | }, 9 | 10 | isPrimitive(obj) { 11 | return !( (obj instanceof File ) || (obj instanceof Array) || (obj instanceof Object)); 12 | }, 13 | 14 | reportError (message, error, stack) { 15 | // e instanceof ErrorEvent 16 | jQuery("#resultLoading").hide(); 17 | 18 | var html = `

javascript error : ${message}

`; 19 | var stack = stack || (error && error.stack); 20 | if( stack ) { 21 | html += `
${stack}
` 22 | } 23 | html += "
" 24 | 25 | jQuery("body").prepend(html); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/administa/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/show.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Show 4 | 5 | def show 6 | 7 | @result = show_result(params[:id]) 8 | 9 | respond_to do |format| 10 | format.html { render :index } 11 | format.json { render json: to_json(@result) } 12 | end 13 | rescue => e 14 | handle_exception(e) 15 | end 16 | 17 | protected 18 | def show_result(id) 19 | result = request.xhr? ? default_result : index_result 20 | 21 | resource = model.find(id, action: :show) 22 | 23 | result[:id] = resource.id 24 | result[:resource] = model.as_json(resource, action: :show) 25 | result 26 | end 27 | 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /js/Constants.js: -------------------------------------------------------------------------------- 1 | 2 | var keyMirror = function(obj) { 3 | var ret = {}; 4 | var key; 5 | if (!(obj instanceof Object && !Array.isArray(obj))) { 6 | throw new Error('keyMirror(...): Argument must be an object.'); 7 | } 8 | for (key in obj) { 9 | if (obj.hasOwnProperty(key)) { 10 | ret[key] = key; 11 | } 12 | } 13 | return ret; 14 | }; 15 | 16 | export default keyMirror({ 17 | INITIALIZE: null, 18 | APP_TRANSITION: null, 19 | RESOURCE_BUILD: null, 20 | RESOURCE_FETCH: null, 21 | RESOURCE_LIST: null, 22 | RESOURCE_UPDATED: null, 23 | RESOURCE_INVALID: null, 24 | RESOURCE_DELETED: null, 25 | DIALOG_OPENED: null, 26 | DIALOG_CLOSED: null, 27 | MENU_INITIALIZED: null, 28 | USER_INITIALIZED: null, 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/administa/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | // Load packed libraries js file that compiled by webpack 14 | //= require ./build/vendors 15 | // 16 | // Incudes bundled js that compiled by webpack 17 | //= require ./build/app.bundle 18 | -------------------------------------------------------------------------------- /app/controllers/administa/controller.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Controller 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | include ::Administa::Base 7 | include ::Administa::Actions::Index 8 | include ::Administa::Actions::Show 9 | include ::Administa::Actions::New 10 | include ::Administa::Actions::Create 11 | include ::Administa::Actions::Edit 12 | include ::Administa::Actions::Update 13 | include ::Administa::Actions::Destroy 14 | include ::Administa::ErrorHandlers 15 | 16 | respond_to :html, :json 17 | 18 | class_attribute :model, :instance_write => false 19 | end 20 | 21 | module ClassMethods 22 | 23 | def administa(model:, **options) 24 | self.model = Administa::Model.new(self, model, options) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /js/plugin/ForceReplaceSourceMapppingURLPlugin.js: -------------------------------------------------------------------------------- 1 | function ForceReplaceSourceMapppingURLPlugin(patterns) { 2 | this.patterns = patterns; 3 | } 4 | 5 | module.exports = ForceReplaceSourceMapppingURLPlugin; 6 | ForceReplaceSourceMapppingURLPlugin.prototype.apply = function(compiler) { 7 | var patterns = this.patterns; 8 | compiler.plugin("compilation", function(compilation) { 9 | compilation.plugin("after-optimize-assets", function(assets) { 10 | patterns.forEach(function(pattern) { 11 | asset = assets[pattern.asset]; 12 | if(asset) { 13 | var children = asset.children; 14 | var comment = children.pop(); 15 | children.push( 16 | comment.replace(/sourceMappingURL=(.+)/, 'sourceMappingURL=' + pattern.sourceMappingURL) 17 | ); 18 | } 19 | }); 20 | }); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /administa.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "administa/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "administa" 9 | s.version = Administa::VERSION 10 | s.authors = ["Tomohito Ozaki"] 11 | s.email = ["ozaki@yuroyoro.com"] 12 | s.homepage = "https://github.com/yuroyoro/administa" 13 | s.summary = "The administration console framework/generator" 14 | s.description = "The administration console framework/generator" 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency "rails", '>= 3.2', '< 5.0' 21 | 22 | s.add_development_dependency "sqlite3" 23 | end 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Administa' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | require 'rake/testtask' 28 | 29 | Rake::TestTask.new(:test) do |t| 30 | t.libs << 'lib' 31 | t.libs << 'test' 32 | t.pattern = 'test/**/*_test.rb' 33 | t.verbose = false 34 | end 35 | 36 | 37 | task default: :test 38 | -------------------------------------------------------------------------------- /js/components/form/HasMany.react.js: -------------------------------------------------------------------------------- 1 | import InputMixin from './InputMixin'; 2 | import CollectionMixin from './CollectionMixin'; 3 | import PropertyMixin from 'components/PropertyMixin'; 4 | 5 | export default React.createClass({ 6 | displayName: 'form/HasMany', 7 | 8 | mixins: [PropertyMixin, InputMixin, CollectionMixin], 9 | 10 | getFormValue() { 11 | var keys = Object.keys(this.refs); 12 | var result = {}; 13 | var targets = []; 14 | var name = this.props.column.association.name; 15 | 16 | for (var i = 0, len = keys.length; i < len; i++) { 17 | var key = keys[i]; 18 | var property = this.refs[key]; 19 | if(property.isDirty()) { 20 | var value = property.getFormValue(); 21 | targets.push(value[name]); 22 | } 23 | } 24 | 25 | result[name] = targets; 26 | return result; 27 | }, 28 | 29 | }) 30 | -------------------------------------------------------------------------------- /app/controllers/administa/generic_controller.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class GenericController < ActionController::Base 3 | include Administa::Controller 4 | 5 | if Rails.version < "4.0" 6 | before_filter :reject_except_json 7 | else 8 | before_action :reject_except_json 9 | end 10 | 11 | def model 12 | return @model if @model 13 | 14 | name = params[:model].try(:camelize).try(:singularize) 15 | if @model = Administa.config.models[name] 16 | return @model 17 | end 18 | 19 | model = name.try(:safe_constantize) 20 | @model = Administa::Model.new(self, model, {}) 21 | @model.setup_options! 22 | @model 23 | end 24 | 25 | def reject_except_json 26 | unless request.format == :json 27 | render :nothing => true, :status => 406 28 | end 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/administa/model.rb: -------------------------------------------------------------------------------- 1 | require "administa/model/options" 2 | require "administa/model/finder" 3 | require "administa/model/relation" 4 | require "administa/model/json" 5 | require "administa/model/attributes" 6 | 7 | module Administa 8 | class Model 9 | include Administa::Model::Options 10 | include Administa::Model::Finder 11 | include Administa::Model::Json 12 | include Administa::Model::Attributes 13 | 14 | attr_accessor :name, :controller, :klass, :options, :given_options 15 | def initialize(controller, klass, options = {}) 16 | self.name = klass.name.underscore 17 | self.controller = controller 18 | self.klass = klass 19 | self.given_options = options 20 | 21 | Administa.config.add_model(self) 22 | end 23 | 24 | delegate :table_name, :column_names, :columns_hash, :arel_table, :to => :klass 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/administa/model/finder.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class Model 3 | module Finder 4 | def all 5 | rel = (Rails.version < "4.0") ? klass.scoped : klass.all 6 | Administa::Model::Relation.new(self, rel) 7 | end 8 | 9 | def select(options = {}) 10 | options = extract_options_for_query(options) 11 | all.select(options) 12 | end 13 | 14 | def find(id, options = {}) 15 | options = extract_options_for_query(options) 16 | 17 | klass.includes(options[:includes]).find(id) 18 | end 19 | 20 | private 21 | def extract_options_for_query(options = {}) 22 | options = options.dup 23 | action = options.delete(:action) 24 | options[:includes] = options[:includes] || action.try{|act| self.includes(act) } || {} 25 | 26 | options 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)] 6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) 7 | require "rails/test_help" 8 | 9 | # Filter out Minitest backtrace while allowing backtrace from other libraries 10 | # to be shown. 11 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new 12 | 13 | # Load support files 14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 15 | 16 | # Load fixtures from the engine 17 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 18 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 19 | ActiveSupport::TestCase.fixtures :all 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/create.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Create 4 | 5 | def create 6 | 7 | resource = model.klass.new 8 | 9 | # TODO: strong parameters 10 | params.permit! if params.respond_to? :permit! 11 | 12 | attrs = params["resource"] 13 | 14 | model.assign(resource, attrs) 15 | unless resource.save! 16 | handle_validate_errors(resource.errors) 17 | return 18 | end 19 | 20 | @result = show_result(resource.id) 21 | @result["flash"] = I18n.t("administa.flash.created", name: model.label, id: resource.id ) 22 | 23 | respond_to do |format| 24 | format.html { render :index } 25 | format.json { render json: to_json(@result) } 26 | end 27 | rescue => e 28 | handle_exception(e) 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/views/layouts/administa/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Administa 5 | 6 | <%= stylesheet_link_tag "administa/application", media: "all" %> 7 | <%= javascript_include_tag "administa/application" %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= yield %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/update.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Update 4 | 5 | def update 6 | 7 | resource = model.find(params[:id], action: :edit) 8 | 9 | # TODO: strong parameters 10 | params.permit! if params.respond_to? :permit! 11 | 12 | attrs = params["resource"] 13 | 14 | model.assign(resource, attrs) 15 | unless resource.save! 16 | handle_validate_errors(resource.errors) 17 | return 18 | end 19 | 20 | @result = show_result(params[:id]) 21 | @result["flash"] = I18n.t("administa.flash.updated", name: model.label, id: resource.id ) 22 | 23 | respond_to do |format| 24 | format.html { render :index } 25 | format.json { render json: to_json(@result) } 26 | end 27 | rescue => e 28 | handle_exception(e) 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /js/components/detail/Property.react.js: -------------------------------------------------------------------------------- 1 | import PropertyMixin from 'components/PropertyMixin'; 2 | 3 | export default React.createClass({ 4 | displayName: 'detail/Property', 5 | 6 | mixins: [PropertyMixin], 7 | 8 | propTypes: { 9 | column: React.PropTypes.object.isRequired, 10 | resource: React.PropTypes.object.isRequired, 11 | settings: React.PropTypes.object.isRequired, 12 | 13 | }, 14 | 15 | render() { 16 | var column = this.props.column; 17 | var name = this.toProperyName(column); 18 | 19 | var resource = this.props.resource; 20 | 21 | var value = this.toLabel(column, resource, 22 | { search_columns: this.props.settings.search_columns, wrap_tag: true } 23 | ); 24 | 25 | return( 26 |
27 | 28 |
{ value }
29 |
30 | ); 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /js/stores/UserStore.js: -------------------------------------------------------------------------------- 1 | import Events from 'events' 2 | import Constants from '../Constants'; 3 | import AppDispatcher from '../AppDispatcher'; 4 | import assign from 'object-assign'; 5 | 6 | var _user ={}; 7 | var UserStore = assign({}, Events.EventEmitter.prototype, { 8 | 9 | emitEvent() { 10 | this.emit(); 11 | }, 12 | 13 | addEventListener(callback) { 14 | this.on("user:change", callback); 15 | }, 16 | 17 | removeEventListener(callback) { 18 | this.removeListener("user:change", callback); 19 | }, 20 | 21 | setState(user) { 22 | _user = user; 23 | }, 24 | 25 | getState() { 26 | return _user; 27 | }, 28 | }); 29 | 30 | AppDispatcher.register((action) => { 31 | switch(action.type) { 32 | case Constants.USER_INITIALIZED: 33 | var data = action.data; 34 | UserStore.setState(data); 35 | UserStore.emitEvent(); 36 | 37 | break; 38 | 39 | default: // no op 40 | } 41 | }); 42 | export default UserStore; 43 | -------------------------------------------------------------------------------- /js/stores/MenuStore.js: -------------------------------------------------------------------------------- 1 | import Events from 'events' 2 | import Constants from '../Constants'; 3 | import AppDispatcher from '../AppDispatcher'; 4 | import assign from 'object-assign'; 5 | 6 | var _menus = []; 7 | var MenuStore = assign({}, Events.EventEmitter.prototype, { 8 | 9 | emitEvent() { 10 | this.emit(); 11 | }, 12 | 13 | addEventListener(callback) { 14 | this.on("menu:change", callback); 15 | }, 16 | 17 | removeEventListener(callback) { 18 | this.removeListener("menu:change", callback); 19 | }, 20 | 21 | setState(menus) { 22 | _menus = menus; 23 | }, 24 | 25 | getState() { 26 | return _menus; 27 | }, 28 | }); 29 | 30 | AppDispatcher.register((action) => { 31 | switch(action.type) { 32 | case Constants.MENU_INITIALIZED: 33 | var data = action.data; 34 | MenuStore.setState(data); 35 | MenuStore.emitEvent(); 36 | 37 | break; 38 | 39 | default: // no op 40 | } 41 | }); 42 | export default MenuStore; 43 | -------------------------------------------------------------------------------- /test/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 83ba22679f6fce98d247dd6cf874b5fd25929533a455911124a8998503baf660d43c48d7ae66bcb62076de4aafc198df98ed6998290ea3d9cc57ad650d7b366d 15 | 16 | test: 17 | secret_key_base: 120d8b6712def827e6293da2abc8bb3c87c1a3687e4f5f294f1b25c940bdf72a72014c829e0884d0c6b358e5a143b9b0b06df6dbb19f0fac5935b7c36a5af729 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Tomohito Ozaki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/controllers/administa/error_handlers.rb: -------------------------------------------------------------------------------- 1 | 2 | module Administa 3 | module ErrorHandlers 4 | 5 | def handle_exception(exception, status = 400) 6 | case exception 7 | when ActiveRecord::RecordInvalid 8 | handle_validate_errors(exception.record.errors) 9 | return 10 | when ActiveRecord::RecordNotFound 11 | render :nothing => true, :status => 404 12 | return 13 | when ActiveRecord::PreparedStatementInvalid 14 | render :nothing => true, :status => 400 15 | return 16 | else 17 | raise exception 18 | return 19 | end 20 | end 21 | 22 | def handle_validate_errors(*errors) 23 | errors = errors.as_json 24 | errors = errors.first if errors.is_a? Array 25 | errors[self.model..name.underscore] = I18n.t(:invalid, :scope => "errors.messages") if errors.blank? 26 | 27 | respond_to do |format| 28 | format.json { render :json => {:errors => errors}.as_json, :status => 422 } 29 | format.any { render :nothing => true, :status => 406 } 30 | end 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "administa" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | 22 | # Do not swallow errors in after_commit/after_rollback callbacks. 23 | config.active_record.raise_in_transactional_callbacks = true 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /js/components/form/HasOne.react.js: -------------------------------------------------------------------------------- 1 | import InputMixin from './InputMixin'; 2 | import PropertyMixin from 'components/PropertyMixin'; 3 | 4 | export default React.createClass({ 5 | displayName: 'form/HasOne', 6 | 7 | mixins: [PropertyMixin, InputMixin], 8 | 9 | getFormValue() { 10 | return this.refs.association.getFormValue(); 11 | }, 12 | 13 | getResourceValue() { 14 | return this.refs.association.getResourceValue(); 15 | }, 16 | 17 | isDirty() { 18 | return this.refs.association.isDirty() 19 | }, 20 | 21 | hasError() { 22 | return this.props.invalid; 23 | }, 24 | 25 | render() { 26 | var column = this.props.column; 27 | var name = column.name; 28 | var label = this.toProperyName(column); 29 | var association = this.props.column.association; 30 | var target = this.props.resource[association.name]; 31 | 32 | return( 33 |
34 | 35 | 36 | { this.createAssociation(target, {ref: 'association'}) } 37 | { this.errorsBlock(label) } 38 |
39 | ); 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /js/stores/AppStore.js: -------------------------------------------------------------------------------- 1 | import Events from 'events' 2 | import Constants from '../Constants'; 3 | import AppDispatcher from '../AppDispatcher'; 4 | import assign from 'object-assign'; 5 | 6 | var _app={}; 7 | var AppStore = assign({}, Events.EventEmitter.prototype, { 8 | 9 | emitEvent() { 10 | this.emit("app:change"); 11 | }, 12 | 13 | addEventListener(callback) { 14 | this.on("app:change", callback); 15 | }, 16 | 17 | removeEventListener(callback) { 18 | this.removeListener("app:change", callback); 19 | }, 20 | 21 | transitionTo(route, params, query) { 22 | _app.transitionTo = { 23 | route: route, 24 | params: params, 25 | query: query, 26 | } 27 | }, 28 | 29 | setState(app) { 30 | _app = app; 31 | }, 32 | 33 | getState() { 34 | return _app; 35 | }, 36 | }); 37 | 38 | AppDispatcher.register((action) => { 39 | switch(action.type) { 40 | case Constants.APP_TRANSITION: 41 | AppStore.transitionTo(action.route, action.params, action.query); 42 | AppStore.emitEvent(); 43 | 44 | break; 45 | 46 | default: // no op 47 | } 48 | }); 49 | export default AppStore; 50 | -------------------------------------------------------------------------------- /app/controllers/administa/actions/index.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Actions 3 | module Index 4 | 5 | def index 6 | @result = index_result 7 | 8 | respond_to do |format| 9 | format.html 10 | format.json { render json: to_json(@result) } 11 | end 12 | rescue => e 13 | handle_exception(e) 14 | end 15 | 16 | protected 17 | def default_result 18 | { 19 | name: model.name.pluralize, 20 | settings: model.settings, 21 | csrf_token: form_authenticity_token, 22 | } 23 | 24 | end 25 | 26 | def index_result 27 | page = params[:page].try(&:to_i) 28 | limit = params[:limit].try(&:to_i) 29 | order = params[:order] 30 | q = params[:q] 31 | 32 | resources = model. 33 | select(action: :index). 34 | filter_by_keywords(q). 35 | order(order). 36 | paginate(page: page, limit:limit) 37 | 38 | result = default_result 39 | result[:resources] = model.as_json(resources.to_a, action: :index) 40 | result[:pagination] = resources.pagination_metadata.merge(:q => q) 41 | 42 | result 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /js/components/Dialog.react.js: -------------------------------------------------------------------------------- 1 | import DialogActions from 'actions/DialogActions'; 2 | 3 | export default React.createClass({ 4 | displayName: 'Dialog', 5 | 6 | propTypes: { 7 | name: React.PropTypes.string, 8 | opened: React.PropTypes.bool, 9 | component: React.PropTypes.func, 10 | componentProps: React.PropTypes.object, 11 | onclose: React.PropTypes.func, 12 | index: React.PropTypes.number, 13 | }, 14 | 15 | close() { 16 | if(this.props.onclose) { 17 | this.props.onclose(); 18 | } 19 | DialogActions.close(this.props.name); 20 | }, 21 | 22 | render() { 23 | if( this.props.opened && this.props.component) { 24 | var InnerComponent = this.props.component; 25 | 26 | var index = this.props.index; 27 | var zindex = 1000 + ((index + 1) * 10); 28 | var style = { 29 | top: (index * 20) + "px", 30 | left: (index * 20) + "px", 31 | }; 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | ); 42 | } else { 43 | return
; 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "administa", 3 | "version": "1.0.0", 4 | "description": "= Administa", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "engine-strict": true, 10 | "engines": { 11 | "node": ">=4.6.0" 12 | }, 13 | "scripts": { 14 | "watch": "webpack --progress --profile --colors -d -w", 15 | "release": "webpack --progress --profile --colors --optimize-occurence-order --optimize-dedupe --config webpack.config.release.js" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "babel-core": "^5.8.34", 21 | "babel-loader": "^5.4.0", 22 | "bower": "^1.4.1", 23 | "css-loader": "^0.23.0", 24 | "expose-loader": "^0.7.1", 25 | "file-loader": "^0.8.1", 26 | "less": "^2.5.0", 27 | "less-loader": "^2.2.0", 28 | "style-loader": "^0.13.0", 29 | "url-loader": "^0.5.5", 30 | "webpack": "^1.7.3" 31 | }, 32 | "dependencies": { 33 | "extract-text-webpack-plugin": "^0.9.1", 34 | "jquery-datetimepicker": "^2.4.0", 35 | "moment": "^2.10.6", 36 | "react": "~0.13.1", 37 | "jquery": "~2.1.3", 38 | "lodash": "~3.6.0", 39 | "admin-lte": "2.3.2", 40 | "bootstrap": "~3.3.4", 41 | "react-router": "~0.13.2", 42 | "flux": "~2.0.2", 43 | "object-assign": "~2.0.0", 44 | "jqnotifybar": "https://github.com/yuroyoro/jQuery-Notify-bar", 45 | "left-pad": "~1.1.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /js/components/Dialogs.react.js: -------------------------------------------------------------------------------- 1 | import DialogStore from 'stores/DialogStore'; 2 | import Dialog from './Dialog.react'; 3 | 4 | export default React.createClass({ 5 | displayName: 'Dialogs', 6 | 7 | getInitialState: function() { 8 | return DialogStore.getAllState(); 9 | }, 10 | 11 | componentDidMount() { 12 | DialogStore.addEventListener("*", this._onChange); 13 | }, 14 | 15 | componentWillUnmount() { 16 | DialogStore.removeEventListener("*", this._onChange); 17 | }, 18 | 19 | _onChange(e) { 20 | var st = DialogStore.getAllState(); 21 | 22 | this.setState(st); 23 | // this.setState(ResourceStore.getState(this.props.params.name)); 24 | }, 25 | 26 | render() { 27 | 28 | var dialogs = Object.keys(this.state). 29 | map((k) => { return this.state[k]; }). 30 | filter((d) => { return d.opened; }). 31 | sort((d1, d2) => { return d1 > d2 ; }). 32 | map((dialog) => { 33 | return (); 42 | }); 43 | 44 | return ( 45 |
46 | { dialogs } 47 |
48 | ); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /js/components/Header.react.js: -------------------------------------------------------------------------------- 1 | import UserStore from 'stores/UserStore'; 2 | 3 | export default React.createClass({ 4 | displayName: 'Header', 5 | 6 | getInitialState() { 7 | return { user: UserStore.getState() }; 8 | }, 9 | 10 | componentDidMount() { 11 | UserStore.addEventListener(this._onChange); 12 | }, 13 | 14 | componentWillUnmount() { 15 | UserStore.removeEventListener(this._onChange); 16 | }, 17 | 18 | _onChange() { 19 | var user = UserStore.getState(); 20 | this.setState({ user: user}); 21 | }, 22 | 23 | render() { 24 | return( 25 |
26 | Administa 27 | 42 |
43 | ); 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /js/components/form/Through.react.js: -------------------------------------------------------------------------------- 1 | import InputMixin from './InputMixin'; 2 | import CollectionMixin from './CollectionMixin'; 3 | import PropertyMixin from 'components/PropertyMixin'; 4 | 5 | export default React.createClass({ 6 | displayName: 'form/Through', 7 | 8 | mixins: [PropertyMixin, InputMixin, CollectionMixin], 9 | 10 | getFormValue() { 11 | var keys = Object.keys(this.refs); 12 | var result = {}; 13 | var targets = []; 14 | var ids = []; 15 | var name = this.props.column.association.name; 16 | var dirty = false; 17 | 18 | for (var i = 0, len = keys.length; i < len; i++) { 19 | var key = keys[i]; 20 | var property = this.refs[key]; 21 | var target = property.state.target; 22 | if(target && target.id) { 23 | ids.push(target.id); 24 | } 25 | if(property.isDirty()) { 26 | dirty = true 27 | switch (property.state.reason) { 28 | case 'created': 29 | case 'edited': 30 | var value = property.getFormValue(); 31 | targets.push(value[name]); 32 | break; 33 | case 'selected': 34 | break; 35 | case 'cleared': 36 | if( property.state.destroy ) { 37 | targets.push(property.state.destroy); 38 | } 39 | } 40 | } 41 | } 42 | 43 | if( targets.length > 0) { 44 | result[name] = targets; 45 | } 46 | if(dirty) { 47 | result[this.props.column.association.foreign_key] = ids; 48 | } 49 | return result; 50 | }, 51 | }) 52 | -------------------------------------------------------------------------------- /js/components/form/BelongsTo.react.js: -------------------------------------------------------------------------------- 1 | import InputMixin from './InputMixin'; 2 | import PropertyMixin from 'components/PropertyMixin'; 3 | 4 | export default React.createClass({ 5 | displayName: 'form/BelongsTo', 6 | 7 | mixins: [PropertyMixin, InputMixin], 8 | 9 | getFormValue() { 10 | var value = this.getResourceValue(); 11 | var association = this.props.column.association; 12 | if(!(association.create || association.update)) { 13 | // remove if readonly 14 | delete value[association.name]; 15 | } 16 | 17 | return value; 18 | }, 19 | 20 | getResourceValue() { 21 | var value = {}; 22 | var name = this.props.column.name; 23 | value[name] = null; 24 | var target = this.refs.association.state.target; 25 | if(target){ 26 | var association_name = this.props.column.association.name; 27 | 28 | value[association_name + "_id"] = target.id; 29 | value[association_name] = target; 30 | } 31 | 32 | return value; 33 | }, 34 | 35 | isDirty() { 36 | return this.refs.association.isDirty() 37 | }, 38 | 39 | hasError() { 40 | return this.props.invalid; 41 | }, 42 | 43 | render() { 44 | var column = this.props.column; 45 | var name = column.name; 46 | var label = this.toProperyName(column); 47 | var association = this.props.column.association; 48 | var target = this.props.resource[association.name]; 49 | 50 | return( 51 |
52 | 53 | 54 | { this.createAssociation(target, {ref: 'association'}) } 55 | { this.errorsBlock(label) } 56 |
57 | ); 58 | }, 59 | }) 60 | -------------------------------------------------------------------------------- /js/components/list/Item.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | import ResourceActions from 'actions/ResourceActions'; 3 | import LinkMixin from 'components/LinkMixin'; 4 | import PropertyMixin from 'components/PropertyMixin'; 5 | 6 | var Link = Router.Link; 7 | var PT = React.PropTypes 8 | 9 | export default React.createClass({ 10 | displayName: 'list/Item', 11 | 12 | mixins: [LinkMixin, PropertyMixin, Router.Navigation ], 13 | 14 | propTypes: { 15 | name: PT.string.isRequired, 16 | resource: PT.object.isRequired, 17 | columns: PT.array.isRequired, 18 | search_columns: PT.array.isRequired, 19 | pagination: PT.object.isRequired, 20 | onclick: PT.func, 21 | }, 22 | 23 | showLink(resource){ 24 | 25 | var attrs = this.linkAttrs(this.props.name, resource.id, this.props.pagination); 26 | attrs.label = 'show'; 27 | 28 | return this.linkToShow(attrs); 29 | }, 30 | 31 | 32 | render() { 33 | var resource = this.props.resource; 34 | var classes = this.props.selected ? "info" : ""; 35 | 36 | var cols = this.props.columns.map((col) => { 37 | var label = this.toLabel( col, resource, 38 | { search_columns: this.props.search_columns, wrap_tag: true, ellipsis: true } 39 | ); 40 | var title = this.toLabel( col, resource, 41 | { search_columns: this.props.search_columns, wrap_tag: false, ellipsis: true} 42 | ); 43 | 44 | return ({ label }); 45 | }); 46 | 47 | return( 48 | 49 | { cols } 50 | 51 | ); 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /lib/administa/config.rb: -------------------------------------------------------------------------------- 1 | require "administa/config/auth" 2 | require "administa/config/menu" 3 | require "administa/config/dynamic_controller" 4 | 5 | module Administa 6 | class Config 7 | include ::Administa::Config::Auth 8 | include ::Administa::Config::Menu 9 | include ::Administa::Config::DynamicController 10 | 11 | attr_reader :models, :controllers, :timezone_obj 12 | 13 | def initialized? 14 | !!@initialized 15 | end 16 | 17 | # Initialization of Administa.config is delayed until 18 | # the first request incomming. 19 | # 20 | # see Administa::Base#_ensure_administa_config_initialized 21 | # 22 | def initialize! 23 | run_menu_def 24 | initialize_models! 25 | @initialized = true 26 | end 27 | 28 | def initialize_models! 29 | @models.to_a.each do |_, m| 30 | m.setup_options! 31 | end 32 | end 33 | 34 | def add_model(model) 35 | @models ||= {} 36 | @models[model.name.to_sym] = model 37 | end 38 | 39 | def namespace(ns = nil) 40 | @namespace = ns if ns 41 | @namespace || :administa 42 | end 43 | 44 | def base_controller(base = nil) 45 | @base_controller = base if base 46 | @base_controller || "ApplicationController" 47 | end 48 | 49 | def actions(*args) 50 | @actions = args if args.present? 51 | @actions || [:create, :edit, :destroy] 52 | end 53 | 54 | def timezone(tz = nil) 55 | @timezone = tz if tz 56 | @timezone ||= "Etc/UTC" 57 | end 58 | 59 | def timezone_offset 60 | timezone_obj = ActiveSupport::TimeZone.new(@timezone) 61 | ActiveSupport::TimeZone.seconds_to_utc_offset(timezone_obj.utc_offset) 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/administa/model/json.rb: -------------------------------------------------------------------------------- 1 | 2 | module Administa 3 | class Model 4 | module Json 5 | def as_json(records, options ={}) 6 | action = options[:action] 7 | raise ArgumentError, ":action is required" unless action 8 | 9 | inc = (options[:includes] || self.includes(action)).try{|xs| convert_for_json_include(xs) } || {} 10 | methods = options[:method] || self.options[action][:columns].select{|c| c[:accessor] == :method }.map{|c| c[:name]} 11 | 12 | opt = (inc.is_a? Hash) ? inc.merge(methods: methods) : { include: inc, methods: methods } 13 | 14 | # specialize each uploader's serializable_hash method to 15 | # contain 'content_type' in json expression hash 16 | file_columns = self.options[action][:columns].select{|c| c[:type] == :file} 17 | file_columns.each do |col| 18 | Array.wrap(records).each do |r| 19 | uploader = r.send(col[:name]) 20 | specialize_serializable_hash(uploader) 21 | end 22 | end 23 | 24 | records.as_json(opt) 25 | end 26 | 27 | def specialize_serializable_hash(uploader) 28 | uploader.instance_eval do 29 | def serializable_hash(options = nil) 30 | json = super(options) 31 | json["content_type"] = self.content_type 32 | json["is_image"] = !!(self.content_type =~ /^image\//) 33 | json 34 | end 35 | end 36 | end 37 | 38 | def convert_for_json_include(includes, wrap_include = true) 39 | res = case includes 40 | when Array 41 | includes.map{|value| convert_for_json_include(value, false) } 42 | when Hash 43 | includes.inject({}){|h,(k,v)| h[k] = convert_for_json_include(v);h } 44 | else 45 | includes 46 | end 47 | wrap_include ? {:include => res} : res 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /js/Loader.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | // http://projects.lukehaas.me/css-loaders/ 4 | // http://w3lessons.info/2014/01/26/showing-busy-loading-indicator-during-an-ajax-request-using-jquery/ 5 | 6 | setup() { 7 | jQuery(document).ajaxStart(() => { 8 | //show ajax indicator 9 | this.start('Loading...'); 10 | }).ajaxStop(() => { 11 | //hide ajax indicator 12 | this.stop(); 13 | }); 14 | }, 15 | 16 | start(text) { 17 | if(jQuery('body').find('#resultLoading').attr('id') != 'resultLoading'){ 18 | jQuery('body'). 19 | append(''); 20 | } 21 | 22 | jQuery('#resultLoading').css({ 23 | 'width':'100%', 24 | 'height':'100%', 25 | 'position':'fixed', 26 | 'z-index':'10000000', 27 | 'top':'0', 28 | 'left':'0', 29 | 'right':'0', 30 | 'bottom':'0', 31 | 'margin':'auto' 32 | }); 33 | 34 | jQuery('#resultLoading .bg').css({ 35 | 'background':'#000000', 36 | 'opacity':'0.7', 37 | 'width':'100%', 38 | 'height':'100%', 39 | 'position':'absolute', 40 | 'top':'0' 41 | }); 42 | 43 | jQuery('#resultLoading > div:last').css({ 44 | // 'width': '250px', 45 | // 'height':'75px', 46 | // 'text-align': 'center', 47 | 'position': 'fixed', 48 | 'top':'0', 49 | 'left':'0', 50 | 'right':'0', 51 | 'bottom':'0', 52 | 'margin':'auto', 53 | // 'font-size':'16px', 54 | 'z-index':'10', 55 | // 'color':'#ffffff' 56 | 57 | }); 58 | 59 | jQuery('#resultLoading .bg').height('100%'); 60 | jQuery('#resultLoading').fadeIn(300); 61 | jQuery('body').css('cursor', 'wait'); 62 | }, 63 | 64 | 65 | stop() { 66 | jQuery('#resultLoading .bg').height('100%'); 67 | jQuery('#resultLoading').fadeOut(300); 68 | jQuery('body').css('cursor', 'default'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /js/components/form/InputMixin.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import Association from './Association.react'; 3 | 4 | export default { 5 | propTypes: { 6 | column: React.PropTypes.object.isRequired, 7 | resource: React.PropTypes.object.isRequired, 8 | settings: React.PropTypes.object.isRequired, 9 | disabled: React.PropTypes.bool, 10 | invalid: React.PropTypes.bool, 11 | errors: React.PropTypes.string, 12 | }, 13 | 14 | errorsBlock(label) { 15 | if(!this.props.errors || this.props.errors.length == 0) return null; 16 | 17 | return this.props.errors.map((e, i) => { 18 | var msg = `${label} ${e}`; 19 | return

{ msg }

; 20 | }); 21 | }, 22 | 23 | inputClasses() { 24 | var classes = "form-control input-sm"; 25 | classes += " " + this.inputStatusClasses(); 26 | return classes; 27 | }, 28 | 29 | inputStatusClasses() { 30 | var classes = ""; 31 | if(this.isDirty()){ 32 | classes += "modified"; 33 | } 34 | if(this.hasError()){ 35 | classes += "invalid"; 36 | } 37 | return classes; 38 | }, 39 | 40 | formClasses() { 41 | var classes = "form-group"; 42 | if(this.hasError()){ 43 | classes += " has-error"; 44 | } 45 | return classes; 46 | }, 47 | 48 | createAssociation(target, options) { 49 | var column = this.props.column; 50 | var name = column.name; 51 | var association = this.props.column.association; 52 | 53 | var attrs = { 54 | name: name, 55 | column: column, 56 | resource: this.props.resource, 57 | settings: this.props.settings, 58 | buttons: { select: association.select, clear: association.select, create: association.create, edit: association.update }, 59 | disabled: this.props.disabled, 60 | target: target, 61 | } 62 | 63 | assign(attrs, options); 64 | 65 | return ; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/controllers/administa/base.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | module Base 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | layout 'administa/application' 7 | 8 | if Rails.version < "4.0" 9 | before_filter :_authenticate! 10 | before_filter :_ensure_administa_config_initialized 11 | else 12 | before_action :_authenticate! 13 | before_action :_ensure_administa_config_initialized 14 | end 15 | helper_method :user_info, :to_json 16 | end 17 | 18 | private 19 | 20 | def _ensure_administa_config_initialized 21 | unless Administa.config.initialized? 22 | Administa.config.initialize! 23 | 24 | # reload routes for install dynamic controller's routes 25 | Rails.application.routes_reloader.reload! 26 | 27 | redirect_to "#{request.path}?#{request.query_string}" 28 | end 29 | end 30 | 31 | def _authenticate! 32 | user = instance_eval(&Administa.config.authenticate_with) 33 | unless user 34 | path = Administa.config.redirect_path 35 | redirect_to path 36 | end 37 | end 38 | 39 | def _current_user 40 | instance_eval(&Administa.config.current_user_method) 41 | end 42 | 43 | def user_info 44 | user = _current_user 45 | { 46 | name: Administa.config.user_name_proc.call(user), 47 | email: Administa.config.user_email_proc.call(user), 48 | icon: Administa.config.user_icon_image_proc.call(user), 49 | } 50 | end 51 | 52 | def to_json(obj) 53 | # change datetime format : FIXME race condition 54 | use_standard_json_time_format = ActiveSupport::JSON::Encoding.use_standard_json_time_format 55 | ActiveSupport::JSON::Encoding.use_standard_json_time_format = false 56 | begin 57 | obj.to_json 58 | ensure 59 | ActiveSupport::JSON::Encoding.use_standard_json_time_format = !!use_standard_json_time_format 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/administa/config/auth.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | module Administa 4 | class Config 5 | module Auth 6 | 7 | DEFAULT_CURRENT_USER = Proc.new{ 8 | request.env["warden"].try(:user) || respond_to?(:current_user) && current_user 9 | } 10 | DEFAULT_AUTHENTICATION = Proc.new{ 11 | request.env['warden'].try(:authenticate!) 12 | } 13 | 14 | DEFAULT_USER_ICON_PROC = Proc.new {|user| 15 | Administa.config.user_email_proc.call(user).try{|email| 16 | hash = Digest::MD5.hexdigest(email) 17 | "https://www.gravatar.com/avatar/#{hash}.png" 18 | } 19 | } 20 | DEFAULT_USER_NAME_PROC = Proc.new{|user| 21 | (user.respond_to?(:name) && user.send(:name)) || 22 | Administa.config.user_email_proc.call(user) || 23 | nil 24 | } 25 | DEFAULT_USER_EMAIL_PROC = Proc.new{|user| 26 | (user.respond_to?(:email) && user.send(:email)) || 27 | (user.respond_to?(:mail_adress) && user.send(:mail_adress)) || 28 | nil 29 | } 30 | 31 | def current_user_method(&block) 32 | @current_user_proc = block if block 33 | @current_user_proc || DEFAULT_CURRENT_USER 34 | end 35 | 36 | def user_icon_image_proc(&block) 37 | @user_icon_proc = block if block 38 | @user_icon_proc || DEFAULT_USER_ICON_PROC 39 | end 40 | 41 | def user_name_proc(&block) 42 | @user_name_proc = block if block 43 | @user_name_proc || DEFAULT_USER_NAME_PROC 44 | end 45 | 46 | def user_email_proc(&block) 47 | @user_email_proc = block if block 48 | @user_email_proc || DEFAULT_USER_EMAIL_PROC 49 | end 50 | 51 | def authenticate_with(&block) 52 | @authenticate = block if block 53 | @authenticate || DEFAULT_AUTHENTICATION 54 | end 55 | 56 | def redirect_path(path = nil) 57 | @redirect_path = path if path 58 | @redirect_path || "/" 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /js/components/list/SearchBox.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | import ResourceActions from 'actions/ResourceActions'; 3 | import LinkMixin from 'components/LinkMixin'; 4 | 5 | var PT = React.PropTypes 6 | 7 | export default React.createClass({ 8 | displayName: 'list/SearchBox', 9 | 10 | mixins: [LinkMixin, Router.Navigation], 11 | 12 | propTypes: { 13 | name: PT.string, 14 | pagination: PT.object, 15 | transition: PT.bool, 16 | }, 17 | 18 | getInitialState: function() { 19 | return {value: this.props.pagination.q}; 20 | }, 21 | 22 | handleChange: function(event) { 23 | this.setState({value: event.target.value}); 24 | }, 25 | 26 | handleKeyPress: function(event) { 27 | if(event.charCode == 13){ // enter key 28 | this.search(); 29 | } 30 | }, 31 | 32 | search() { 33 | let name = this.props.name; 34 | let id = this.props.id; 35 | let route = 'resource'; 36 | 37 | var params = { name: name } 38 | if (id) { 39 | params.id = id; 40 | route = 'show'; 41 | } 42 | 43 | var options = { 44 | name: name, 45 | id: id, 46 | page: 1, 47 | limit: this.props.pagination.limit, 48 | order: this.props.pagination.order, 49 | q: this.state.value 50 | } 51 | var query = this.linkToListQuery(options); 52 | ResourceActions.list(name, query).then(() => { 53 | var transition = !(this.props.transition == false); 54 | if( transition ) { 55 | this.promiseTransition(route, params, query); 56 | } 57 | }); 58 | }, 59 | 60 | 61 | render() { 62 | var value = this.state.value; 63 | return( 64 |
65 | 66 | 67 | 68 | 69 |
70 | ); 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /js/stores/DialogStore.js: -------------------------------------------------------------------------------- 1 | import Events from 'events' 2 | import Constants from '../Constants'; 3 | import AppDispatcher from '../AppDispatcher'; 4 | import assign from 'object-assign'; 5 | 6 | var dialogs = {}; 7 | var index = 0; 8 | 9 | class DialogState { 10 | constructor(name, component, index, props) { 11 | this.name = name; 12 | this.component = component; 13 | this.index = index 14 | this.props = props; 15 | this.opened = false; 16 | } 17 | 18 | open() { 19 | this.opened = true; 20 | } 21 | 22 | close() { 23 | this.opened = false; 24 | } 25 | } 26 | 27 | var DialogStore = assign({}, Events.EventEmitter.prototype, { 28 | 29 | eventTag(name) { 30 | return "dialog:" + name; 31 | }, 32 | 33 | emitEvent(name) { 34 | this.emit(this.eventTag(name)); 35 | 36 | if(name != "*") { 37 | this.emit(this.eventTag("*")); 38 | } 39 | }, 40 | 41 | addEventListener(name, callback) { 42 | this.on(this.eventTag(name), callback); 43 | }, 44 | 45 | removeEventListener(name, callback) { 46 | this.removeListener(this.eventTag(name), callback); 47 | }, 48 | 49 | setState(name, dialog) { 50 | dialogs[name] = dialog; 51 | }, 52 | 53 | getState(name) { 54 | return dialogs[name]; 55 | }, 56 | 57 | getAllState() { 58 | return dialogs; 59 | }, 60 | }); 61 | 62 | AppDispatcher.register((action) => { 63 | switch(action.type) { 64 | case Constants.DIALOG_OPENED: 65 | var data = action.data; 66 | var name = data.name; 67 | 68 | var dialog = new DialogState(name, data.component, index++, data.props); 69 | 70 | dialog.open(); 71 | DialogStore.setState(name, dialog); 72 | 73 | DialogStore.emitEvent(name); 74 | break; 75 | 76 | case Constants.DIALOG_CLOSED: 77 | var data = action.data; 78 | var name = data.name; 79 | 80 | var dialog = DialogStore.getState(name); 81 | if( dialog ) { 82 | dialog.close(); 83 | --index; 84 | DialogStore.setState(name, dialog); 85 | DialogStore.emitEvent(name); 86 | } 87 | 88 | break; 89 | default: // no op 90 | } 91 | }); 92 | export default DialogStore; 93 | -------------------------------------------------------------------------------- /js/components/Resource.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | import ResourceActions from 'actions/ResourceActions'; 3 | import ResourceStore from 'stores/ResourceStore'; 4 | import List from './list/List.react'; 5 | 6 | var RouteHandler = Router.RouteHandler; 7 | 8 | export default React.createClass({ 9 | 10 | displayName: 'Resource', 11 | 12 | getInitialState() { 13 | return ResourceStore.getState(this.props.params.name); 14 | }, 15 | 16 | componentDidMount() { 17 | ResourceStore.addEventListener(this.props.params.name, this._onChange); 18 | }, 19 | 20 | componentWillUnmount() { 21 | ResourceStore.removeEventListener(this.props.params.name, this._onChange); 22 | }, 23 | 24 | _onChange() { 25 | var st = ResourceStore.getState(this.props.params.name); 26 | 27 | this.setState(st); 28 | // this.setState(ResourceStore.getState(this.props.params.name)); 29 | }, 30 | 31 | contextTypes: { 32 | router: React.PropTypes.func 33 | }, 34 | 35 | render() { 36 | 37 | var leftcol = 12; 38 | var rightcol= 12; 39 | var id = this.state.currentId || this.props.params.id; 40 | var name = this.state.name || this.props.params.name; 41 | var resource = this.state.currentResource || {}; 42 | var resources = this.state.resources || []; 43 | var settings = this.state.settings || {}; 44 | var pagination = this.state.pagination || {}; 45 | var errors = this.state.errors || {}; 46 | var csrfToken = this.state.csrfToken || ""; 47 | 48 | if(resource && Object.keys(resource).length != 0) { 49 | leftcol = 8; 50 | rightcol= 4; 51 | id = Number(id); 52 | } 53 | 54 | var attrs = { 55 | name: name, 56 | id: id, 57 | label: settings.label, 58 | settings: settings, 59 | pagination: pagination, 60 | errors: errors, 61 | csrfToken: csrfToken, 62 | }; 63 | 64 | var list_attrs = { 65 | col: leftcol, 66 | resources: resources 67 | } 68 | 69 | return ( 70 |
71 | 72 | 73 |
74 | ); 75 | } 76 | }); 77 | 78 | 79 | -------------------------------------------------------------------------------- /lib/administa/model/relation.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class Model 3 | class Relation 4 | include Administa::Model::Finder 5 | 6 | attr_accessor :klass, :relation, :pagination_metadata 7 | def initialize(klass, relation) 8 | self.klass = klass 9 | self.relation = relation 10 | end 11 | 12 | def to_a 13 | relation.to_a 14 | end 15 | 16 | def select(options = {}) 17 | rel = relation.includes(options[:includes]) 18 | 19 | self.class.new(klass, rel) 20 | end 21 | 22 | def filter_by_keywords(keyword) 23 | return self unless keyword.present? 24 | 25 | words = keyword.split(/\s/).map{ |w| "%#{w.gsub(/%/, '%%')}%" } 26 | 27 | columns = klass.column_names & klass.options[:search_columns].map(&:to_s) 28 | 29 | arel = klass.arel_table 30 | query = columns. 31 | select{|col| klass.column_names.include?(col.to_s) }. 32 | map{|col| 33 | arel[col].matches_all(words) 34 | }.inject{|q1, q2| q1.or(q2) } 35 | 36 | query = query.or(arel[:id].eq(keyword.to_i)) if keyword =~ /^\d+$/ 37 | 38 | rel = relation.where(query) 39 | self.class.new(klass, rel) 40 | end 41 | 42 | def order(*args) 43 | o = args.compact.presence || klass.options[:order] 44 | 45 | unless o.to_s.include?(".") 46 | o = "%s.%s" % [ klass.table_name, o] 47 | end 48 | 49 | self.class.new(klass, relation.order(o)) 50 | end 51 | 52 | def paginate(page: 1, limit: klass.options[:limit]) 53 | page ||= 1 54 | limit ||= klass.options[:limit] 55 | 56 | offset = (page <= 1 ? 0 : limit * (page - 1)) 57 | 58 | count = relation.count 59 | rel = relation.limit(limit).offset(offset) 60 | 61 | result = self.class.new(klass, rel) 62 | result.calculate_pagination_metadata(count, page, limit, offset) 63 | result 64 | end 65 | 66 | protected 67 | def calculate_pagination_metadata(count , page, limit, offset) 68 | self.pagination_metadata = { 69 | page: page, 70 | limit: limit, 71 | offset: offset, 72 | count: count, 73 | total_pages: limit.zero? ? 0 : (count.to_f / limit).ceil, 74 | } 75 | end 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/administa/config/dynamic_controller.rb: -------------------------------------------------------------------------------- 1 | 2 | module Administa 3 | class Config 4 | module DynamicController 5 | 6 | attr_reader :dynamic_controllers 7 | 8 | def generate_controller(name, options = {}) 9 | base = Administa.config.base_controller 10 | namespace = Administa.config.namespace.to_s.camelize 11 | 12 | unless ns = namespace.safe_constantize 13 | ns = Module.new 14 | Object.const(namespace, nested_mod) 15 | end 16 | 17 | controller_name = "#{name.pluralize.camelize}Controller" 18 | if controller = "#{namespace}::#{controller_name}".safe_constantize 19 | return controller 20 | end 21 | 22 | base_klass = base.constantize 23 | klass = Class.new(base_klass) 24 | model = name.singularize.camelize.constantize 25 | 26 | ns.const_set(controller_name, klass) 27 | klass.send(:include, ::Administa::Controller) 28 | 29 | model_options = options.merge(model: model) 30 | klass.class_eval do 31 | administa model_options 32 | end 33 | 34 | @dynamic_controllers ||= {} 35 | @dynamic_controllers[name.to_s.singularize.to_sym] = klass 36 | 37 | klass 38 | end 39 | 40 | def add_dynamic_controller_routes 41 | ns = self.namespace.to_sym 42 | 43 | dcs = self.controllers.to_a.map(&:model).map(&:name).map(&:pluralize).map(&:to_sym) 44 | 45 | Rails.application.routes.draw do 46 | namespace ns do 47 | dcs.each do |res| 48 | resources res 49 | end 50 | 51 | root to: 'main#index' 52 | get :main, to: 'main#index' 53 | 54 | get ':model', to: 'generic#index' 55 | get ':model/new', to: 'generic#new' 56 | post ':model/create', to: 'generic#create' 57 | get ':model/:id/edit', to: 'generic#edit' 58 | put ':model/:id', to: 'generic#update' 59 | get ':model/:id', to: 'generic#show' 60 | delete ':model/:id', to: 'generic#destroy' 61 | 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | 69 | class Rails::Application::RoutesReloader 70 | 71 | def load_paths_with_dynamic_routes 72 | Administa.config.add_dynamic_controller_routes 73 | 74 | load_paths_without_dynamic_routes 75 | end 76 | alias_method_chain :load_paths, :dynamic_routes 77 | 78 | end 79 | -------------------------------------------------------------------------------- /js/components/list/Pagination.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | import ResourceActions from 'actions/ResourceActions'; 3 | import LinkMixin from 'components/LinkMixin'; 4 | 5 | var Link = Router.Link; 6 | var PT = React.PropTypes 7 | 8 | export default React.createClass({ 9 | displayName: 'list/Pagination', 10 | 11 | mixins: [LinkMixin, Router.Navigation], 12 | 13 | propTypes: { 14 | name: PT.string, 15 | id: PT.number, 16 | resource: PT.object, 17 | pagination: PT.object, 18 | transition: PT.bool 19 | }, 20 | 21 | paginationLink(label, page) { 22 | let name = this.props.name; 23 | let id = this.props.id; 24 | 25 | var className = ''; 26 | if (page == this.props.pagination.page) { 27 | className = 'active'; 28 | } 29 | 30 | var linkAttrs = { 31 | name: name, 32 | id: id, 33 | label: label, 34 | page: page, 35 | limit: this.props.pagination.limit, 36 | order: this.props.pagination.order, 37 | q: this.props.pagination.q, 38 | transition: this.props.transition 39 | } 40 | 41 | return( 42 |
  • 43 | { this.linkToList(linkAttrs) } 44 |
  • 45 | ); 46 | 47 | }, 48 | 49 | render() { 50 | var page = this.props.pagination.page; 51 | var count = this.props.pagination.count; 52 | var total = this.props.pagination.total_pages; 53 | var from = page - 2; 54 | var to = page + 2; 55 | 56 | if (from < 1) from = 1; 57 | if (to > total) to = total; 58 | 59 | if (to - from < 4) { 60 | if(from + 4 <= total) { 61 | to = from + 4; 62 | } 63 | else if(to - 4 > 1) { 64 | from = to - 4; 65 | } 66 | } 67 | 68 | var links = []; 69 | if (from > 1) { 70 | links.push(this.paginationLink('«', 1)); 71 | } 72 | 73 | for (var i=from; i <= to; i++) { 74 | links.push(this.paginationLink(i, i)); 75 | } 76 | 77 | if (to < total) { 78 | links.push(this.paginationLink('»', total)); 79 | } 80 | 81 | links.push(
  • { count + " records"}
  • ); 82 | links.push(
  • { total + " pages"}
  • ); 83 | 84 | return( 85 |
      86 | { links } 87 |
    88 | ); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /js/components/form/CollectionMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | getState(resource) { 4 | var targets = resource[this.props.column.association.name]; 5 | return { 6 | targets: targets, 7 | dirty: false, 8 | }; 9 | }, 10 | getInitialState() { 11 | return this.getState(this.props.resource); 12 | }, 13 | 14 | componentWillReceiveProps(nextProps) { 15 | if (nextProps.resource && nextProps.resource.id != this.props.resource.id) { 16 | this.setState(this.getState(nextProps.resource)); 17 | } 18 | }, 19 | 20 | getResourceValue() { 21 | var keys = Object.keys(this.refs); 22 | var result = {}; 23 | var targets = []; 24 | var name = this.props.column.association.name; 25 | 26 | for (var i = 0, len = keys.length; i < len; i++) { 27 | var key = keys[i]; 28 | var property = this.refs[key]; 29 | var value = property.getResourceValue(); 30 | targets.push(value[name]); 31 | } 32 | 33 | result[name] = targets; 34 | return result; 35 | }, 36 | 37 | isDirty() { 38 | if( this.props.dirty ) return true; 39 | 40 | var keys = Object.keys(this.refs); 41 | for (var i = 0, len = keys.length; i < len; i++) { 42 | var key = keys[i]; 43 | var property = this.refs[key]; 44 | if( property.isDirty() ) { 45 | return true; 46 | } 47 | } 48 | return false; 49 | }, 50 | 51 | add() { 52 | var targets = this.state.targets; 53 | targets.push(null); 54 | this.setState({ 55 | targets: targets, 56 | dirty: false, 57 | }); 58 | }, 59 | 60 | hasError() { 61 | return this.props.invalid; 62 | }, 63 | 64 | render() { 65 | var column = this.props.column; 66 | var name = column.name; 67 | var label = this.toProperyName(column); 68 | var targets = this.state.targets || []; 69 | if(targets.length == 0) { 70 | targets.push(null); // append blank record if empty 71 | } 72 | var associations = targets.map((tgt, i) => { 73 | var attrs = { 74 | key: `${name}[${i}]`, 75 | ref: i, 76 | }; 77 | return this.createAssociation(tgt, attrs); 78 | }); 79 | 80 | return( 81 |
    82 | 83 | 84 | { associations } 85 |
    86 | 87 |
    88 | 89 | { this.errorsBlock(label) } 90 |
    91 | ); 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /lib/generators/administa/administa_generator.rb: -------------------------------------------------------------------------------- 1 | class AdministaGenerator < Rails::Generators::NamedBase 2 | source_root File.expand_path('../templates', __FILE__) 3 | 4 | argument :name, type: :string, required: false, default: "" 5 | class_option :init, type: "boolean", default: false 6 | class_option :namespace, type: "string", default: "", aliases: '-n' 7 | class_option :base_class, type: "string", default: "", aliases: '-b' 8 | 9 | def initialize(args, *options) 10 | super 11 | 12 | if !self.options[:init] && self.name.blank? 13 | raise ArgumentError, "No value provided for required arguments 'name'" 14 | end 15 | end 16 | 17 | def initialize_options 18 | @options = options.dup 19 | if @options[:namespace].blank? 20 | @options[:namespace] = Administa.config.namespace.to_s 21 | end 22 | if @options[:base_class].blank? 23 | @options[:base_class] = Administa.config.base_controller.to_s 24 | end 25 | end 26 | 27 | # connfig/initializers/administa.rb 28 | def create_initializers 29 | return if File.exist? "config/initializers/administa.rb" 30 | 31 | # generate 32 | initializer "administa.rb", <<-RUBY_CODE.strip_heredoc 33 | Administa.config do |config| 34 | 35 | # Authentication settings 36 | # 37 | # config.current_user_method { current_user } 38 | # config.user_name_proc {|user| user.try(:name) } 39 | # config.user_email_proc {|user| user.try(:email) } 40 | # config.authenticate_with { warden.authenticate! :scope => :user } 41 | 42 | # Menu settings 43 | # 44 | config.menu do 45 | # label "Authors" 46 | # group Administa::AuthorsController do 47 | # menu Administa::BooksController 48 | # end 49 | # 50 | # menu Administa::OrdersController 51 | end 52 | 53 | end 54 | RUBY_CODE 55 | end 56 | 57 | def append_menu 58 | return if @options[:init] 59 | 60 | 61 | # append menu Administa::FooController 62 | insert_into_file "config/initializers/administa.rb", 63 | " menu #{@options[:namespace].camelize}::#{class_name.pluralize}Controller\n", 64 | after: "config.menu do\n" 65 | 66 | end 67 | 68 | # app/controllers/administa/foo_controller.rb 69 | def generate_controller 70 | return if @options[:init] 71 | 72 | dest = File.join("app", "controllers", @options[:namespace]) 73 | 74 | # create dir 75 | empty_directory dest unless File.exist? dest 76 | 77 | # generate controller 78 | template "controller.rb", File.join(dest, "#{file_name.pluralize}_controller.rb") 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | 5 | entry: { 6 | app:[ './js/app'], // application js file 7 | vendors: ['react', 'jquery', 'flux', 'lodash'] // libraries js file 8 | }, 9 | 10 | output: { 11 | filename: '[name].bundle.js', 12 | // We want to save the bundle into assets pipeline 13 | path: './app/assets/javascripts/administa/build', 14 | publicPath: 'administa/assets/build' 15 | }, 16 | 17 | resolve: { 18 | modulesDirectories: ['node_modules', 'css', 'js'], 19 | alias: { 20 | adminlte: 'admin-lte/dist/js/app.js', 21 | 'adminlte.css': 'admin-lte/dist/css/AdminLTE.css', 22 | 'adminlte-skins-blue.css': 'admin-lte/dist/css/skins/skin-blue.min.css', 23 | 'adminlte-skins-black.css': 'admin-lte/dist/css/skins/skin-black.min.css', 24 | 'bootstrap.css': 'bootstrap/dist/css/bootstrap.css', 25 | 'jquery-datetimepicker.js': 'jquery-datetimepicker/build/jquery.datetimepicker.full.js', 26 | 'jquery.datetimepicker.css': 'jquery-datetimepicker/jquery.datetimepicker.css', 27 | 'jquery.notifyBar.css' : 'jqnotifybar/css/jquery.notifyBar.css' 28 | } 29 | }, 30 | 31 | plugins: [ 32 | // take the vendors chunk and create a vendors.js from the "vendors" in entry section. 33 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js'), 34 | 35 | // export to global 36 | new webpack.ProvidePlugin({ 37 | $: 'jquery', 38 | jQuery: "jquery", 39 | _: 'lodash', 40 | React: 'react', 41 | Flux: 'flux', 42 | moment: 'moment' 43 | }), 44 | ], 45 | 46 | // Turns on source maps 47 | devtool: '#inline-source-map', 48 | 49 | module: { 50 | loaders: [ 51 | // exports Administa object to global 52 | { test: require.resolve('./js/app'), loader: 'expose?Administa' }, 53 | { test: require.resolve('./node_modules/jquery/dist/jquery'), loader: "expose?jQuery"}, 54 | 55 | // babel-loader : the transpiler es6 to es5 56 | { test: /\.js$/, loader: 'babel-loader', exclude: [/node_modules/], }, 57 | 58 | // css-loader 59 | { test: /\.css$/, loaders: ['style-loader', 'css-loader?sourceMap!'] }, 60 | 61 | { test: /\.png$/, loader: "url-loader?limit=100000&mimetype=image/png" }, 62 | { test: /\.jpg$/, loader: "file-loader" }, 63 | { test: /\.gif$/, loader: "file-loader" }, 64 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" }, 65 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" }, 66 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/octet-stream" }, 67 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" }, 68 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=image/svg+xml" } 69 | ], 70 | noParse: [/\.min\.js/] 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /js/components/detail/Detail.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | import LinkMixin from 'components/LinkMixin'; 3 | import Property from './Property.react'; 4 | 5 | export default React.createClass({ 6 | displayName: 'detali/Detail', 7 | 8 | mixins: [LinkMixin, Router.Navigation], 9 | 10 | propTypes: { 11 | name: React.PropTypes.string, 12 | id: React.PropTypes.number, 13 | col: React.PropTypes.number, 14 | resource: React.PropTypes.object.isRequired, 15 | pagination: React.PropTypes.object, 16 | settings: React.PropTypes.object 17 | }, 18 | 19 | shouldComponentUpdate(nextProps, nextState) { 20 | return !!nextProps.resource; 21 | }, 22 | 23 | editLink(resource){ 24 | if( this.props.settings.actions.indexOf("edit") < 0 ) { 25 | return null; 26 | } 27 | 28 | let name = this.props.name; 29 | let id = resource.id; 30 | 31 | var linkAttrs = { 32 | name: name, 33 | id: id, 34 | label: 'edit', 35 | page: this.props.pagination.page, 36 | limit: this.props.pagination.limit, 37 | order: this.props.pagination.order, 38 | q: this.props.pagination.q 39 | } 40 | 41 | return this.linkToEdit(linkAttrs); 42 | }, 43 | 44 | deleteLink(resource){ 45 | if( this.props.settings.actions.indexOf("destroy") < 0 ) { 46 | return null; 47 | } 48 | let name = this.props.name; 49 | let id = resource.id; 50 | 51 | var linkAttrs = { 52 | name: name, 53 | id: id, 54 | label: 'delete', 55 | page: this.props.pagination.page, 56 | limit: this.props.pagination.limit, 57 | order: this.props.pagination.order, 58 | q: this.props.pagination.q, 59 | csrfToken: this.props.csrfToken, 60 | confirm: `Are you sure to delete ${this.props.label}(id:${this.props.id})`, 61 | } 62 | 63 | return this.linkToDestroy(linkAttrs); 64 | }, 65 | 66 | properties() { 67 | return this.props.settings.show.columns.map((col) => { 68 | return ; 69 | }); 70 | }, 71 | 72 | render() { 73 | var resource = this.props.resource; 74 | var classes = "resource-detail" ; 75 | classes += " col-md-" + this.props.col; 76 | 77 | if (!resource.id) { 78 | return
    79 | } 80 | 81 | var properties = this.properties(); 82 | 83 | return( 84 |
    85 |
    86 |
    87 |

    Detail : {this.props.label}(id:{this.props.id})

    88 |
    89 |
    90 |
    91 | { this.deleteLink(resource) } 92 | { this.editLink(resource) } 93 |
    94 |
    95 |
    96 | { properties } 97 |
    98 |
    99 |
    100 | ); 101 | }, 102 | }) 103 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | administa (0.0.1) 5 | rails (~> 4.2.1.rc3) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionmailer (4.2.1) 11 | actionpack (= 4.2.1) 12 | actionview (= 4.2.1) 13 | activejob (= 4.2.1) 14 | mail (~> 2.5, >= 2.5.4) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | actionpack (4.2.1) 17 | actionview (= 4.2.1) 18 | activesupport (= 4.2.1) 19 | rack (~> 1.6) 20 | rack-test (~> 0.6.2) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | actionview (4.2.1) 24 | activesupport (= 4.2.1) 25 | builder (~> 3.1) 26 | erubis (~> 2.7.0) 27 | rails-dom-testing (~> 1.0, >= 1.0.5) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 29 | activejob (4.2.1) 30 | activesupport (= 4.2.1) 31 | globalid (>= 0.3.0) 32 | activemodel (4.2.1) 33 | activesupport (= 4.2.1) 34 | builder (~> 3.1) 35 | activerecord (4.2.1) 36 | activemodel (= 4.2.1) 37 | activesupport (= 4.2.1) 38 | arel (~> 6.0) 39 | activesupport (4.2.1) 40 | i18n (~> 0.7) 41 | json (~> 1.7, >= 1.7.7) 42 | minitest (~> 5.1) 43 | thread_safe (~> 0.3, >= 0.3.4) 44 | tzinfo (~> 1.1) 45 | arel (6.0.0) 46 | builder (3.2.2) 47 | erubis (2.7.0) 48 | globalid (0.3.3) 49 | activesupport (>= 4.1.0) 50 | hike (1.2.3) 51 | i18n (0.7.0) 52 | json (1.8.2) 53 | loofah (2.0.1) 54 | nokogiri (>= 1.5.9) 55 | mail (2.6.3) 56 | mime-types (>= 1.16, < 3) 57 | mime-types (2.4.3) 58 | mini_portile (0.6.2) 59 | minitest (5.5.1) 60 | multi_json (1.11.0) 61 | nokogiri (1.6.6.2) 62 | mini_portile (~> 0.6.0) 63 | rack (1.6.0) 64 | rack-test (0.6.3) 65 | rack (>= 1.0) 66 | rails (4.2.1) 67 | actionmailer (= 4.2.1) 68 | actionpack (= 4.2.1) 69 | actionview (= 4.2.1) 70 | activejob (= 4.2.1) 71 | activemodel (= 4.2.1) 72 | activerecord (= 4.2.1) 73 | activesupport (= 4.2.1) 74 | bundler (>= 1.3.0, < 2.0) 75 | railties (= 4.2.1) 76 | sprockets-rails 77 | rails-deprecated_sanitizer (1.0.3) 78 | activesupport (>= 4.2.0.alpha) 79 | rails-dom-testing (1.0.6) 80 | activesupport (>= 4.2.0.beta, < 5.0) 81 | nokogiri (~> 1.6.0) 82 | rails-deprecated_sanitizer (>= 1.0.1) 83 | rails-html-sanitizer (1.0.2) 84 | loofah (~> 2.0) 85 | railties (4.2.1) 86 | actionpack (= 4.2.1) 87 | activesupport (= 4.2.1) 88 | rake (>= 0.8.7) 89 | thor (>= 0.18.1, < 2.0) 90 | rake (10.4.2) 91 | sprockets (2.12.3) 92 | hike (~> 1.2) 93 | multi_json (~> 1.0) 94 | rack (~> 1.0) 95 | tilt (~> 1.1, != 1.3.0) 96 | sprockets-rails (2.2.4) 97 | actionpack (>= 3.0) 98 | activesupport (>= 3.0) 99 | sprockets (>= 2.8, < 4.0) 100 | sqlite3 (1.3.10) 101 | thor (0.19.1) 102 | thread_safe (0.3.5) 103 | tilt (1.4.1) 104 | tzinfo (1.2.2) 105 | thread_safe (~> 0.1) 106 | 107 | PLATFORMS 108 | ruby 109 | 110 | DEPENDENCIES 111 | administa! 112 | sqlite3 113 | -------------------------------------------------------------------------------- /js/components/Menu.react.js: -------------------------------------------------------------------------------- 1 | import MenuStore from 'stores/MenuStore'; 2 | 3 | export default React.createClass({ 4 | displayName: 'Menu', 5 | 6 | getInitialState() { 7 | return { menus: MenuStore.getState() }; 8 | }, 9 | 10 | componentDidMount() { 11 | MenuStore.addEventListener(this._onChange); 12 | }, 13 | 14 | componentWillUnmount() { 15 | MenuStore.removeEventListener(this._onChange); 16 | }, 17 | 18 | _onChange() { 19 | var menus = MenuStore.getState(); 20 | this.setState({ menus: menus }); 21 | }, 22 | 23 | label(m, key) { 24 | return
  • { m.label }
  • ; 25 | }, 26 | 27 | group(m, key) { 28 | var l = m.label; 29 | var title = null; 30 | 31 | switch(l.type){ 32 | case "label": 33 | title = ( 34 | 35 | { l.label } 36 | < 37 | /a> 38 | ); 39 | break; 40 | case "menu": 41 | var f = (e) => { 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | window.location.href = l.path; 45 | return false; 46 | }; 47 | var iconstyle = "fa fa-circle-o"; 48 | if(l.selected) iconstyle += " text-aqua"; 49 | 50 | title = ( 51 | 52 | 53 | { l.label } 54 | 55 | 56 | ); 57 | break; 58 | } 59 | 60 | var children = m.menus.map((c, i) => { 61 | return this.generate(c, `${key}_${i}`); 62 | }); 63 | 64 | var listyle = ""; 65 | var ulstyle = "treeview-menu"; 66 | if(m.opened){ 67 | listyle = "active"; 68 | ulstyle += " menu-open"; 69 | } 70 | if(!m.visible) { 71 | listyle += " hide"; 72 | } 73 | 74 | return ( 75 |
  • 76 | { title } 77 |
      78 | { children } 79 |
    80 |
  • 81 | ); 82 | }, 83 | 84 | menu(m, key) { 85 | var className=""; 86 | var iconstyle = "fa fa-circle-o"; 87 | 88 | if(m.selected) { 89 | className = "active"; 90 | iconstyle += " text-aqua"; 91 | } 92 | if(!m.visible) { 93 | className += " hide"; 94 | } 95 | 96 | return ( 97 |
  • 98 | 99 | 100 | { m.label } 101 | 102 |
  • 103 | ); 104 | }, 105 | 106 | generate(m, key) { 107 | switch(m.type) { 108 | case "label": 109 | return this.label(m, key); 110 | case "group": 111 | return this.group(m, key); 112 | case "menu": 113 | return this.menu(m, key); 114 | default: 115 | //noop 116 | } 117 | }, 118 | 119 | menus() { 120 | return this.state.menus.map((m, i) => { 121 | return this.generate(m, i); 122 | }); 123 | }, 124 | 125 | render() { 126 | return ( 127 | 134 | ); 135 | }, 136 | }) 137 | -------------------------------------------------------------------------------- /lib/administa/model/attributes.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class Model 3 | module Attributes 4 | 5 | def assign(record, attrs) 6 | attrs = transform_attributes(attrs, klass) 7 | record.assign_attributes(attrs.except(:id, :updated_at, :created_at)) 8 | end 9 | 10 | def transform_attributes(attr, klass = nil) 11 | role = self.options[:attr_accessible_role] || :default 12 | authorizer = (klass.respond_to? :active_authorizer) ? klass.try(:active_authorizer).try(:[], role) : nil 13 | white_list = (authorizer ? authorizer.to_a : klass.column_names).map(&:to_sym) 14 | white_list.unshift(:id) 15 | 16 | res = attr.slice(*white_list) 17 | 18 | nested_attribute_keys = klass.nested_attributes_options.keys 19 | 20 | # reject association that is not specified by accepts_nested_attributes_for 21 | unallowed_associations = (association_names(klass) - nested_attribute_keys) 22 | res.except!(*unallowed_associations) 23 | 24 | remains = attr.except(*white_list) 25 | 26 | # accept if parameter name is "_attributes" 27 | nested_attribute_names = klass.nested_attributes_options.keys 28 | attributes_keys = nested_attribute_names.map{|n| "#{n}_attributes"} 29 | remains.slice(*attributes_keys).each do |name, nested_attr| 30 | res[name] = nested_attr 31 | end 32 | remains.except!(*attributes_keys) 33 | 34 | # if parameter name is "", 35 | # cordinate paramters by nested association recursively 36 | remains.slice(*nested_attribute_keys).each do |name, nested_attr| 37 | next unless nested_attr.present? 38 | nested_klass = klass_of(name) || klass_of(attr[name + '_type']) 39 | if nested_klass 40 | res["#{name}_attributes"] = case nested_attr 41 | when Array then nested_attr.map{|na| transform_attributes(na, nested_klass) } 42 | when Hash then transform_attributes(nested_attr, nested_klass) 43 | else nested_attr 44 | end 45 | end 46 | end 47 | remains.except!(*nested_attribute_keys) 48 | 49 | # if repsond_to? returuns true, set the paramster to record 50 | # accept "_destroy" for destory nested associations 51 | acceptable_names = ["id", "_destroy"] 52 | remains.except!(*association_names(klass)) 53 | 54 | # reject common specific attributes(i.e create_at, updated_at) 55 | ignore_attrs = [:created_at, :updated_at].map(&:to_s) 56 | 57 | remains.except!(ignore_attrs) 58 | remains.reject!{|k,v| authorizer.deny?(k) && acceptable_names.include?(k) == false } if authorizer 59 | 60 | methods = klass.instance_methods.map(&:to_s) 61 | remains.each do |k,v| 62 | res[k] = v if methods.include?("#{k}=") || k == "_destroy" 63 | end 64 | 65 | res 66 | end 67 | 68 | def klass_of(name, klass = self.klass) 69 | name.to_s.singularize.camelize.constantize 70 | rescue NameError 71 | association = klass.reflect_on_all_associations.find{|a| a.plural_name == name.to_s && a.options.key?(:class_name) } 72 | 73 | association && association.options[:class_name].to_s.constantize 74 | end 75 | 76 | def association_names(klass = self.klass) 77 | klass.reflect_on_all_associations.map{|a| a.collection? ? a.plural_name.to_sym : a.name } 78 | end 79 | end 80 | end 81 | end 82 | 83 | 84 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /js/components/PropertyMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | toProperyName(col) { 4 | var name = col.label; 5 | if(col.association) { 6 | name = col.association.label; 7 | } 8 | return name; 9 | }, 10 | 11 | toProperyKey(col) { 12 | var name = col.name; 13 | if(col.association) { 14 | name = col.association.name; 15 | } 16 | return name; 17 | }, 18 | 19 | toLabel(col, resource, options = {}) { 20 | var name = this.toProperyKey(col); 21 | var val = resource[name]; 22 | var search_columns = options.search_columns; 23 | var wrap_tag = options.wrap_tag || false ; 24 | var ellipsis = options.ellipsis || false ; 25 | 26 | switch( col.type ) { 27 | case "file" : 28 | var imgtag = null; 29 | if( val.url ) { 30 | if ( wrap_tag && val.is_image) { 31 | val = { val.url }; 32 | } else { 33 | val = val.url 34 | } 35 | } 36 | break; 37 | case "boolean" : 38 | if(!val) { 39 | if( wrap_tag ) { 40 | val = off; 41 | } else { 42 | val = 'on'; 43 | } 44 | } else { 45 | if( wrap_tag ) { 46 | val = on; 47 | } else { 48 | val = 'on'; 49 | } 50 | } 51 | break; 52 | case "datetime": 53 | // convert value with browser timezone by moment.js 54 | var format = 'YYYY/MM/DD HH:mm:ss Z'; 55 | val = moment(val, format).format(format); 56 | } 57 | 58 | if(col.association) { 59 | name = col.association.name; 60 | var nested = val; 61 | 62 | if( !nested ) { 63 | return ""; 64 | } 65 | if(col.association.type == 'has_many' || col.association.type == 'through') { 66 | val = []; 67 | var to = nested.length; 68 | if (ellipsis && to > 3) to = 3; 69 | for(var i = 0; i < to; i++) { 70 | if( !nested[i] ) continue; 71 | var s = this.extractLabel(col.association.label, nested[i], search_columns); 72 | if( !wrap_tag ) { 73 | val.push(s); 74 | } else { 75 | s = this.wrapPermlink(s, nested[i], col.association); 76 | val.push(
    {s}
    ); 77 | } 78 | } 79 | 80 | if( ellipsis && nested.length > to ) { 81 | if( !wrap_tag ) { 82 | val.push("...") 83 | } else { 84 | val.push(
    ...
    ); 85 | } 86 | } 87 | 88 | if( !wrap_tag ) val = val.join(", ") 89 | 90 | return val; 91 | } else { 92 | val = this.extractLabel(name, nested, search_columns); 93 | } 94 | 95 | if( !wrap_tag ) return val; 96 | 97 | // permlink 98 | return this.wrapPermlink(val, nested, col.association); 99 | 100 | } 101 | return val; 102 | }, 103 | 104 | wrapPermlink(val, obj, association) { 105 | // permlink 106 | if( !(obj && obj.id && association.controller_path)) { 107 | return val; 108 | } 109 | 110 | var href = `/${ association.controller_path }/${ obj.id }`; 111 | return { val } 112 | }, 113 | 114 | extractLabel(name, obj, search_columns) { 115 | var label = name; 116 | var id = obj.id || 'new' 117 | for(var i = 0; i < search_columns.length; i++) { 118 | var v = obj[search_columns[i]]; 119 | if(v) { 120 | label = v; 121 | break; 122 | } 123 | } 124 | return label + "(" + id + ")" 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /js/components/list/List.react.js: -------------------------------------------------------------------------------- 1 | import Router from 'react-router'; 2 | 3 | import ResourceActions from 'actions/ResourceActions'; 4 | import ResourceItem from './Item.react'; 5 | import Pagination from './Pagination.react'; 6 | import SearchBox from './SearchBox.react'; 7 | import LinkMixin from 'components/LinkMixin'; 8 | import PropertyMixin from 'components/PropertyMixin'; 9 | 10 | var PT = React.PropTypes 11 | 12 | export default React.createClass({ 13 | displayName: 'list/List', 14 | 15 | mixins: [LinkMixin, PropertyMixin, Router.Navigation ], 16 | 17 | propTypes: { 18 | name: PT.string, 19 | id: PT.number, 20 | col: PT.number, 21 | resources: PT.arrayOf(PT.object), 22 | settings: PT.object.isRequired, 23 | pagination: PT.object, 24 | }, 25 | 26 | clickItem(resource) { 27 | let name = this.props.name; 28 | let id = resource.id; 29 | let pagination = this.props.pagination; 30 | 31 | let attrs = this.linkAttrs(name, id, pagination); 32 | let query = this.linkToListQuery(attrs); 33 | 34 | ResourceActions.fetch(name, id, query).then(() => { 35 | this.transitionToShow(name, id, pagination); 36 | }); 37 | }, 38 | 39 | 40 | newLink(){ 41 | if( this.props.settings.actions.indexOf("create") < 0 ) { 42 | return null; 43 | } 44 | let name = this.props.name; 45 | 46 | var linkAttrs = { 47 | name: name, 48 | label: 'create', 49 | page: this.props.pagination.page, 50 | limit: this.props.pagination.limit, 51 | order: this.props.pagination.order, 52 | q: this.props.pagination.q 53 | } 54 | 55 | return this.linkToNew(linkAttrs); 56 | }, 57 | 58 | render() { 59 | let Item = ResourceItem; 60 | 61 | var classes = "resource-list" ; 62 | classes += " col-md-" + this.props.col; 63 | 64 | var index_settings = this.props.settings.index; 65 | var headers = index_settings.columns.map((col) => { 66 | var name = this.toProperyName(col); 67 | return ({ name }); 68 | }); 69 | 70 | var items = this.props.resources.map((resource) => { 71 | var selected = resource.id == this.props.id; 72 | var attrs = { 73 | name: this.props.name, 74 | resource: resource, 75 | key: resource.id, 76 | selected: selected, 77 | columns: index_settings.columns, 78 | search_columns: this.props.settings.search_columns, 79 | pagination: this.props.pagination, 80 | onclick: () => { this.clickItem(resource) }, 81 | }; 82 | 83 | return (); 84 | }); 85 | 86 | return( 87 |
    88 |
    89 | 90 |
    91 |

    {this.props.settings.label}

    92 |
    93 | 94 | 95 |
    96 | { this.newLink() } 97 |
    98 | 99 |
    100 |
    101 |
    102 | 103 |
    104 | 105 | 106 | 107 | { headers } 108 | 109 | 110 | 111 | 112 | { items } 113 | 114 |
    115 |
    116 |
    117 | 118 |
    119 | 120 |
    121 |
    122 |
    123 | ); 124 | }, 125 | }) 126 | -------------------------------------------------------------------------------- /lib/administa/config/menu.rb: -------------------------------------------------------------------------------- 1 | module Administa 2 | class Config 3 | module Menu 4 | 5 | def menus(controller) 6 | 7 | f = ->(m) { 8 | case m[:type] 9 | when :menu 10 | lf = m.delete(:label_f) 11 | m[:label] = lf.call 12 | sf = m.delete(:selected_f) 13 | m[:selected] = sf.call(controller) 14 | when :group 15 | m[:label] = f.call(m[:label].dup) 16 | m[:menus] = m[:menus].map{|sm| f.call(sm.dup) } 17 | m[:opened] = m[:label][:selected] || m[:menus].any?{|sm| sm[:opend] || sm[:selected] } 18 | when :label 19 | lf = m.delete(:label_f) 20 | m[:label] = lf.call 21 | end 22 | 23 | m 24 | } 25 | 26 | @menu_tree.menus.map do |m| 27 | f.call(m.dup) 28 | end 29 | end 30 | 31 | def menu(&block) 32 | @menu_def = block 33 | end 34 | 35 | def run_menu_def 36 | @menu_tree = Tree.new(&@menu_def) 37 | @menu_tree.run 38 | 39 | @controllers = @menu_tree.controllers 40 | end 41 | 42 | class Tree 43 | attr_accessor :menus, :controllers 44 | 45 | def initialize(&block) 46 | @menus = [] 47 | @controllers = [] 48 | @block = block 49 | end 50 | 51 | def run(current_controller = nil) 52 | return unless @block 53 | @current_controller = current_controller 54 | self.instance_eval(&@block) 55 | self 56 | end 57 | 58 | def t(s, options = {}) 59 | if s.is_a? Symbol 60 | s = I18n.t(s, scope: options[:scope], default: s) 61 | end 62 | return s 63 | end 64 | 65 | def label_def(s, options = {}) 66 | { 67 | type: :label, 68 | label_f: -> { t(s) }, 69 | visible: (options[:visible].nil? ? true : options[:visible]), 70 | } 71 | end 72 | 73 | def label(s, options = {}) 74 | @menus.push(label_def(s, options)) 75 | end 76 | 77 | def label_group(title, options = {}, &block) 78 | t = label_def(title, options) 79 | add_group(t, options, &block) 80 | end 81 | 82 | def group(title, options = {}, &block) 83 | c = case title 84 | when String, Symbol then Administa.config.generate_controller(title.to_s, options) 85 | else title 86 | end 87 | 88 | t = controller(c, options) 89 | add_group(t, options, &block) 90 | end 91 | 92 | def add_group(title, options = {}, &block) 93 | m = Tree.new(&block) 94 | m.run 95 | 96 | @controllers += m.controllers.to_a 97 | children = m.menus 98 | opened= children.any?{|h| h[:selected] || h[:opened]} 99 | 100 | @menus.push({ 101 | type: :group, 102 | label: title, 103 | menus: children, 104 | opened: opened, 105 | visible: (options[:visible].nil? ? true : options[:visible]), 106 | }) 107 | end 108 | 109 | def menu(controller, options = {}) 110 | c = case controller 111 | when String, Symbol then Administa.config.generate_controller(controller.to_s, options) 112 | else controller 113 | end 114 | 115 | @menus.push(controller(c, options)) 116 | end 117 | 118 | private 119 | def controller(c, options= {}) 120 | selected_f = ->(current){ current.try(:controller_path) == c.controller_path } 121 | label_f = -> { c.model.label } 122 | @controllers.push(c) 123 | { 124 | type: :menu, 125 | path: "/#{c.controller_path}", 126 | label_f: label_f, 127 | selected_f: selected_f, 128 | visible: (options[:visible].nil? ? true : options[:visible]), 129 | } 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /js/components/list/Selection.react.js: -------------------------------------------------------------------------------- 1 | import ResourceStore from 'stores/ResourceStore'; 2 | import ResourceItem from './Item.react'; 3 | import Pagination from './Pagination.react'; 4 | import SearchBox from './SearchBox.react'; 5 | import PropertyMixin from 'components/PropertyMixin'; 6 | 7 | var PT = React.PropTypes 8 | 9 | export default React.createClass({ 10 | displayName: 'list/Selection', 11 | 12 | mixins: [PropertyMixin], 13 | 14 | propTypes: { 15 | name: PT.string, 16 | title: PT.string, 17 | onselect: PT.func, 18 | }, 19 | 20 | getInitialState() { 21 | return ResourceStore.getState(this.props.name); 22 | }, 23 | 24 | componentDidMount() { 25 | ResourceStore.addEventListener(this.props.name, this._onChange); 26 | }, 27 | 28 | componentWillUnmount() { 29 | ResourceStore.removeEventListener(this.props.name, this._onChange); 30 | }, 31 | 32 | _onChange() { 33 | var st = ResourceStore.getState(this.props.name); 34 | 35 | this.setState(st); 36 | }, 37 | 38 | render() { 39 | let Item = ResourceItem; 40 | 41 | var classes = "dialog-body resource-list" ; 42 | 43 | var settings = this.state.settings; 44 | var index_settings = this.state.settings.index; 45 | 46 | var columns = []; 47 | var push_column = (col) => { 48 | var found = false; 49 | for(var i = 0; i < columns.length; i++) { 50 | found = columns[i].name == col.name || 51 | (columns[i].association && col.association && 52 | columns[i].association.name == col.association.name); 53 | if(found) break; 54 | } 55 | if(!found){ 56 | columns.push(col); 57 | } 58 | } 59 | 60 | var searchcols = settings.search_columns; 61 | var cols = index_settings.columns; 62 | 63 | for(var i = 0; i < cols.length; i++) { 64 | var col = cols[i]; 65 | for(var j = 0; j < searchcols.length; j++) { 66 | if( col.name == 'id' || col.name == searchcols[j]) { 67 | push_column(col); 68 | } 69 | if( col.association && col.association.name == searchcols[j] ) { 70 | push_column(col); 71 | } 72 | } 73 | } 74 | if( columns.length == 1 ) { 75 | for(var i = 0; i < cols.length; i++) { 76 | var col = cols[i]; 77 | if( col.name != 'id' && col.name != 'created_at' && col.name != 'updated_at') { 78 | push_column(col); 79 | } 80 | } 81 | } 82 | 83 | var col_span = columns.length * 2; 84 | classes += " col-md-" + col_span ; 85 | 86 | var headers = columns.map((col) => { 87 | var name = this.toProperyName(col); 88 | return ({ name }); 89 | }); 90 | 91 | var items = this.state.resources.map((resource) => { 92 | var attrs = { 93 | name: this.props.name, 94 | resource: resource, 95 | key: resource.id, 96 | columns: columns, 97 | search_columns: settings.search_columns, 98 | pagination: this.state.pagination, 99 | onclick: ()=> { this.props.onselect(resource) }, 100 | }; 101 | 102 | return (); 103 | }); 104 | 105 | var title = this.props.title || this.props.name; 106 | 107 | return( 108 | 109 |
    110 |
    111 | 112 |
    113 |

    { title }

    114 |
    115 | 116 | 117 | 118 |
    119 |
    120 |
    121 | 122 |
    123 | 124 | 125 | 126 | { headers } 127 | 128 | 129 | 130 | 131 | { items } 132 | 133 |
    134 |
    135 |
    136 | 137 |
    138 | 139 |
    140 |
    141 |
    142 | ); 143 | }, 144 | }) 145 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Router from 'react-router'; 4 | import Bootstrap from 'bootstrap'; 5 | import AdminLTE from 'adminlte'; 6 | 7 | // react-router 8 | var Route = Router.Route; 9 | var NotFoundRoute = Router.NotFoundRoute; 10 | var DefaultRoute = Router.DefaultRoute; 11 | var Link = Router.Link; 12 | var Redirect = Router.Redirect; 13 | var RouteHandler = Router.RouteHandler; 14 | 15 | // require stylesheets 16 | require("bootstrap.css"); 17 | require("adminlte.css"); 18 | // require("adminlte-skins-blue.css"); 19 | require("adminlte-skins-black.css"); 20 | require("app.css"); 21 | 22 | //setup datetimepicker with moment.js 23 | require("jquery.datetimepicker.css"); 24 | require("jquery-datetimepicker.js"); 25 | 26 | // setup jquery notify bar 27 | require("jqnotifybar"); 28 | require("jquery.notifyBar.css") 29 | 30 | Date.parseDate = function( input, format ){ 31 | return moment(input,format).toDate(); 32 | }; 33 | Date.prototype.dateFormat = function( format ){ 34 | return moment(this).format(format); 35 | }; 36 | 37 | import AppStore from 'stores/AppStore'; 38 | import ResourceActions from 'actions/ResourceActions'; 39 | import MenuActions from 'actions/MenuActions'; 40 | import UserActions from 'actions/UserActions'; 41 | 42 | import Header from 'components/Header.react'; 43 | import Menu from 'components/Menu.react'; 44 | import Main from 'components/Main.react'; 45 | import Footer from 'components/Footer.react'; 46 | import Loader from 'Loader'; 47 | 48 | import Resource from 'components/Resource.react'; 49 | import ResourceDetail from 'components/detail/Detail.react'; 50 | import ResourceForm from 'components/form/Form.react'; 51 | 52 | import Dialogs from 'components/Dialogs.react'; 53 | import Utils from 'Utils'; 54 | 55 | // expose React to global (workarround) 56 | global.React = React; 57 | 58 | 59 | // Display js error 60 | window.addEventListener('error', function(e){ 61 | 62 | var stack = e.stack || (e.error && e.error.stack); 63 | Utils.reportError(e.message, e.error, stack); 64 | }); 65 | 66 | var App = React.createClass({ 67 | displayName: 'App', 68 | 69 | mixins: [Router.Navigation], 70 | 71 | getInitialState() { 72 | return AppStore.getState(); 73 | }, 74 | 75 | componentDidMount() { 76 | AppStore.addEventListener(this._onChange); 77 | }, 78 | 79 | componentWillUnmount() { 80 | AppStore.removeEventListener(this._onChange); 81 | }, 82 | 83 | componentWillUpdate(nextProps, nextState) { 84 | 85 | if (nextState.transitionTo ) { 86 | var route = nextState.transitionTo.route; 87 | var params = nextState.transitionTo.params; 88 | var query = nextState.transitionTo.query; 89 | 90 | this.transitionTo(route, params, query); 91 | } 92 | }, 93 | 94 | _onChange() { 95 | var st = AppStore.getState(); 96 | this.setState(st); 97 | }, 98 | 99 | render() { 100 | return ( 101 |
    102 |
    103 | 104 | 105 |
    106 |
    107 | 108 | 109 | 110 |
    111 |
    112 | 113 |
    114 | 115 |
    116 | ); 117 | } 118 | }); 119 | 120 | var routes = ( 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | ); 133 | 134 | Loader.setup(); 135 | 136 | export default { 137 | dataElement() { 138 | return document.getElementById('initial-data'); 139 | }, 140 | 141 | extractFromDataElement(name) { 142 | return JSON.parse(this.dataElement().getAttribute(name)); 143 | }, 144 | 145 | initialData() { 146 | return this.extractFromDataElement('data-json'); 147 | }, 148 | 149 | user() { 150 | return this.extractFromDataElement('user-json'); 151 | }, 152 | 153 | menus() { 154 | return this.extractFromDataElement('menu-json'); 155 | }, 156 | 157 | initializeAdminLTE() { 158 | //Activate the layout maker 159 | $.AdminLTE.layout.fix() 160 | //Enable sidebar tree view controls 161 | $.AdminLTE.tree('.sidebar'); 162 | }, 163 | 164 | render(data) { 165 | 166 | ResourceActions.initialize(data); 167 | UserActions.initialize(this.user()); 168 | MenuActions.initialize(this.menus()); 169 | 170 | Router.run(routes, Router.HistoryLocation, (Handler, state) => { 171 | let params = state.params; 172 | React.render(, document.body); 173 | }); 174 | 175 | this.initializeAdminLTE(); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /js/stores/ResourceStore.js: -------------------------------------------------------------------------------- 1 | import Events from 'events' 2 | import Constants from '../Constants'; 3 | import AppDispatcher from '../AppDispatcher'; 4 | import assign from 'object-assign'; 5 | import Utils from 'Utils'; 6 | 7 | var states = {}; 8 | 9 | class ResourceState { 10 | constructor(name, data = {}) { 11 | this.name = name || ''; 12 | this.currentId = data.id; 13 | this.currentResource = data.resource; 14 | this.settings = data.settings || {}; 15 | this.resources = data.resources || []; 16 | this.pagination = data.pagination || {}; 17 | this.errors = data.errors || {}; 18 | this.csrfToken = data.csrf_token || ""; 19 | this.flash = data.flash || ""; 20 | } 21 | 22 | update(other) { 23 | var setUnlessEmpty = function(attr, self) { 24 | if( other.hasOwnProperty(attr) == false ) { 25 | return; 26 | } 27 | var v = other[attr]; 28 | if(v && Utils.isPrimitive(v)) { 29 | self[attr] = v; 30 | return; 31 | } 32 | if(v && Object.keys(v).length > 0) { 33 | self[attr] = v; 34 | } 35 | }; 36 | 37 | if (other.currentId) this.currentId = other.currentId; 38 | 39 | setUnlessEmpty("currentResource", this); 40 | setUnlessEmpty("resources", this); 41 | setUnlessEmpty("settings", this); 42 | setUnlessEmpty("pagination", this); 43 | setUnlessEmpty("errors", this); 44 | setUnlessEmpty("csrfToken", this); 45 | setUnlessEmpty("flash", this); 46 | return this; 47 | } 48 | 49 | clone() { 50 | return new ResourceState( this.name, { 51 | id: this.currentId, 52 | resource: this.currentResource, 53 | settings: this.settings, 54 | resources: this.resources, 55 | pagination: this.pagination, 56 | csrf_token: this.csrfToken, 57 | flash: this.flash, 58 | }); 59 | } 60 | } 61 | 62 | var ResourceStore = assign({}, Events.EventEmitter.prototype, { 63 | 64 | eventTag(name) { 65 | return "resource:" + name; 66 | }, 67 | 68 | emitEvent(name) { 69 | this.emit(this.eventTag(name)); 70 | 71 | if(name != "*") { 72 | this.emit(this.eventTag("*")); 73 | } 74 | }, 75 | 76 | addEventListener(name, callback) { 77 | this.on(this.eventTag(name), callback); 78 | }, 79 | 80 | removeEventListener(name, callback) { 81 | this.removeListener(this.eventTag(name), callback); 82 | }, 83 | 84 | updateState(name, data) { 85 | var old = this.getState(name); 86 | var state = new ResourceState(name, data); 87 | 88 | if(old) { 89 | state = old.clone().update(state); 90 | } 91 | 92 | this.setState(name, state); 93 | return state; 94 | }, 95 | 96 | setState(name, state) { 97 | states[name] = state; 98 | }, 99 | 100 | getState(name) { 101 | return states[name]; 102 | }, 103 | 104 | getAllState() { 105 | return states; 106 | }, 107 | 108 | }); 109 | 110 | AppDispatcher.register((action) => { 111 | switch(action.type) { 112 | case Constants.INITIALIZE: 113 | var data = action.data; 114 | var name = data.name; 115 | 116 | var state = new ResourceState(name, data); 117 | 118 | ResourceStore.setState(name, state); 119 | ResourceStore.emitEvent(name); 120 | 121 | break; 122 | case Constants.RESOURCE_FETCH: 123 | 124 | var data = action.data; 125 | var name = action.name; 126 | 127 | ResourceStore.updateState(name, data); 128 | ResourceStore.emitEvent(name); 129 | 130 | break; 131 | case Constants.RESOURCE_LIST: 132 | var data = action.data; 133 | var name = action.name; 134 | 135 | ResourceStore.updateState(name, data); 136 | ResourceStore.emitEvent(name); 137 | 138 | break; 139 | case Constants.RESOURCE_BUILD: 140 | 141 | var data = action.data; 142 | var name = action.name; 143 | 144 | var state = ResourceStore.updateState(name, data); 145 | state.currentId = null; // clear currentid 146 | ResourceStore.emitEvent(name); 147 | 148 | break; 149 | case Constants.RESOURCE_CREATED: 150 | 151 | var data = action.data; 152 | var name = action.name; 153 | 154 | ResourceStore.updateState(name, data); 155 | ResourceStore.emitEvent(name); 156 | 157 | break; 158 | case Constants.RESOURCE_UPDATED: 159 | 160 | var data = action.data; 161 | var name = action.name; 162 | 163 | ResourceStore.updateState(name, data); 164 | ResourceStore.emitEvent(name); 165 | 166 | break; 167 | case Constants.RESOURCE_INVALID: 168 | 169 | var data = action.data; 170 | var name = action.name; 171 | 172 | ResourceStore.updateState(name, data); 173 | ResourceStore.emitEvent(name); 174 | 175 | case Constants.RESOURCE_DELETED: 176 | 177 | var data = action.data; 178 | var name = action.name; 179 | 180 | var state = ResourceStore.updateState(name, data); 181 | state.currentId = null; // clear currentid 182 | state.currentResource = null; 183 | ResourceStore.emitEvent(name); 184 | 185 | default: // no op 186 | } 187 | }); 188 | export default ResourceStore; 189 | -------------------------------------------------------------------------------- /js/components/LinkMixin.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../AppDispatcher'; 2 | import Constants from '../Constants'; 3 | import Router from 'react-router'; 4 | import ResourceActions from 'actions/ResourceActions'; 5 | 6 | var Link = Router.Link; 7 | 8 | export default { 9 | 10 | promiseTransition(route, params, query) { 11 | AppDispatcher.dispatch({ 12 | type: Constants.APP_TRANSITION, 13 | route: route, 14 | params: params, 15 | query: query, 16 | }); 17 | }, 18 | 19 | transitionToShow(name, id, pagination) { 20 | var options = { 21 | name: name, 22 | id: id, 23 | page: pagination.page, 24 | limit: pagination.limit, 25 | order: pagination.order, 26 | q: pagination.q 27 | }; 28 | 29 | var params = { name: name, id: id }; 30 | var query = this.linkToListQuery(options); 31 | 32 | this.promiseTransition('show', params, query); 33 | 34 | }, 35 | 36 | linkToNew(options = {}) { 37 | let name = options.name; 38 | let label = options.label; 39 | 40 | var params = { name: name }; 41 | var query = this.linkToListQuery(options); 42 | 43 | var f = (event) => { 44 | event.preventDefault(); 45 | 46 | ResourceActions.build(name, query).then(() => { 47 | this.promiseTransition('new', params, query); 48 | }); 49 | }; 50 | 51 | var classes = options.classes || "btn btn-primary btn-xs btn-flat" 52 | var href = this.makeHref('new', params, query); 53 | 54 | var link = { label }; 55 | return link; 56 | }, 57 | 58 | linkToEdit(options = {}) { 59 | let name = options.name; 60 | let id = options.id; 61 | let label = options.label; 62 | 63 | var params = { name: name, id: id }; 64 | var query = this.linkToListQuery(options); 65 | 66 | var f = (event) => { 67 | event.preventDefault(); 68 | ResourceActions.fetch(name, id, query).then(() => { 69 | this.promiseTransition('edit', params, query); 70 | }); 71 | }; 72 | 73 | var classes = options.classes || "btn btn-primary btn-xs btn-flat" 74 | var href = this.makeHref('edit', params, query); 75 | 76 | var link = { label }; 77 | return link; 78 | }, 79 | 80 | linkToDestroy(options = {}) { 81 | let name = options.name; 82 | let id = options.id; 83 | let label = options.label; 84 | 85 | var params = { name: name, id: id }; 86 | var query = this.linkToListQuery(options); 87 | 88 | var f = (event) => { 89 | event.preventDefault(); 90 | 91 | if( options.confirm ){ 92 | if( !window.confirm(options.confirm)) { 93 | return ; 94 | } 95 | } 96 | ResourceActions.destroy(name, id, query, options).then((data) => { 97 | 98 | var flash = data.flash; 99 | 100 | if( flash ) { 101 | $.notifyBar({ cssClass: "success", html: flash, }); 102 | } 103 | }); 104 | return true; 105 | }; 106 | 107 | var classes = options.classes || "btn btn-danger btn-xs btn-flat" 108 | var href = this.makeHref('show', params, query); 109 | 110 | var link = { label }; 111 | return link; 112 | }, 113 | 114 | linkToShow(options = {}) { 115 | let name = options.name; 116 | let id = options.id; 117 | let label = options.label; 118 | 119 | var params = { name: name, id: id }; 120 | var query = this.linkToListQuery(options); 121 | 122 | var f = (event) => { 123 | event.preventDefault(); 124 | 125 | ResourceActions.fetch(name, id, query).then(() => { 126 | this.promiseTransition('show', params, query); 127 | }); 128 | }; 129 | 130 | var classes = options.classes || "btn btn-primary btn-xs btn-flat" 131 | var href = this.makeHref('show', params, query); 132 | 133 | var link = { label }; 134 | return link; 135 | }, 136 | 137 | linkToList(options = {}) { 138 | let route = 'resource'; 139 | let name = options.name; 140 | let id = options.id; 141 | let label = options.label; 142 | 143 | var params = { name: name } 144 | if (id) { 145 | params.id = id; 146 | route = 'show'; 147 | } 148 | var query = this.linkToListQuery(options); 149 | var transition = !(options.transition == false); 150 | 151 | var f = (event) => { 152 | event.preventDefault(); 153 | 154 | ResourceActions.list(options.name, query).then(() => { 155 | if(transition) { 156 | this.promiseTransition(route, params, query); 157 | } 158 | }); 159 | }; 160 | 161 | var classes = options.classes ; 162 | var href = this.makeHref(route, params, query); 163 | 164 | var link = { label }; 165 | return link; 166 | }, 167 | 168 | linkAttrs(name, id, pagination){ 169 | return { 170 | name: name, 171 | id: id, 172 | page: pagination.page, 173 | limit: pagination.limit, 174 | order: pagination.order, 175 | q: pagination.q 176 | } 177 | }, 178 | 179 | linkToListQuery(options = {}) { 180 | return { 181 | page: options.page, 182 | limit: options.limit, 183 | order: options.order, 184 | q: options.q 185 | } 186 | }, 187 | 188 | } 189 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | .main { 2 | height: 100vh; 3 | max-height: 100vh; 4 | overflow-y: auto; 5 | } 6 | 7 | .content-wrapper{ 8 | } 9 | 10 | /* header */ 11 | .skin-black .main-header>.logo { 12 | background-color: #0C0C0C; 13 | color: #C7C7C7; 14 | border-right: 1px solid #303030; 15 | } 16 | 17 | .skin-black .main-header>.navbar { 18 | background-color: #0C0C0C; 19 | border: 1px solid transparent; 20 | } 21 | 22 | 23 | .skin-black .main-header>.navbar>.sidebar-toggle { 24 | color: #C7C7C7; 25 | border-right: none; 26 | } 27 | 28 | 29 | .skin-black .main-header>.navbar .navbar-custom-menu .navbar-nav>li>a, .skin-black .main-header>.navbar .navbar-right>li>a { 30 | border-left: none; 31 | border-right-width: 0; 32 | } 33 | 34 | .skin-black .main-header>.navbar .nav>li>a { 35 | color: #C7C7C7; 36 | } 37 | 38 | /* sidebar */ 39 | .skin-black .wrapper, .skin-black .main-sidebar, .skin-black .left-side { 40 | background-color: #303030; 41 | } 42 | 43 | .skin-black .sidebar-menu>li.header { 44 | color: #CDCDCD; 45 | background: #454545; 46 | } 47 | 48 | .skin-black .main-header>.logo:hover { 49 | background: #454545; 50 | color: #C7C7C7; 51 | } 52 | 53 | .skin-black .main-header>.navbar .nav>li>a:hover, .skin-black .main-header>.navbar .nav>li>a:active, .skin-black .main-header>.navbar .nav>li>a:focus, .skin-black .main-header>.navbar .nav .open>a, .skin-black .main-header>.navbar .nav .open>a:hover, .skin-black .main-header>.navbar .nav .open>a:focus { 54 | background: #454545; 55 | color: #C7C7C7; 56 | } 57 | 58 | .skin-black .sidebar-menu>li:hover>a, .skin-black .sidebar-menu>li.active>a { 59 | color: #fff; 60 | background: #1E2022; 61 | border-left-color: #fff; 62 | } 63 | 64 | .skin-black .sidebar-menu>li>.treeview-menu { 65 | margin: 0 0px; 66 | padding-left: 10px; 67 | background: #373B3D; 68 | } 69 | 70 | .skin-black .treeview-menu>li>a { 71 | color: #A5ADB0; 72 | } 73 | 74 | 75 | 76 | 77 | .skin-black .sidebar-form input[type="text"], .skin-black .sidebar-form .btn { 78 | background-color: #373B3D; 79 | } 80 | 81 | .skin-black .sidebar-form { 82 | border: 1px solid #474C4F; 83 | } 84 | 85 | 86 | /* form */ 87 | .form-group .modified { 88 | border-color: royalblue; 89 | color: royalblue; 90 | } 91 | .form-group .invalid{ 92 | border-color: tomato; 93 | color: tomato; 94 | } 95 | 96 | .form-group blockquote { 97 | font-size: 14px; 98 | padding: 0px 20px; 99 | } 100 | 101 | .form-group .input-group { 102 | margin-bottom: 4px; 103 | } 104 | 105 | .form-control[disabled]{ 106 | background-color: #FAFAFA; 107 | } 108 | 109 | .input-group-btn .btn-default { 110 | background-color: #EEEEEE; 111 | border-color: #d2d6de; 112 | } 113 | 114 | .box-header .box-tools .btn { 115 | margin-left: 4px; 116 | 117 | } 118 | /* resoource-list */ 119 | .resource-list .box-body { 120 | scroll: auto; 121 | } 122 | .resource-list .box-tools a, 123 | .resource-list .box-tools button{ 124 | margin-right : .75em; 125 | height: 30px; 126 | padding: 5px 10px; 127 | font-size: 12px; 128 | line-height: 1.5; 129 | } 130 | 131 | .resource-list table tr{ 132 | cursor: pointer ; 133 | } 134 | 135 | .resource-list table th, td{ 136 | white-space: nowrap; 137 | overflow: hidden; 138 | text-overflow: ellipsis; 139 | max-width: 150px; 140 | } 141 | 142 | .resource-list table td img{ 143 | max-width: 150px; 144 | } 145 | 146 | input.search-box-text { 147 | min-width: 80px; 148 | } 149 | 150 | /* pagination */ 151 | .pagination li { 152 | cursor: pointer; 153 | } 154 | 155 | /* dialog */ 156 | 157 | .dialog-base { 158 | width : 100%; 159 | height : 100%; 160 | position : fixed; 161 | z-index : 1000; 162 | top : 0; 163 | left : 0; 164 | right : 0; 165 | bottom : 0; 166 | margin : auto; 167 | } 168 | 169 | .dialog-base .dialog-bg { 170 | background : #000000; 171 | opacity : 0.7; 172 | width : 100%; 173 | height : 100%; 174 | position : absolute; 175 | top : 0; 176 | } 177 | 178 | .dialog-base .dialog-body { 179 | position : fixed; 180 | top : 50px; 181 | left : 0; 182 | right : 0; 183 | bottom : 0; 184 | margin : auto; 185 | z-index : 10; 186 | min-width: 400px; 187 | max-width: 600px; 188 | overflow-y: auto; 189 | } 190 | 191 | /* loader */ 192 | .loader, 193 | .loader:before, 194 | .loader:after { 195 | background: #FFF; 196 | -webkit-animation: load1 1s infinite ease-in-out; 197 | animation: load1 1s infinite ease-in-out; 198 | width: 1em; 199 | height: 4em; 200 | } 201 | .loader:before, 202 | .loader:after { 203 | position: absolute; 204 | top: 0; 205 | content: ''; 206 | } 207 | .loader:before { 208 | left: -1.5em; 209 | -webkit-animation-delay: -0.32s; 210 | animation-delay: -0.32s; 211 | } 212 | .loader { 213 | text-indent: -9999em; 214 | margin: 8em auto; 215 | position: relative; 216 | font-size: 11px; 217 | -webkit-transform: translateZ(0); 218 | -ms-transform: translateZ(0); 219 | transform: translateZ(0); 220 | -webkit-animation-delay: -0.16s; 221 | animation-delay: -0.16s; 222 | } 223 | .loader:after { 224 | left: 1.5em; 225 | } 226 | @-webkit-keyframes load1 { 227 | 0%, 228 | 80%, 229 | 100% { 230 | box-shadow: 0 0 #FFF; 231 | height: 4em; 232 | } 233 | 40% { 234 | box-shadow: 0 -2em #ffffff; 235 | height: 5em; 236 | } 237 | } 238 | @keyframes load1 { 239 | 0%, 240 | 80%, 241 | 100% { 242 | box-shadow: 0 0 #FFF; 243 | height: 4em; 244 | } 245 | 40% { 246 | box-shadow: 0 -2em #ffffff; 247 | height: 5em; 248 | } 249 | } 250 | 251 | 252 | 253 | div.wild-error-notification { 254 | position: absolute; 255 | background-color: rgba(51, 51, 51, 0.72); 256 | width: 100%; 257 | z-index: 9999; 258 | color: #EEE; 259 | padding: 20px; 260 | } 261 | div.wild-error-notification h3 { 262 | color: orange; 263 | } 264 | 265 | 266 | -------------------------------------------------------------------------------- /js/components/form/Input.react.js: -------------------------------------------------------------------------------- 1 | import InputMixin from './InputMixin'; 2 | import PropertyMixin from 'components/PropertyMixin'; 3 | 4 | 5 | export default React.createClass({ 6 | displayName: 'form/Input', 7 | 8 | mixins: [PropertyMixin, InputMixin], 9 | 10 | getState(resource) { 11 | return { 12 | value: resource[this.props.column.name], 13 | dirty: false, 14 | }; 15 | }, 16 | 17 | getInitialState() { 18 | var value = this.props.resource[this.props.column.name]; 19 | var dirty = false; 20 | 21 | if( !this.props.resource.id ) { // cordinate itinial values for creation 22 | if ( this.props.column.default && !value) { 23 | value = this.props.column.default; 24 | dirty = true; 25 | } 26 | 27 | if ( this.props.column.type == 'enum' && value ) { 28 | // this filed is considered as 'edited' if enum type has default value on creation 29 | dirty = true; 30 | } 31 | } 32 | 33 | return { 34 | value: value, 35 | dirty: dirty, 36 | }; 37 | }, 38 | 39 | componentDidMount() { 40 | if ( this.props.column.type == "datetime") { 41 | var options = { 42 | lang: this.props.settings.locale, 43 | format: 'Y/m/d H:i', 44 | defaultDate:new Date() 45 | }; 46 | var str_min_sec = "00:00"; 47 | var str_timezone = ""; 48 | var the_value = this.props.resource[this.props.column.name]; 49 | if(the_value) { 50 | var arr = /\d+\/\d+\/\d+ \d+:(\d+):(\d+)( [+-]\d+)?/.exec(the_value); 51 | if(arr) { 52 | str_min_sec = "" + arr[1] + ":" + arr[2]; 53 | str_timezone = arr[3] || ""; 54 | } 55 | } 56 | options['onClose'] = (current_time, input, event) => { 57 | var determined_value = jQuery(input).val(); 58 | var matched = /^\d+\/\d+\/\d+ \d+:/.exec(determined_value); 59 | if(matched) { 60 | determined_value = matched[0] + str_min_sec + str_timezone; 61 | } 62 | this.handleChange(event, determined_value); 63 | } 64 | 65 | // in moment.js, default timezone offset is detected by Date.prototype.getTimezoneOffset 66 | jQuery("input[name='" + this.props.column.name + "'].datetimepicker").datetimepicker(options); 67 | } 68 | }, 69 | 70 | componentWillReceiveProps(nextProps) { 71 | if (nextProps.resource && nextProps.resource.id != this.props.resource.id) { 72 | this.setState(this.getState(nextProps.resource)); 73 | } 74 | }, 75 | 76 | handleChange(event, value) { 77 | var initial = this.props.resource[this.props.column.name]; 78 | // set null value that represents "blank" 79 | var value = event.target.value || value || null; 80 | if ( this.props.column.type == 'file') { 81 | value = jQuery(event.target).prop('files')[0]; 82 | } 83 | if ( this.props.column.type == 'boolean') { 84 | value = event.target.checked; 85 | } 86 | this.setState({ 87 | value: value, 88 | dirty: (value != initial), 89 | }); 90 | }, 91 | 92 | getFormValue() { 93 | return this.getResourceValue(); 94 | }, 95 | 96 | getResourceValue() { 97 | var value = {}; 98 | value[this.props.column.name] = this.state.value; 99 | return value; 100 | }, 101 | 102 | isDirty() { 103 | return this.state.dirty; 104 | }, 105 | 106 | hasError() { 107 | return this.props.invalid; 108 | }, 109 | 110 | inputField() { 111 | var name = this.props.column.name; 112 | var value = this.state.value; 113 | 114 | // TODO: refactor 115 | switch( this.props.column.type ) { 116 | case "file" : 117 | var imgtag = null; 118 | if( value.url ) { 119 | imgtag = ; 120 | } 121 | 122 | return ( 123 |
    124 | { imgtag } 125 | 126 |
    127 | ); 128 | case "boolean" : 129 | var text = 'on'; 130 | if(!this.state.value) text = 'off'; 131 | 132 | return
    133 | case "enum": 134 | var options = this.props.column.enums.map((e) => { 135 | var val = e; 136 | var label = e; 137 | if(Array.isArray(e)){ 138 | label = e[0]; 139 | val = e[1]; 140 | } 141 | return ; 142 | }); 143 | 144 | if ( this.props.column.nullable ) { 145 | options.unshift(); 146 | } 147 | 148 | return 151 | break; 152 | case "text": 153 | return