From ac43efc60fd12b5203acef6ddf20fb9f782384fa Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 12 Jul 2015 03:32:40 +0200 Subject: [PATCH] Added branding feature. --- .../app/controllers/_settings/area.js.coffee | 118 +++++++++++++++++- .../app/controllers/settings.js.coffee | 42 +++---- .../app/views/settings/logo.jst.eco | 36 ++++++ app/assets/stylesheets/zammad.css.scss | 10 +- app/controllers/settings_controller.rb | 59 +++++++++ config/routes/setting.rb | 12 +- db/migrate/20150712000001_update_setting.rb | 9 ++ db/seeds.rb | 41 +++--- 8 files changed, 267 insertions(+), 60 deletions(-) create mode 100644 app/assets/javascripts/app/views/settings/logo.jst.eco create mode 100644 db/migrate/20150712000001_update_setting.rb diff --git a/app/assets/javascripts/app/controllers/_settings/area.js.coffee b/app/assets/javascripts/app/controllers/_settings/area.js.coffee index 0613eac4b..637a74950 100644 --- a/app/assets/javascripts/app/controllers/_settings/area.js.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area.js.coffee @@ -5,17 +5,32 @@ class App.SettingsArea extends App.Controller # check authentication return if !@authenticate() - App.Setting.bind 'refresh change', @render - App.Setting.fetch() + @load() + + load: -> + @ajax( + id: "setting_area_#{@area}" + type: 'GET' + url: "#{@apiPath}/settings/area/#{@area}" + processData: true + success: (data, status, xhr) => + App.Collection.load( localStorage: false, type: 'Setting', data: data ) + @render() + ) render: => - settings = App.Setting.all() + settings = App.Setting.search( + filter: + area: @area + ) html = $('
') for setting in settings - if setting.area is @area + if setting.name is 'product_logo' + item = new App.SettingsAreaLogo( setting: setting ) + else item = new App.SettingsAreaItem( setting: setting ) - html.append( item.el ) + html.append( item.el ) @html html @@ -55,6 +70,7 @@ class App.SettingsAreaItem extends App.Controller update: (e) => e.preventDefault() + @formDisable(e) params = @formParam(e.target) directValue = 0 @@ -78,6 +94,7 @@ class App.SettingsAreaItem extends App.Controller ui = @ @setting.save( done: => + ui.formEnable(e) App.Event.trigger 'notify', { type: 'success' @@ -90,4 +107,95 @@ class App.SettingsAreaItem extends App.Controller # login check App.Auth.loginCheck() + fail: => + ui.formEnable(e) ) + +class App.SettingsAreaLogo extends App.Controller + elements: + '.logo-preview': 'logoPreview' + + events: + 'submit form': 'submit' + 'change .js-upload': 'onLogoPick' + + constructor: -> + super + @render() + + render: -> + logoFile = App.Config.get('product_logo') + logoUrl = App.Config.get('image_path') + "/#{logoFile}" + @html App.view('settings/logo')( + setting: @setting + logoUrl: logoUrl + ) + + onLogoPick: (event) => + reader = new FileReader() + + reader.onload = (e) => + @logoPreview.attr('src', e.target.result) + + file = event.target.files[0] + + # if no file is given, about in file upload was used + if !file + return + + maxSiteInMb = 8 + if file.size && file.size > 1024 * 1024 * maxSiteInMb + #@showAlert( 'logo', App.i18n.translateInline( 'File too big, max. %s MB allowed.', maxSiteInMb ) ) + @logoPreview.attr( 'src', '' ) + return + + reader.readAsDataURL(file) + + submit: (e) => + e.preventDefault() + @formDisable(e) + + # get params + @params = @formParam(e.target) + + # add logo + @params.logo = @logoPreview.attr('src') + + store = (logoResizeDataUrl) => + + # store image + @params.logo_resize = logoResizeDataUrl + @ajax( + id: "setting_image_#{@setting.id}" + type: 'PUT' + url: "#{@apiPath}/settings/image/#{@setting.id}" + data: JSON.stringify(@params) + processData: true + success: (data, status, xhr) => + @formEnable(e) + if data.result is 'ok' + @formEnable(e) + + App.Event.trigger 'notify', { + type: 'success' + msg: App.i18n.translateContent('Update successful!') + timeout: 2000 + } + @render() + + App.Event.trigger( 'ui:rerender' ) + for key, value of data.settings + App.Config.set( key, value ) + else + App.Event.trigger 'notify', { + type: 'error' + msg: App.i18n.translateContent(data.message) + timeout: 2000 + } + + fail: => + @formEnable(e) + ) + + # add resized image + App.ImageService.resizeForAvatar( @params.logo, @logoPreview.width(), @logoPreview.height(), store ) diff --git a/app/assets/javascripts/app/controllers/settings.js.coffee b/app/assets/javascripts/app/controllers/settings.js.coffee index 763c3e724..eef155cf7 100644 --- a/app/assets/javascripts/app/controllers/settings.js.coffee +++ b/app/assets/javascripts/app/controllers/settings.js.coffee @@ -1,31 +1,31 @@ +class Branding extends App.ControllerTabs + constructor: -> + super + return if !@authenticate() + @title 'Branding', true + @tabs = [ + { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Branding' } }, + ] + @render() + class System extends App.ControllerTabs constructor: -> super - return if !@authenticate() - @title 'System', true - @tabs = [ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } }, - # { name: 'Language', 'target': 'language', controller: App.SettingsSystem, params: { area: 'System::Language' } }, - # { name: 'Log', 'target': 'log', controller: App.SettingsSystem, params: { area: 'System::Log' } }, { name: 'Storage', 'target': 'storage', controller: App.SettingsArea, params: { area: 'System::Storage' } }, { name: 'Geo Services', 'target': 'geo', controller: App.SettingsArea, params: { area: 'System::Geo' } }, { name: 'Frontend', 'target': 'ui', controller: App.SettingsArea, params: { area: 'System::UI' } }, ] - - # render page @render() class Security extends App.ControllerTabs constructor: -> super - return if !@authenticate() - @title 'Security', true - @tabs = [ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Security::Base' } }, # { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } }, @@ -33,44 +33,34 @@ class Security extends App.ControllerTabs { name: 'Third-Party Applications', 'target': 'third_party_auth', controller: App.SettingsArea, params: { area: 'Security::ThirdPartyAuthentication' } }, # { name: 'Session', 'target': 'session', controller: '' }, ] - @render() class Import extends App.ControllerTabs constructor: -> super - return if !@authenticate() - @title 'Import', true - - # import @tabs = [ - { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } }, - { name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } }, + { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } }, + { name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } }, ] - @render() class Ticket extends App.ControllerTabs constructor: -> super - return if !@authenticate() - @title 'Ticket', true - - # ticket @tabs = [ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } }, { name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } }, # { name: 'Sender Format', 'target': 'sender-format', controller: App.SettingsArea, params: { area: 'Ticket::SenderFormat' } }, ] - @render() -App.Config.set( 'SettingSystem', { prio: 1400, parent: '#settings', name: 'System', target: '#settings/system', controller: System, role: ['Admin'] }, 'NavBarAdmin' ) +App.Config.set( 'SettingBranding', { prio: 1200, parent: '#settings', name: 'Branding', target: '#settings/branding', controller: Branding, role: ['Admin'] }, 'NavBarAdmin' ) +App.Config.set( 'SettingSystem', { prio: 1400, parent: '#settings', name: 'System', target: '#settings/system', controller: System, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingSecurity', { prio: 1500, parent: '#settings', name: 'Security', target: '#settings/security', controller: Security, role: ['Admin'] }, 'NavBarAdmin' ) -App.Config.set( 'SettingImport', { prio: 1550, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, role: ['Admin'] }, 'NavBarAdmin' ) -App.Config.set( 'SettingTicket', { prio: 1600, parent: '#settings', name: 'Ticket', target: '#settings/ticket', controller: Ticket, role: ['Admin'] }, 'NavBarAdmin' ) +App.Config.set( 'SettingImport', { prio: 1550, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, role: ['Admin'] }, 'NavBarAdmin' ) +App.Config.set( 'SettingTicket', { prio: 1600, parent: '#settings', name: 'Ticket', target: '#settings/ticket', controller: Ticket, role: ['Admin'] }, 'NavBarAdmin' ) diff --git a/app/assets/javascripts/app/views/settings/logo.jst.eco b/app/assets/javascripts/app/views/settings/logo.jst.eco new file mode 100644 index 000000000..dde50e7f6 --- /dev/null +++ b/app/assets/javascripts/app/views/settings/logo.jst.eco @@ -0,0 +1,36 @@ +
+

<%- @T( @setting.title ) %>

+

<%- @T( @setting.description ) %>

+ + + +
+ +
+
diff --git a/app/assets/stylesheets/zammad.css.scss b/app/assets/stylesheets/zammad.css.scss index 601b1bbc7..d2ab736a8 100644 --- a/app/assets/stylesheets/zammad.css.scss +++ b/app/assets/stylesheets/zammad.css.scss @@ -4811,12 +4811,12 @@ label + .wizard-buttonList { } } -.setup.wizard .logo-preview { +.setup.wizard .logo-preview, .branding .logo-preview { display: block; height: 0; max-width: 200px; max-height: 100px; - + &[src=""] { visibility: hidden; } @@ -4832,7 +4832,7 @@ label + .wizard-buttonList { } } -.setup.wizard .logo-preview:not([src=""]) { +.setup.wizard .logo-preview:not([src=""]), .branding .logo-preview:not([src=""]) { margin: 0 auto 15px; height: auto; @@ -4845,6 +4845,10 @@ label + .wizard-buttonList { margin-top: 15px; } +.branding.login { + padding: 24px 24px 0px; +} + .import.wizard .wizard-slide { height: 300px; } diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 189ee794b..35df2d7ee 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -6,6 +6,14 @@ class SettingsController < ApplicationController # GET /settings def index return if deny_if_not_role(Z_ROLENAME_ADMIN) + + # only serve requested items + if params[:area] + model_index_render_result( Setting.where(area: params[:area]) ) + return + end + + # serve all items model_index_render(Setting, params) end @@ -27,6 +35,57 @@ class SettingsController < ApplicationController model_update_render(Setting, params) end + # PUT /settings/image/:id + def update_image + + if !params[:logo] + render json: { + result: 'invalid', + message: 'Need logo param', + } + return + end + + # validate image + if params[:logo] !~ /^data:image/i + render json: { + result: 'invalid', + message: 'Invalid payload, need data:image in logo param', + } + return + end + + # process image + file = StaticAssets.data_url_attributes(params[:logo]) + if !file[:content] || !file[:mime_type] + render json: { + result: 'invalid', + message: 'Unable to process image upload.', + } + return + end + + # store image 1:1 + StaticAssets.store_raw(file[:content], file[:mime_type]) + + # store resized image 1:1 + setting = Setting.find_by(name: 'product_logo') + if params[:logo_resize] && params[:logo_resize] =~ /^data:image/i + + # data:image/png;base64 + file = StaticAssets.data_url_attributes( params[:logo_resize] ) + + # store image 1:1 + setting.state = StaticAssets.store( file[:content], file[:mime_type] ) + setting.save + end + + render json: { + result: 'ok', + settings: [setting], + } + end + # DELETE /settings/1 def destroy return if deny_if_not_role(Z_ROLENAME_ADMIN) diff --git a/config/routes/setting.rb b/config/routes/setting.rb index 86c660fe0..f8d89a17e 100644 --- a/config/routes/setting.rb +++ b/config/routes/setting.rb @@ -2,10 +2,12 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # base objects - match api_path + '/settings', to: 'settings#index', via: :get - match api_path + '/settings/:id', to: 'settings#show', via: :get - match api_path + '/settings', to: 'settings#create', via: :post - match api_path + '/settings/:id', to: 'settings#update', via: :put - match api_path + '/settings/:id', to: 'settings#destroy', via: :delete + match api_path + '/settings', to: 'settings#index', via: :get + match api_path + '/settings/area/:area', to: 'settings#index', via: :get + match api_path + '/settings/:id', to: 'settings#show', via: :get + match api_path + '/settings', to: 'settings#create', via: :post + match api_path + '/settings/image/:id', to: 'settings#update_image', via: :put + match api_path + '/settings/:id', to: 'settings#update', via: :put + match api_path + '/settings/:id', to: 'settings#destroy', via: :delete end diff --git a/db/migrate/20150712000001_update_setting.rb b/db/migrate/20150712000001_update_setting.rb new file mode 100644 index 000000000..1e1b2f4d6 --- /dev/null +++ b/db/migrate/20150712000001_update_setting.rb @@ -0,0 +1,9 @@ +class UpdateSetting < ActiveRecord::Migration + def up + %w(product_name product_logo organization).each {|setting_name| + setting = Setting.find_by(name: setting_name) + setting.area = 'System::Branding' + setting.save + } + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 8ef16fa2e..0e0fadbbb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -36,7 +36,7 @@ Setting.create_if_not_exists( Setting.create_if_not_exists( title: 'Product Name', name: 'product_name', - area: 'System::Base', + area: 'System::Branding', description: 'Defines the name of the application, shown in the web interface, tabs and title bar of the web browser.', options: { form: [ @@ -51,29 +51,10 @@ Setting.create_if_not_exists( state: 'Zammad', frontend: true ) -Setting.create_if_not_exists( - title: 'Logo', - name: 'product_logo', - area: 'System::CI', - description: 'Defines the logo of the application, shown in the web interface.', - options: { - form: [ - { - display: '', - null: false, - name: 'product_logo', - tag: 'input', - }, - ], - }, - state: 'logo.svg', - frontend: true -) - Setting.create_if_not_exists( title: 'Organization', name: 'organization', - area: 'System::Base', + area: 'System::Branding', description: 'Will also be included in emails as an X-Header.', options: { form: [ @@ -88,6 +69,24 @@ Setting.create_if_not_exists( state: '', frontend: true ) +Setting.create_if_not_exists( + title: 'Logo', + name: 'product_logo', + area: 'System::Branding', + description: 'Defines the logo of the application, shown in the web interface.', + options: { + form: [ + { + display: '', + null: false, + name: 'product_logo', + tag: 'input', + }, + ], + }, + state: 'logo.svg', + frontend: true +) Setting.create_if_not_exists( title: 'SystemID',