├── .gitattributes ├── css ├── dashboard_icon.css ├── dashboard-button-options.css └── dashboard.css ├── images ├── add.gif ├── close.png ├── trash.png ├── delete.png ├── dropdown.gif ├── configure.png ├── dashboard.png ├── panel-add.png ├── panel-blog.png ├── panel-grid.png ├── quick-links.png └── dashboard-panel-default.png ├── _config.php ├── _config ├── adminpanels.yml ├── legacy.yml └── config.yml ├── templates └── UncleCheese │ └── Dashboard │ ├── DashboardQuickLinksPanel.ss │ ├── DashboardDropdownField.ss │ ├── DashboardHasManyRelationEditorDetailForm.ss │ ├── DashboardButtonOptionsField.ss │ ├── DashboardChart.ss │ ├── DashboardButtonOptionsField_holder.ss │ ├── CMSMain_ListView.ss │ ├── DashboardHasManyRelationEditor.ss │ ├── DashboardPanel.ss │ └── Dashboard_Content.ss ├── .editorconfig ├── src ├── DashboardSiteConfig.php ├── extensions │ └── DashboardItemEditForm.php ├── DashboardButtonOptionsField.php ├── panels │ ├── DashboardQuickLinksPanel.php │ └── DashboardPanel.php ├── DashboardQuickLink.php ├── DashboardPanelDataObject.php ├── DashboardPanelAction.php ├── DashboardMember.php ├── DashboardChart.php ├── DashboardHasManyRelationEditor.php └── Dashboard.php ├── composer.json ├── javascript ├── dashboard-gridfield-panel.js ├── dashboard-button-options.js ├── dashboard-chart.js ├── dashboard.js └── jquery.flip.js ├── lang ├── en.yml ├── es.yml ├── fi.yml ├── nl.yml └── de.yml ├── README.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | /.gitignore export-ignore 2 | -------------------------------------------------------------------------------- /css/dashboard_icon.css: -------------------------------------------------------------------------------- 1 | .icon.icon-dashboard {background-image:url(../images/dashboard.png);} 2 | -------------------------------------------------------------------------------- /images/add.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/add.gif -------------------------------------------------------------------------------- /images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/close.png -------------------------------------------------------------------------------- /images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/trash.png -------------------------------------------------------------------------------- /images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/delete.png -------------------------------------------------------------------------------- /images/dropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/dropdown.gif -------------------------------------------------------------------------------- /images/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/configure.png -------------------------------------------------------------------------------- /images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/dashboard.png -------------------------------------------------------------------------------- /images/panel-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/panel-add.png -------------------------------------------------------------------------------- /images/panel-blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/panel-blog.png -------------------------------------------------------------------------------- /images/panel-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/panel-grid.png -------------------------------------------------------------------------------- /images/quick-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/quick-links.png -------------------------------------------------------------------------------- /images/dashboard-panel-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecheese/silverstripe-dashboard/HEAD/images/dashboard-panel-default.png -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | 2 | <% loop $Links %> 3 |
  • target="_blank"<% end_if %> class="ss-ui-button dashboard-panel-quick-link" href="$Link">$Text
  • 4 | <% end_loop %> 5 | -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardDropdownField.ss: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardHasManyRelationEditorDetailForm.ss: -------------------------------------------------------------------------------- 1 |
    2 | <% loop $DetailForm.Fields %> 3 | $FieldHolder 4 | <% end_loop %> 5 |
    6 | <% loop $DetailForm.Actions %> 7 | $Field 8 | <% end_loop %> 9 |
    10 |
    -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardButtonOptionsField.ss: -------------------------------------------------------------------------------- 1 |
    2 | <% loop Options %>$Title<% end_loop %> 3 | 4 |
    -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: dashboard 3 | --- 4 | SilverStripe\Security\Member: 5 | extensions: 6 | [UncleCheese\Dashboard\DashboardMember] 7 | 8 | SilverStripe\SiteConfig\SiteConfig: 9 | extensions: 10 | [UncleCheese\Dashboard\DashboardSiteConfig] 11 | SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest: 12 | extensions: 13 | [UncleCheese\Dashboard\DashboardItemEditForm] 14 | 15 | SilverStripe\Admin\LeftAndMain: 16 | extra_requirements_css: 17 | [unclecheese/dashboard:css/dashboard_icon.css] -------------------------------------------------------------------------------- /src/DashboardSiteConfig.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DashboardSiteConfig extends DataExtension { 15 | 16 | 17 | private static $has_many = [ 18 | 'DashboardPanels' => DashboardPanel::class, 19 | ]; 20 | 21 | 22 | } -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardChart.ss: -------------------------------------------------------------------------------- 1 |
    11 | 12 |
    13 | 14 | <% loop $ChartData %> 15 |
    16 | <% end_loop %> 17 | 18 | 19 |
    -------------------------------------------------------------------------------- /src/extensions/DashboardItemEditForm.php: -------------------------------------------------------------------------------- 1 | owner->request->getVar('ID')) { 20 | Injector::inst()->get(CMSMain::class)->setCurrentPageID($id); 21 | $form->Fields()->push(new HiddenField('ID','', $id)); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unclecheese/dashboard", 3 | "description": "Adds a dashboard to the SilverStripe CMS", 4 | "type": "silverstripe-vendormodule", 5 | "keywords": ["silverstripe", "dashboard", "module", "cms"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Uncle Cheese", 10 | "email": "unclecheese@leftandmain.com" 11 | } 12 | ], 13 | "require": 14 | { 15 | "silverstripe/framework": "^4.0" 16 | }, 17 | "replace": {"silverstripe/dashboard": "*"}, 18 | "extra": { 19 | "expose": [ 20 | "css", 21 | "images", 22 | "javascript" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/DashboardButtonOptionsField.php: -------------------------------------------------------------------------------- 1 | Size = $size; 25 | return $this; 26 | } 27 | 28 | 29 | 30 | 31 | } -------------------------------------------------------------------------------- /javascript/dashboard-gridfield-panel.js: -------------------------------------------------------------------------------- 1 | 2 | (function($) { 3 | 4 | $('.DashboardGridFieldPanel [name=SubjectPageID]').entwine({ 5 | 6 | 7 | loadResults: function() { 8 | var $t = this; 9 | $t.getPanel().find('[name=GridFieldName]').attr('disabled',true); 10 | $.ajax({ 11 | url: this.data('lookupurl'), 12 | dataType: "JSON", 13 | data: { 14 | "pageid": this.val() 15 | }, 16 | success: function(data) { 17 | html = ""; 18 | for(value in data){ 19 | html +=""; 20 | } 21 | $t.getPanel().find('[name=GridFieldName]').attr('disabled',false).html(html); 22 | } 23 | }); 24 | }, 25 | 26 | 27 | onchange: function(e) { 28 | e.preventDefault(); 29 | this.loadResults(); 30 | } 31 | 32 | 33 | 34 | }) 35 | 36 | })(jQuery); -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardButtonOptionsField_holder.ss: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 | 6 | 7 | 8 |
    9 | <% if $RightTitle %><% end_if %> 10 | <% if $Message %>$Message<% end_if %> 11 | <% if $Description %>$Description<% end_if %> 12 |
    13 | -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/CMSMain_ListView.ss: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Dashboard 4 |
    5 | 6 |
    7 | 8 |
    9 | $AddForm 10 |
    11 | 12 |
    13 | <% if $TreeIsFiltered %> 14 |
    15 | <%t SilverStripe\CMS\Controllers\CMSMain.ListFiltered 'Filtered list.' %> 16 | 17 | <%t SilverStripe\CMS\Controllers\CMSMain.TreeFilteredClear 'Clear filter' %> 18 | 19 |
    20 | <% end_if %> 21 | 22 |
    23 | $ListViewForm 24 |
    25 |
    -------------------------------------------------------------------------------- /javascript/dashboard-button-options.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $('.dashboard-button-options-btn-group').entwine({ 3 | 4 | 5 | onmatch: function() { 6 | this.setValue(); 7 | }, 8 | 9 | setValue: function(val) { 10 | if(!val) val = this.find('[type=hidden]').val(); 11 | var $t = this; 12 | this.find('a').removeClass('active').each(function() { 13 | if($(this).data('value') == val) { 14 | $(this).addClass('active'); 15 | $t.getInput().val(val); 16 | } 17 | }) 18 | }, 19 | 20 | 21 | getValue: function() { 22 | return this.getInput().val(); 23 | }, 24 | 25 | getInput: function() { 26 | return this.find('[type=hidden]'); 27 | } 28 | 29 | }); 30 | 31 | 32 | $('.dashboard-button-options-btn-group *').entwine({ 33 | getButtonGroup: function() { 34 | return this.closest(".dashboard-button-options-btn-group"); 35 | } 36 | }) 37 | 38 | 39 | $('.dashboard-button-options-btn-group > a').entwine({ 40 | onclick: function(e) { 41 | e.preventDefault(); 42 | this.getButtonGroup().setValue(this.data('value')); 43 | } 44 | }) 45 | })(jQuery); -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardHasManyRelationEditor.ss: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | <%t UncleCheese\Dashboard\Dashboard.ADD 'Add' %> 5 |
    6 | <% if $Items %> 7 | 15 | <% else %> 16 |
    <%t UncleCheese\Dashboard\Dashboard.NORECORDS 'No records' %>
    17 | <% end_if %> 18 | 19 |
    20 | 21 |
    22 |
    23 | -------------------------------------------------------------------------------- /src/panels/DashboardQuickLinksPanel.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class DashboardQuickLinksPanel extends DashboardPanel { 12 | 13 | private static $table_name = 'DashboardQuickLinksPanel'; 14 | 15 | private static $has_many = [ 16 | 'Links' => DashboardQuickLink::class 17 | ]; 18 | 19 | 20 | 21 | private static $defaults = [ 22 | 'PanelSize' => "small" 23 | ]; 24 | 25 | 26 | 27 | private static $icon = "unclecheese/dashboard:images/quick-links.png"; 28 | 29 | 30 | 31 | private static $configure_on_create = true; 32 | 33 | 34 | 35 | public function getLabel() { 36 | return _t('UncleCheese\Dashboard\Dashboard.QUICKLINKSLABEL','Quick Links'); 37 | } 38 | 39 | 40 | 41 | public function getDescription() { 42 | return _t('UncleCheese\Dashboard\Dashbaord.QUICKLINKSDESCRIPTION','Allows management of arbitrary links from the dashboard'); 43 | } 44 | 45 | 46 | public function getConfiguration() { 47 | $fields = parent::getConfiguration(); 48 | $fields->push(DashboardHasManyRelationEditor::create($this, "Links", DashboardQuickLink::class)); 49 | return $fields; 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/DashboardQuickLink.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DashboardQuickLink extends DashboardPanelDataObject { 15 | 16 | private static $table_name = 'DashboardQuickLink'; 17 | 18 | private static $db = [ 19 | 'Link' => 'Varchar(255)', 20 | 'Text' => 'Varchar(50)', 21 | 'NewWindow' => 'Boolean' 22 | ]; 23 | 24 | private static $has_one = [ 25 | 'Panel' => DashboardQuickLinksPanel::class, 26 | ]; 27 | 28 | 29 | 30 | private static $label_field = "Text"; 31 | 32 | 33 | 34 | 35 | public function getConfiguration() { 36 | $fields = parent::getConfiguration(); 37 | $fields->push(TextField::create("Link",_t('UncleCheese\Dashboard\DashboardQuickLink.LINK','Link (include http://)'))); 38 | $fields->push(TextField::create("Text",_t('UncleCheese\Dashboard\DashboardQuickLink.LINKTEXT','Link text'))); 39 | $fields->push(CheckboxField::create("NewWindow",_t('UncleCheese\Dashboard\DashboardQuickLink.NEWWINDOW','Open link in new window'))); 40 | return $fields; 41 | } 42 | } -------------------------------------------------------------------------------- /javascript/dashboard-chart.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $('.dashboard-chart').entwine({ 3 | onmatch: function() { 4 | var $t = this; 5 | this.getData().hide(); 6 | var data = new google.visualization.DataTable(); 7 | data.addColumn('string', this.data('xlabel')); 8 | data.addColumn('number', this.data('ylabel')); 9 | this.getData().each(function() { 10 | data.addRow([ 11 | $(this).data('x'), $(this).data('y') 12 | ]); 13 | }); 14 | 15 | var chart = new google.visualization.AreaChart(document.getElementById(this.getChart().attr('id'))); 16 | chart.draw(data, {width: $t.innerWidth(), height: $t.data('height'), title: $t.data('title'), 17 | colors:['#058dc7','#e6f4fa'], 18 | areaOpacity: 0.1, 19 | hAxis: {textPosition: $t.data('textposition'), showTextEvery: $t.data('textinterval'), slantedText: false, textStyle: { color: '#058dc7', fontSize: $t.data('fontsize') } }, 20 | pointSize: $t.data('pointsize'), 21 | legend: 'none', 22 | chartArea:{left:0,top:30,width:"100%",height:"100%"} 23 | }); 24 | }, 25 | 26 | 27 | getChart: function() { 28 | return this.find('.dashboard-chart-canvas'); 29 | }, 30 | 31 | 32 | getData: function() { 33 | return this.find('.dashboard-chart-data'); 34 | } 35 | 36 | 37 | }); 38 | 39 | 40 | 41 | })(jQuery); 42 | -------------------------------------------------------------------------------- /css/dashboard-button-options.css: -------------------------------------------------------------------------------- 1 | .dashboard-button-options-btn-group > a { 2 | padding: 0.5em; 3 | border-radius: 5px; 4 | border: 1px solid #C0C0C2; 5 | border-bottom: 1px solid #A6A6A9; 6 | background-color: #E6E6E6; 7 | background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, white), color-stop(100%, #D9D9D9)); 8 | background: -webkit-linear-gradient(white, #D9D9D9); 9 | background: -moz-linear-gradient(white, #D9D9D9); 10 | background: -o-linear-gradient(white, #D9D9D9); 11 | text-shadow: white 0 1px 1px; 12 | color: #333; 13 | font-weight: bold; 14 | } 15 | 16 | .dashboard-button-options-btn-group > a:hover { 17 | text-decoration:none; 18 | background: -webkit-linear-gradient(white, #E6E6E6); 19 | background: -moz-linear-gradient(white, #E6E6E6); 20 | background: -o-linear-gradient(white, #E6E6E6); 21 | background: linear-gradient(white, #E6E6E6); 22 | -webkit-box-shadow: 0 0 5px #B3B3B3; 23 | -moz-box-shadow: 0 0 5px #b3b3b3; 24 | box-shadow: 0 0 5px #B3B3B3; 25 | } 26 | .dashboard-button-options-btn-group > a.active { 27 | background:white; 28 | } 29 | .dashboard-button-options-btn-group > a.middle { 30 | border-radius:0; 31 | } 32 | 33 | .dashboard-button-options-btn-group > a.first { 34 | border-radius: 5px 0 0 5px; 35 | border-right: 0; 36 | } 37 | 38 | .dashboard-button-options-btn-group > a.last { 39 | border-radius: 0 5px 5px 0; 40 | border-left: 0; 41 | } 42 | 43 | .dashboard-button-options-btn-group > a img { 44 | vertical-align:middle; 45 | } 46 | 47 | .dashboard-button-options-btn-group.small > a { 48 | padding:0.1em 0.3em; 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/DashboardPanelDataObject.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class DashboardPanelDataObject extends DataObject { 18 | 19 | private static $table_name = 'DashboardPanelDataObject'; 20 | 21 | private static $db = [ 22 | 'SortOrder' => 'Int' 23 | ]; 24 | 25 | 26 | 27 | private static $has_one = [ 28 | 'DashboardPanel' => 'DashboardPanel' 29 | ]; 30 | 31 | 32 | private static $default_sort = "SortOrder ASC"; 33 | 34 | 35 | 36 | /** 37 | * @var string Like $summary_fields, but these objects only render one field in list view. 38 | */ 39 | private static $label_field = "ID"; 40 | 41 | 42 | /** 43 | * @return FieldList 44 | */ 45 | public function getConfiguration() { 46 | $fields = FieldList::create(); 47 | return $fields; 48 | } 49 | 50 | 51 | 52 | 53 | /** 54 | * Gets a form for editing or creating this object 55 | * 56 | * TODO: Is this used? Seems to be broken but dunno if it affects anything. 57 | * @return Form 58 | */ 59 | public function getConfigFields() { 60 | $form = Form::create(Injector::inst()->get(Dashboard::class), "Form", $this->getConfiguration()); 61 | } 62 | 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/DashboardPanelAction.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DashboardPanelAction extends ViewableData { 14 | 15 | 16 | /** 17 | * @var string The link for this action button 18 | */ 19 | protected $Link; 20 | 21 | 22 | 23 | /** 24 | * @var string The title (label) of the button 25 | */ 26 | protected $Title; 27 | 28 | 29 | 30 | /** 31 | * @var string The type of action. Default is the plain button color. 32 | * A value of "good" will provide a green "constructive" button 33 | * 34 | * @todo More button types? 35 | */ 36 | protected $Type; 37 | 38 | 39 | 40 | 41 | public function __construct($link, $title, $type = null) { 42 | $this->Link = $link; 43 | $this->Title = $title; 44 | $this->Type = $type; 45 | } 46 | 47 | 48 | 49 | 50 | /** 51 | * Converts the simple type name into a real SS CSS class. 52 | * 53 | * @return string 54 | */ 55 | public function getUIClass() { 56 | switch($this->Type) { 57 | case "good": 58 | return "btn-primary"; 59 | 60 | } 61 | return ""; 62 | } 63 | 64 | 65 | 66 | /** 67 | * Gets the HTML link 68 | * 69 | * @return string 70 | */ 71 | public function forTemplate() { 72 | return "$this->Title"; 73 | } 74 | 75 | 76 | 77 | 78 | /** 79 | * A template accessor used to render this object 80 | * 81 | * @return string 82 | */ 83 | public function Action() { 84 | return $this->forTemplate(); 85 | } 86 | 87 | 88 | } -------------------------------------------------------------------------------- /src/DashboardMember.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DashboardMember extends DataExtension { 17 | 18 | 19 | 20 | private static $db = [ 21 | 'HasConfiguredDashboard' => 'Boolean' 22 | ]; 23 | 24 | 25 | 26 | private static $has_many = [ 27 | 'DashboardPanels' => DashboardPanel::class, 28 | ]; 29 | 30 | 31 | 32 | /** 33 | * Removes the DashboardPanels tab from the Security section. Panels should not be managed there. 34 | */ 35 | public function updateCMSFields(FieldList $fields) { 36 | $fields->removeByName("DashboardPanels"); 37 | } 38 | 39 | 40 | 41 | /** 42 | * Ensures that new members get the default dashboard configuration. Once it has been applied, 43 | * make sure this doesn't happen again, if for some reason a user insists on having an empty 44 | * dashboard. 45 | * @throws \SilverStripe\ORM\ValidationException 46 | */ 47 | public function onAfterWrite() { 48 | if(!$this->owner->HasConfiguredDashboard && !$this->owner->DashboardPanels()->exists()) { 49 | /** @var DashboardPanel $p */ 50 | foreach(SiteConfig::current_site_config()->DashboardPanels() as $p) { 51 | $clone = $p->duplicate(); 52 | $clone->SiteConfigID = 0; 53 | $clone->MemberID = $this->owner->ID; 54 | $clone->write(); 55 | } 56 | 57 | DB::query("UPDATE \"Member\" SET \"HasConfiguredDashboard\" = 1 WHERE \"ID\" = {$this->owner->ID}"); 58 | $this->owner->flushCache(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/DashboardPanel.ss: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <% if $PrimaryActions %> 5 |
    6 | <% loop $PrimaryActions %> 7 | $Action 8 | <% end_loop %> 9 |
    10 | <% end_if %> 11 | 12 |
    13 | 14 |
    15 | 16 |

    $Title

    17 |
    18 | 19 |
    20 | $Content 21 |
    22 | 23 | 40 | 41 |
    42 |
    43 |
    44 | <% loop $Form.Fields %> 45 | $FieldHolder 46 | <% end_loop %> 47 |
    48 |
    49 | <% loop $Form.Actions %> 50 | $Field 51 | <% end_loop %> 52 |
    53 |
    54 |
    55 |
    56 |
    -------------------------------------------------------------------------------- /templates/UncleCheese/Dashboard/Dashboard_Content.ss: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <% include SilverStripe/Admin/CMSBreadcrumbs %> 5 |
    6 | 21 |
    22 |
    23 |
    24 |
    $PanelHolder
    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 |

    <%t UncleCheese\Dashboard\Dashboard.CHOOSEPANELTYPE 'Choose a panel type' %>

    38 |
    39 |
    40 | <% loop $AllPanels %> 41 |
    data-configure="true"<% end_if %>> 42 |
    43 | 44 |
    45 |
    46 |

    $Label

    47 |

    $Description

    48 |
    49 |
    50 | <% end_loop %> 51 |
    52 | 57 |
    58 |
    59 |
    60 |
    61 | 62 |
    63 | -------------------------------------------------------------------------------- /src/DashboardChart.php: -------------------------------------------------------------------------------- 1 | 15 | * @package Dashboard 16 | */ 17 | class DashboardChart extends ViewableData { 18 | 19 | 20 | /** 21 | * @var int A count of the instances, used to create a unique ID for the chart 22 | */ 23 | private static $instances = 0; 24 | 25 | 26 | 27 | /** 28 | * @var array The chart data, in x/y pairs. The Y value must be an integer 29 | */ 30 | protected $chartData = []; 31 | 32 | 33 | 34 | /** 35 | * @var int The number of points between each text label on the X axis 36 | */ 37 | public $TextInterval = 5; 38 | 39 | 40 | 41 | /** 42 | * @var int The height of the chart in pixels 43 | */ 44 | public $Height = 200; 45 | 46 | 47 | 48 | /** 49 | * @var int The size of the circle on each data point, in pixels 50 | */ 51 | public $PointSize = 5; 52 | 53 | 54 | 55 | /** 56 | * @var int The font size on the chart 57 | */ 58 | public $FontSize = 10; 59 | 60 | 61 | 62 | 63 | /** 64 | * @var string The position of the text on the chart 65 | */ 66 | public $TextPosition = 'in'; 67 | 68 | 69 | 70 | 71 | /** 72 | * Creates a new instance of a DashboardChart 73 | * 74 | * @param string The title of the chart 75 | * @param string The label of the X axis 76 | * @param string The label for the Y axis 77 | * @param array The chart data, in x/y pairs 78 | * @return DashboardChart 79 | */ 80 | public static function create(...$args) { 81 | list($title, $x_label, $y_label, $chartData) = $args; 82 | if ($chartData === null) $chartData = []; 83 | self::$instances++; 84 | return new DashboardChart($title, $x_label, $y_label, $chartData); 85 | } 86 | 87 | 88 | 89 | 90 | /** 91 | * Constructor for the DashboardChart 92 | * 93 | * @param string The title of the chart 94 | * @param string The label of the X axis 95 | * @param string The label for the Y axis 96 | * @param array The chart data, in x/y pairs 97 | */ 98 | public function __construct($title = null, $x_label = null, $y_label = null, $chartData = []) { 99 | if(!is_array($chartData)) { 100 | user_error("DashboardChart: \$chartData must be an array", E_USER_ERROR); 101 | } 102 | 103 | $this->chartData = $chartData; 104 | $this->Title = $title; 105 | $this->YAxisLabel = $y_label; 106 | $this->XAxisLabel = $x_label; 107 | } 108 | 109 | 110 | 111 | 112 | /** 113 | * The ID of the chart. Javascript needs to target a specific element 114 | * 115 | * @return string 116 | */ 117 | public function getChartID() { 118 | return "dashboard-chart-".self::$instances; 119 | } 120 | 121 | 122 | 123 | 124 | /** 125 | * Gets a list of x/y pairs for the template 126 | * 127 | * @return ArrayList 128 | */ 129 | public function getChartData() { 130 | $list = ArrayList::create([]); 131 | foreach($this->chartData as $x => $y) { 132 | $list->push(ArrayData::create([ 133 | 'XValue' => $x, 134 | 'YValue' => $y 135 | ])); 136 | } 137 | return $list; 138 | } 139 | 140 | 141 | 142 | 143 | /** 144 | * Adds a single data point to the chart 145 | * 146 | * @param string The X value 147 | * @param int The Y value 148 | */ 149 | public function addData($x, $y) { 150 | $this->chartData[$x] = $y; 151 | } 152 | 153 | 154 | 155 | 156 | /** 157 | * Sets the chart data, in x/y pairs 158 | * 159 | * @param array The chart data 160 | */ 161 | public function setData($data) { 162 | $this->chartData = $data; 163 | } 164 | 165 | 166 | 167 | 168 | /** 169 | * Renders the chart and loads the dependencies 170 | * 171 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 172 | */ 173 | public function forTemplate() { 174 | Requirements::javascript("dashboard/javascript/thirdparty/google_jsapi_visualization.js"); 175 | Requirements::javascript("dashboard/javascript/dashboard-chart.js"); 176 | return $this->renderWith('DashboardChart'); 177 | } 178 | 179 | 180 | } -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | SilverStripe\CMS\Controllers\CMSMain: 3 | AddNew: 'Add new page' 4 | ListFiltered: 'Filtered list.' 5 | TreeFilteredClear: 'Clear filter' 6 | UncleCheese\Dashboard\Dashboard_Content: 7 | ADMINISTRATION: Administration 8 | UncleCheese\Dashboard\DashboardModelAdmin: 9 | COUNT: 'Number of records to display' 10 | UncleCheese\Dashboard\Dashboard: 11 | QUICKLINKSDESCRIPTION: 'Allows management of arbitrary links from the dashboard' 12 | SECTIONEDITORDESCRIPTION: 'Pulls pages from a section of the website for viewing and creation' 13 | ACCESS: 'Access to ''{title}'' section' 14 | ACCESS_HELP: 'Allow user to remove panels from his/her dashboard' 15 | ACUSTOMPATH: 'Filter by a specific path' 16 | ADD: Add 17 | ADDPANEL: 'New Panel' 18 | ADDPANELS: 'Add dashboard panels' 19 | APPLYTOALL: 'Apply this dashboard to all members' 20 | APPLYTOALLSUCCESS: 'Success! This dashboard configuration has been applied to all members who have dashboard access.' 21 | CANCEL: Cancel 22 | CELCIUS: Celcius 23 | CHOOSEPANELTYPE: 'Choose a panel type' 24 | CONFIGUREANELS: 'Configure dashboard panels' 25 | CREATENEW: 'Create new %s' 26 | DATEFORMAT: 'Date format' 27 | DATERANGE: 'Date range' 28 | DAY: Day 29 | DELETEPANELS: 'Remove dashboard panels' 30 | ENTIRESITE: 'Entire site' 31 | FARENHEIT: Farenheit 32 | FILTERBYPAGE: Filter 33 | GAACCOUNTEMAIL: 'Google account email' 34 | GAACCOUNTPASSWORD: 'Google account password' 35 | GAACCOUNTPROFILE: 'Profile ID (located in the "Profile Settings" of Google Analytics)' 36 | GOOGLEDESCRIPTION: 'Displays a Google Analytics chart for a given page' 37 | GRIDFIELDPANELTITLE: 'Grid Field Editor' 38 | GRIDFIELDPANELDESCRIPTION: 'Adds a summary view of a GridField instance on a given page' 39 | LOCATION: Location 40 | MENUTITLE: Dashboard 41 | MODELADMINCLASS: 'Model admin tab' 42 | MODELADMINMODEL: Model 43 | MODELADMINPANELDESCRIPTION: 'Adds a summary view of a Model Admin section of the CMS' 44 | MODELADMINPANELTITLE: 'Model Admin Editor' 45 | PANELSIZE: '' 46 | NONESHOWALL: 'No filter. Show analytics for the entire site' 47 | NORECORDS: 'No records' 48 | PAGEINLIST: 'Filter by a specific page in the tree' 49 | PAGEVIEWS: Pageviews 50 | PATH: Path 51 | PLEASESELECT: 'Please select' 52 | PREVIOUSSEVENDAYS: '7 days' 53 | PREVIOUSTHIRTYDAYS: '30 days' 54 | PREVIOUSYEAR: '365 days' 55 | QUICKLINKSLABEL: 'Quick Links' 56 | RSSFEED: 'RSS Feed' 57 | RSSFEEDDESCRIPTION: 'Adds an RSS feed from any public URL' 58 | SAVE: Save 59 | SECTIONEDTIORLABEL: 'Section Editor' 60 | SELECTLOCATION: 'Please select a location whose weather you want to display.' 61 | SETASDEFAULT: 'Make this the default dashboard' 62 | SETASDEFAULTSUCCESS: 'Success! This dashboard configuration has been set as the default for all new members.' 63 | TITLE: Title 64 | TODAY: Today 65 | TOMORROW: Tomorrow 66 | UNITS: Units 67 | VIEWALL: 'View all %s' 68 | VIEWFORECAST: 'View full forecast' 69 | WEATHER: Weather 70 | WEATHERDESCRIPTION: 'Shows the weather for a given location.' 71 | WEATHERNORESPONSE: 'The weather server did not respond. Try again in a few minutes.' 72 | UncleCheese\Dashboard\DashboardGoogleAnalyticsPanel: 73 | PLURALNAME: 'Dashboard Google Analytics Panels' 74 | SINGULARNAME: 'Dashboard Google Analytics Panel' 75 | UncleCheese\Dashboard\DashboardModelAdminPanel: 76 | PLURALNAME: 'Model Admins' 77 | SINGULARNAME: 'Model Admin' 78 | UncleCheese\Dashboard\DashboardPanel: 79 | PLURALNAME: 'Dashboard Panels' 80 | SINGULARNAME: 'Dashboard Panel' 81 | UncleCheese\Dashboard\DashboardPanelDataObject: 82 | PLURALNAME: 'Dashboard Panel Data Objects' 83 | SINGULARNAME: 'Dashboard Panel Data Object' 84 | UncleCheese\Dashboard\DashboardQuickLink: 85 | LINK: 'Link (include http://)' 86 | LINKTEXT: 'Link text' 87 | NEWWINDOW: 'Open link in new window' 88 | PLURALNAME: 'Dashboard Quick Links' 89 | SINGULARNAME: 'Dashboard Quick Link' 90 | UncleCheese\Dashboard\DashboardQuickLinksPanel: 91 | PLURALNAME: 'Dashboard Quick Links Panels' 92 | SINGULARNAME: 'Dashboard Quick Links Panel' 93 | UncleCheese\Dashboard\DashboardRSSFeedPanel: 94 | PLURALNAME: 'Dashboard R S S Feed Panels' 95 | SINGULARNAME: 'Dashboard R S S Feed Panel' 96 | UncleCheese\Dashboard\DashboardRecentEdits: 97 | COUNT: 'Number of pages to display' 98 | UncleCheese\Dashboard\DashboardRecentEditsPanel: 99 | PLURALNAME: 'Dashboard Recent Edits Panels' 100 | SINGULARNAME: 'Dashboard Recent Edits Panel' 101 | UncleCheese\Dashboard\DashboardRecentFile: 102 | COUNT: 'Number of files to display' 103 | UncleCheese\Dashboard\DashboardRecentFilesPanel: 104 | PLURALNAME: 'Dashboard Recent Files Panels' 105 | SINGULARNAME: 'Dashboard Recent Files Panel' 106 | UncleCheese\Dashboard\DashboardSectionEditorPanel: 107 | PLURALNAME: 'Dashboard Section Editor Panels' 108 | SINGULARNAME: 'Dashboard Section Editor Panel' 109 | UncleCheese\Dashboard\DashboardWeatherPanel: 110 | PLURALNAME: 'Dashboard Weather Panels' 111 | SINGULARNAME: 'Dashboard Weather Panel' 112 | SilverStripe\Security\Permission: 113 | CMS_ACCESS_CATEGORY: 'CMS Access' 114 | RecentEdits: 115 | DESCRIPTION: 'Shows a linked list of recently edited pages' 116 | LABEL: 'Recent Edits' 117 | RecentFiles: 118 | DESCRIPTION: 'Shows a linked list of recently edited files' 119 | LABEL: 'Recent Files' 120 | -------------------------------------------------------------------------------- /lang/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | SilverStripe\CMS\Controllers\CMSMain: 3 | AddNew: 'Agregar nueva página' 4 | ListFiltered: 'Lista filtrada.' 5 | TreeFilteredClear: 'Limpiar filtro' 6 | UncleCheese\Dashboard\Dashboard_Content: 7 | ADMINISTRATION: Administración 8 | UncleCheese\Dashboard\DashbordModelAdmin: 9 | COUNT: 'Objetos a mostrar' 10 | UncleCheese\Dashboard\Dashboard: 11 | QUICKLINKSDESCRIPTION: 'Enlaces arbitrarios en el panel de control' 12 | SECTIONEDITORDESCRIPTION: 'Agrega páginas a sección del sitio' 13 | ACCESS: 'Acceso a la sección ''{title}''' 14 | ACCESS_HELP: 'Permite al usuario remover paneles desde su panel de control' 15 | ACUSTOMPATH: 'Filtrar por una ruta específica' 16 | ADD: Agregar 17 | ADDPANEL: 'Nuevo panel' 18 | ADDPANELS: 'Agregar paneles de control' 19 | APPLYTOALL: 'Aplicar este panel de control a todos los miembros' 20 | APPLYTOALLSUCCESS: 'Excelente! El panel de control ha sido aplicado a todos los miembros que tienen acceso a paneles de control' 21 | CANCEL: Cancelar 22 | CELCIUS: Celcius 23 | CHOOSEPANELTYPE: 'Elegir tipo de panel' 24 | CONFIGUREANELS: 'Configurar paneles de control' 25 | CREATENEW: 'Crear' 26 | DATEFORMAT: 'Formato de fecha' 27 | DATERANGE: 'Rango de fecha' 28 | DAY: Día 29 | DELETEPANELS: 'Remover paneles de contro' 30 | ENTIRESITE: 'Sitio completo' 31 | FARENHEIT: Farenheit 32 | FILTERBYPAGE: Filtro 33 | GAACCOUNTEMAIL: 'Google email' 34 | GAACCOUNTPASSWORD: 'Google password' 35 | GAACCOUNTPROFILE: 'ID del perfil (ubicado en la sección "Configuración de perfil de Google Analytics)' 36 | GOOGLEDESCRIPTION: 'Analytics para una página o el sitio completo' 37 | LOCATION: Ubicación 38 | MENUTITLE: Panel de control 39 | MODELADMINCLASS: 'Admin a mostrar' 40 | MODELADMINMODEL: Modelo 41 | MODELADMINPANELDESCRIPTION: 'Administrador de Objetos' 42 | MODELADMINPANELTITLE: 'Editor de Objetos' 43 | PANELSIZE: '' 44 | NONESHOWALL: 'Sin filtro. Muestra analytics para el sitio completo' 45 | NORECORDS: 'No hay información' 46 | PAGEINLIST: 'Filtrar por una página específica del sitio' 47 | PAGEVIEWS: Vistas 48 | PATH: Ruta 49 | PLEASESELECT: 'Por favor seleccione' 50 | PREVIOUSSEVENDAYS: '7 días' 51 | PREVIOUSTHIRTYDAYS: '30 días' 52 | PREVIOUSYEAR: '365 días' 53 | QUICKLINKSLABEL: 'Enlaces rápidos' 54 | RSSFEED: 'RSS' 55 | RSSFEEDDESCRIPTION: 'Agrega un RSS desde cualquier URL pública' 56 | SAVE: Guardar 57 | SECTIONEDTIORLABEL: 'Editor de secciones' 58 | SELECTLOCATION: 'Por favor, seleccione la ubicación de la cual quieres mirar el clima.' 59 | SETASDEFAULT: 'Convertir este panel de control en el por defecto' 60 | SETASDEFAULTSUCCESS: 'Excelente! Este panel de control ha sido configurado para todos los miembros.' 61 | TITLE: Título 62 | TODAY: Hoy 63 | TOMORROW: Mañana 64 | UNITS: Unidades 65 | VIEWALL: 'Ver todos' 66 | VIEWFORECAST: 'Ver pronóstico completo' 67 | WEATHER: Clima 68 | WEATHERDESCRIPTION: 'Clima de un lugar en el mundo.' 69 | WEATHERNORESPONSE: 'El servidor de climas no responde, deben tener una tormenta por ahí.' 70 | UncleCheese\Dashboard\DashboardGoogleAnalyticsPanel: 71 | PLURALNAME: 'Paneles de control para Google Analytics' 72 | SINGULARNAME: 'Panel de control para Google Analytics' 73 | UncleCheese\Dashboard\DashboardModelAdminPanel: 74 | PLURALNAME: 'Model Admins' 75 | SINGULARNAME: 'Model Admin' 76 | UncleCheese\Dashboard\DashboardPanel: 77 | PLURALNAME: 'Paneles de control' 78 | SINGULARNAME: 'Panel de control' 79 | UncleCheese\Dashboard\DashboardPanelDataObject: 80 | PLURALNAME: 'Paneles de control para Data Objects' 81 | SINGULARNAME: 'Panel de control para Data Objects' 82 | UncleCheese\Dashboard\DashboardQuickLink: 83 | LINK: 'Enlace (incluír http://)' 84 | LINKTEXT: 'Texto del enlace' 85 | NEWWINDOW: 'Abrir el enlace en una ventana nueva' 86 | PLURALNAME: 'Enlaces rápidos para el panel de control' 87 | SINGULARNAME: 'Enlace rápido para el panel de control' 88 | UncleCheese\Dashboard\DashboardQuickLinksPanel: 89 | PLURALNAME: 'Paneles de control para enlaces' 90 | SINGULARNAME: 'Panel de control para enlaces' 91 | UncleCheese\Dashboard\DashboardRSSFeedPanel: 92 | PLURALNAME: 'Paneles de control para RSS' 93 | SINGULARNAME: 'Panel de control para RSS' 94 | UncleCheese\Dashboard\DashboardRecentEdits: 95 | COUNT: 'Número de páginas a mostrar' 96 | UncleCheese\Dashboard\DashboardRecentEditsPanel: 97 | PLURALNAME: 'Paneles de control para páginas recién editadas' 98 | SINGULARNAME: 'Panel de control para páginas recién editadas' 99 | UncleCheese\Dashboard\DashboardRecentFile: 100 | COUNT: 'Número de archivos a mostrar' 101 | UncleCheese\Dashboard\DashboardRecentFilesPanel: 102 | PLURALNAME: 'Paneles de control para archivos' 103 | SINGULARNAME: 'Panel de control para archivos' 104 | UncleCheese\Dashboard\DashboardSectionEditorPanel: 105 | PLURALNAME: 'Paneles de control para el panel de edición' 106 | SINGULARNAME: 'Panel de control para el panel de edición' 107 | UncleCheese\Dashboard\DashboardWeatherPanel: 108 | PLURALNAME: 'Paneles de control para el panel del tiempo' 109 | SINGULARNAME: 'Panel de control para el panel del tiempo' 110 | SilverStripe\Security\Permission: 111 | CMS_ACCESS_CATEGORY: 'Acceso al CMS' 112 | RecentEdits: 113 | DESCRIPTION: 'Páginas recientemente editadas' 114 | LABEL: 'Ediciones recientes' 115 | RecentFiles: 116 | DESCRIPTION: 'Archivos recientemente editados' 117 | LABEL: 'Archivos recientes' 118 | -------------------------------------------------------------------------------- /lang/fi.yml: -------------------------------------------------------------------------------- 1 | fi: 2 | SilverStripe\CMS\Controllers\CMSMain: 3 | AddNew: 'Lisää uusi sivu' 4 | ListFiltered: 'Suodatettu lista.' 5 | TreeFilteredClear: 'Puhdista lista' 6 | UncleCheese\Dashboard\Dashboard_Content: 7 | ADMINISTRATION: Ylläpito 8 | UncleCheese\Dashboard\DashboardModelAdmin: 9 | COUNT: 'Näytettävien tietueiden määrä' 10 | UncleCheese\Dashboard\Dashboard: 11 | QUICKLINKSDESCRIPTION: 'Antaa ylläpidon hallita satunnaisia linkkejä etusivulta' 12 | SECTIONEDITORDESCRIPTION: 'Vetää sivuja webbisivun alueelta katselua ja luontia varten' 13 | ACCESS: 'Käyttöoikeus osioon ''{title}''' 14 | ACCESS_HELP: 'Oikeuta käyttäjä poistamaan paneeleita hänen etusivultaan' 15 | ACUSTOMPATH: 'Filter by a specific path' 16 | ADD: Lisää 17 | ADDPANEL: 'Uusi paneeli' 18 | ADDPANELS: 'Lisää etusivun paneeleita' 19 | APPLYTOALL: 'Aseta tämä etusivu kaikille käyttäjille' 20 | APPLYTOALLSUCCESS: 'Onnittelut! Tämä etusivun muokkaus on hyväksytty kaikille käyttäjille.' 21 | CANCEL: Peruuta 22 | CELCIUS: Celcius 23 | CHOOSEPANELTYPE: 'Valitse paneelin tyyppi' 24 | CONFIGUREANELS: 'Muokkaa etusivun paneeleita' 25 | CREATENEW: 'Luo uusi %s' 26 | DATEFORMAT: 'Päivä formaatti' 27 | DATERANGE: 'Päivämäärä väli' 28 | DAY: Päivä 29 | DELETEPANELS: 'Poista etusivun paneelit' 30 | ENTIRESITE: 'Koko sivusto' 31 | FARENHEIT: Fahrenheit 32 | FILTERBYPAGE: Suodatin 33 | GAACCOUNTEMAIL: 'Google tilin sähköpostiosoite' 34 | GAACCOUNTPASSWORD: 'Google tilin salasana' 35 | GAACCOUNTPROFILE: 'Profiilin ID (löytyy "Profilin asetukset" kohdasta Google Analytic:stä)' 36 | GOOGLEDESCRIPTION: 'Näyttää Google Analyticsin kaavion määrätyllä sivulla' 37 | GRIDFIELDPANELTITLE: 'GridField-editori' 38 | GRIDFIELDPANELDESCRIPTION: 'Lisää yhteenvetonäkymän GridFieldiin annetulle sivulle' 39 | LOCATION: Sijainti 40 | MENUTITLE: Etusivu 41 | MODELADMINCLASS: 'ModelAdmin välilehti' 42 | MODELADMINMODEL: Model 43 | MODELADMINPANELDESCRIPTION: 'Lisää yhteenvetonäkymän ModelAdminiin CMS:stä' 44 | MODELADMINPANELTITLE: 'ModelAdmin-editori' 45 | PANELSIZE: '' 46 | NONESHOWALL: 'Ei suodatusta. Näytä analytiikka koko sivustolta' 47 | NORECORDS: 'Ei tuloksia' 48 | PAGEINLIST: 'Filter by a specific page in the tree' 49 | PAGEVIEWS: Näyttökertaa 50 | PATH: Polku 51 | PLEASESELECT: 'Valitse' 52 | PREVIOUSSEVENDAYS: '7 päivää' 53 | PREVIOUSTHIRTYDAYS: '30 päivää' 54 | PREVIOUSYEAR: '365 päivää' 55 | QUICKLINKSLABEL: 'Pika Linkit' 56 | RSSFEED: 'Syötteet' 57 | RSSFEEDDESCRIPTION: 'Lisää syötteen mistä tahansa julkisesta URL:sta' 58 | SAVE: Tallenna 59 | SECTIONEDTIORLABEL: 'Osioeditori' 60 | SELECTLOCATION: 'Valitse sijainti kenen sään haluat näyttää.' 61 | SETASDEFAULT: 'Tee tästä oletusetusivu' 62 | SETASDEFAULTSUCCESS: 'Onnittelut! Tämä etusivun muokkaus on asetettu oletukseksi kaikille uusille käyttäjille.' 63 | TITLE: Otsikko 64 | TODAY: Tänään 65 | TOMORROW: Huomenna 66 | UNITS: Yksiköt 67 | VIEWALL: 'Näytä kaikki %s' 68 | VIEWFORECAST: 'Näytä koko säätiedotus' 69 | WEATHER: Sää 70 | WEATHERDESCRIPTION: 'Näyttää sään annetussa sijainnissa.' 71 | WEATHERNORESPONSE: 'Sää palvelin ei vastaa. Kokeile muutaman minuutin kuluttua uudelleen.' 72 | UncleCheese\Dashboard\DashboardGoogleAnalyticsPanel: 73 | PLURALNAME: 'Dashboard Google Analytics Panels' 74 | SINGULARNAME: 'Dashboard Google Analytics Panel' 75 | UncleCheese\Dashboard\DashboardModelAdminPanel: 76 | PLURALNAME: 'ModelAdmins' 77 | SINGULARNAME: 'ModelAdmin' 78 | UncleCheese\Dashboard\DashboardPanel: 79 | PLURALNAME: 'Etusivun Paneelit' 80 | SINGULARNAME: 'Etusivun Paneeli' 81 | UncleCheese\Dashboard\DashboardPanelDataObject: 82 | PLURALNAME: 'Etusivu Paneelin Data Objektit' 83 | SINGULARNAME: 'Etusivu Paneelin Data Objekti' 84 | UncleCheese\Dashboard\DashboardQuickLink: 85 | LINK: 'Linkki (sisältäen alussa http://)' 86 | LINKTEXT: 'Linkin teksti' 87 | NEWWINDOW: 'Avaa linkki uuteen ikkunaan' 88 | PLURALNAME: 'Etusivun Pika Linkit' 89 | SINGULARNAME: 'Etusivun Pika Linkki' 90 | UncleCheese\Dashboard\DashboardQuickLinksPanel: 91 | PLURALNAME: 'Etusivun Pika Linkki Paneelit' 92 | SINGULARNAME: 'Etusivun Pika Linkki Paneeli' 93 | UncleCheese\Dashboard\DashboardRSSFeedPanel: 94 | PLURALNAME: 'Etusivun Syötteen Paneelit' 95 | SINGULARNAME: 'Etusivun Syötteen Paneeli' 96 | UncleCheese\Dashboard\DashboardRecentEdits: 97 | COUNT: 'Näytettävä sivumäärä' 98 | UncleCheese\Dashboard\DashboardRecentEditsPanel: 99 | PLURALNAME: 'Etusivun Viimeaikaiset muutokset Paneelit' 100 | SINGULARNAME: 'Etusivun Viimeaikaiset muutokset Paneeli' 101 | UncleCheese\Dashboard\DashboardRecentFile: 102 | COUNT: 'Näytettävät tiedostot' 103 | UncleCheese\Dashboard\DashboardRecentFilesPanel: 104 | PLURALNAME: 'Etusivun Viimeaikaiset Tiedostot Paneelit' 105 | SINGULARNAME: 'Etusivun Viimeaikaiset Tiedostot Paneeli' 106 | UncleCheese\Dashboard\DashboardSectionEditorPanel: 107 | PLURALNAME: 'Etusivun Editori Paneelit' 108 | SINGULARNAME: 'Etusivun Editori Paneeli' 109 | UncleCheese\Dashboard\DashboardWeatherPanel: 110 | PLURALNAME: 'Etusivun Sää Paneelit' 111 | SINGULARNAME: 'Etusivun Sää Paneeli' 112 | SilverStripe\Security\Permission: 113 | CMS_ACCESS_CATEGORY: 'CMS Access' 114 | RecentEdits: 115 | DESCRIPTION: 'Näyttää linkitetyn listan äskettäin editoiduista sivuista' 116 | LABEL: 'Tuoreet muutokset' 117 | RecentFiles: 118 | DESCRIPTION: 'Näyttää linkitetyn listan äskettäin editoiduista tiedostoista' 119 | LABEL: 'Tuoreet tiedostot' 120 | -------------------------------------------------------------------------------- /lang/nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | SilverStripe\CMS\Controllers\CMSMain: 3 | AddNew: 'Nieuwe pagina' 4 | ListFiltered: 'Gefilterde lijst' 5 | TreeFilteredClear: 'Filter verwijderen' 6 | UncleCheese\Dashboard\Dashboard_Content: 7 | ADMINISTRATION: Administratie 8 | UncleCheese\Dashboard\DashboardModelAdmin: 9 | COUNT: 'Aantal items om te tonen' 10 | UncleCheese\Dashboard\Dashboard: 11 | QUICKLINKSDESCRIPTION: 'Sta management van willekeurige links op het dashboard toe' 12 | SECTIONEDITORDESCRIPTION: 'Haalt pagina''s van een deel van de website voor bekijken en aanmaken' 13 | ACCESS: 'Toegang tot ''{title}'' onderdeel' 14 | ACCESS_HELP: 'Laat gebruikers panelen verwijderen van zijn/haar dashboard' 15 | ACUSTOMPATH: 'Filter bij specifiek pad' 16 | ADD: Toevoegen 17 | ADDPANEL: 'Nieuw Paneel' 18 | ADDPANELS: 'Nieuwe dashboardpanelen toevoegen' 19 | APPLYTOALL: 'Stel dit dashboard in voor alle gebruikers' 20 | APPLYTOALLSUCCESS: 'Succes! Deze dashboard configuratie is toegepast op alle gebruikers met dashboard toegang.' 21 | CANCEL: Annuleren 22 | CELCIUS: Celcius 23 | CHOOSEPANELTYPE: 'Kies een paneeltype' 24 | CONFIGUREANELS: 'Configureer dashboard panelen' 25 | CREATENEW: 'Creeer nieuw %s' 26 | DATEFORMAT: 'Datum notatie' 27 | DATERANGE: 'Datum bereik' 28 | DAY: Dag 29 | DELETEPANELS: 'Verwijder dashboardpanelen' 30 | ENTIRESITE: 'Gehele site' 31 | FARENHEIT: Fahrenheit 32 | FILTERBYPAGE: Filter 33 | GAACCOUNTEMAIL: 'Google account e-mailadres' 34 | GAACCOUNTPASSWORD: 'Google account wachtwoord' 35 | GAACCOUNTPROFILE: 'Profiel ID (te vinden in "Profiel instellingen" van Google Analytics)' 36 | GOOGLEDESCRIPTION: 'Toon Google Analytics grafiek voor gekozen pagina' 37 | GRIDFIELDPANELTITLE: 'Grid Field Editor' 38 | GRIDFIELDPANELDESCRIPTION: 'Voegt een samenvatting van een GridField onderdeel van een gekozen pagina' 39 | LOCATION: Locatie 40 | MENUTITLE: Dashboard 41 | MODELADMINCLASS: 'Model admin tab' 42 | MODELADMINMODEL: Model 43 | MODELADMINPANELDESCRIPTION: 'Voegt een overzicht van een Model Admin deel van het CMS' 44 | MODELADMINPANELTITLE: 'Model Admin Editor' 45 | PANELSIZE: '' 46 | NONESHOWALL: 'Geen filter. Toon analytics voor de gehele site' 47 | NORECORDS: 'Geen items' 48 | PAGEINLIST: 'Filter op een specifieke pagina in de sitetree' 49 | PAGEVIEWS: Paginaviews 50 | PATH: Pad 51 | PLEASESELECT: 'Maak een keuze' 52 | PREVIOUSSEVENDAYS: '7 dagen' 53 | PREVIOUSTHIRTYDAYS: '30 dagen' 54 | PREVIOUSYEAR: '365 dagen' 55 | QUICKLINKSLABEL: 'Snellinks' 56 | RSSFEED: 'RSS Feed' 57 | RSSFEEDDESCRIPTION: 'Voegt een RSS feed van een publieke URL toe' 58 | SAVE: Opslaan 59 | SECTIONEDTIORLABEL: 'Sectie Editor' 60 | SELECTLOCATION: 'Kies een locatie waar je het weerbericht van wilt tonen.' 61 | SETASDEFAULT: 'Maak dit het standaard dashboard' 62 | SETASDEFAULTSUCCESS: 'Succes! Deze dashboard configuratie is nu standaard voor alle gebruikers met dashboard toegang.' 63 | TITLE: Titel 64 | TODAY: Vandaag 65 | TOMORROW: Morgen 66 | UNITS: Units 67 | VIEWALL: 'Bekijk alle %s' 68 | VIEWFORECAST: 'Bekijk gehele verwachting' 69 | WEATHER: Weer 70 | WEATHERDESCRIPTION: 'Toont het weer van de gekozen locatie.' 71 | WEATHERNORESPONSE: 'Weerserver reageerde niet. Probeer nogmaals over een paar minuten.' 72 | UncleCheese\Dashboard\DashboardGoogleAnalyticsPanel: 73 | PLURALNAME: 'Dashboard Google Analytics Paneelen' 74 | SINGULARNAME: 'Dashboard Google Analytics Paneel' 75 | UncleCheese\Dashboard\DashboardModelAdminPanel: 76 | PLURALNAME: 'Model Admins' 77 | SINGULARNAME: 'Model Admin' 78 | UncleCheese\Dashboard\DashboardPanel: 79 | PLURALNAME: 'Dashboardpanelen' 80 | SINGULARNAME: 'Dashboardpaneel' 81 | UncleCheese\Dashboard\DashboardPanelDataObject: 82 | PLURALNAME: 'Dashboard Paneel Data Objects' 83 | SINGULARNAME: 'Dashboard Paneel Data Object' 84 | UncleCheese\Dashboard\DashboardQuickLink: 85 | LINK: 'Link (inclusief http://)' 86 | LINKTEXT: 'Link tekst' 87 | NEWWINDOW: 'Open link in nieuw scherm' 88 | PLURALNAME: 'Dashboard snellinks' 89 | SINGULARNAME: 'Dashboard snellink' 90 | UncleCheese\Dashboard\DashboardQuickLinksPanel: 91 | PLURALNAME: 'Dashboard snellinks Paneelen' 92 | SINGULARNAME: 'Dashboard snellinks Paneel' 93 | UncleCheese\Dashboard\DashboardRSSFeedPanel: 94 | PLURALNAME: 'Dashboard R S S Feed Paneelen' 95 | SINGULARNAME: 'Dashboard R S S Feed Paneel' 96 | UncleCheese\Dashboard\DashboardRecentEdits: 97 | COUNT: 'Aantal pagina''s om te tonen' 98 | UncleCheese\Dashboard\DashboardRecentEditsPanel: 99 | PLURALNAME: 'Dashboard Recente Bewerkingen Paneelen' 100 | SINGULARNAME: 'Dashboard Recente Bewerkingen Paneel' 101 | UncleCheese\Dashboard\DashboardRecentFile: 102 | COUNT: 'Aantal bestanden om te tonen' 103 | UncleCheese\Dashboard\DashboardRecentFilesPanel: 104 | PLURALNAME: 'Dashboard Recente Bestanden Paneelen' 105 | SINGULARNAME: 'Dashboard Recent Bestanden Paneel' 106 | UncleCheese\Dashboard\DashboardSectionEditorPanel: 107 | PLURALNAME: 'Dashboard Sectiebewerker Paneelen' 108 | SINGULARNAME: 'Dashboard Sectiebewerker Paneel' 109 | UncleCheese\Dashboard\DashboardWeatherPanel: 110 | PLURALNAME: 'Dashboard Weer Paneelen' 111 | SINGULARNAME: 'Dashboard Weer Paneel' 112 | SilverStripe\Security\Permission: 113 | CMS_ACCESS_CATEGORY: 'CMS Toegang' 114 | RecentEdits: 115 | DESCRIPTION: 'Toon een gelinkte lijst van recent bewerkte pagina''s' 116 | LABEL: 'Recente bewerkingen' 117 | RecentFiles: 118 | DESCRIPTION: 'Toon een gelinkte lijst van recent bewerkte bestanden' 119 | LABEL: 'Recente bestanden' -------------------------------------------------------------------------------- /lang/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | SilverStripe\CMS\Controllers\CMSMain: 3 | AddNew: 'Neue Seite hinzufügen' 4 | ListFiltered: 'Gefilterte Liste.' 5 | TreeFilteredClear: 'Filter leeren' 6 | UncleCheese\Dashboard\Dashboard_Content: 7 | ADMINISTRATION: Administration 8 | UncleCheese\Dashboard\DashboardModelAdmin: 9 | COUNT: 'Anzahl der Objekte zur Anzeige' 10 | UncleCheese\Dashboard\Dashboard: 11 | QUICKLINKSDESCRIPTION: 'Ermöglicht die Verwaltung von beliebigen Links aus dem Dashboard heraus' 12 | SECTIONEDITORDESCRIPTION: 'Holt Seiten aus einem Bereich der Website zur Anzeige und Erstellung' 13 | ACCESS: 'Zugriff zum ''{title}'' Bereich' 14 | ACCESS_HELP: 'Erlaubt Benutzern das Entfernen von Feldern aus dem Dashboard' 15 | ACUSTOMPATH: 'Nach einem bestimmten Pfad filtern' 16 | ADD: Hinzufügen 17 | ADDPANEL: 'Neues Feld' 18 | ADDPANELS: 'Dashboard Felder hinzufügen' 19 | APPLYTOALL: 'Dieses Dashboard für alle Mitglieder einrichten' 20 | APPLYTOALLSUCCESS: 'Erfolg! Die Dashboard Konfiguration wurde für alle Mitglieder mit Dashboard-Zugriff eingerichtet.' 21 | CANCEL: Abbrechen 22 | CELCIUS: Celsius 23 | CHOOSEPANELTYPE: 'Wähle einen Feld-Typ' 24 | CONFIGUREANELS: 'Dashboard Felder konfigurieren' 25 | CREATENEW: 'Neues %s erstellen' 26 | DATEFORMAT: 'Datumsformat' 27 | DATERANGE: 'Datumsbereich' 28 | DAY: Tag 29 | DELETEPANELS: 'Dashboard Felder entfernen' 30 | ENTIRESITE: 'Gesamte Seite' 31 | FARENHEIT: Fahrenheit 32 | FILTERBYPAGE: Filter 33 | GAACCOUNTEMAIL: 'Google Konto E-Mail' 34 | GAACCOUNTPASSWORD: 'Google Konto Passwort' 35 | GAACCOUNTPROFILE: 'Profil ID (zu finden unter den "Profil Einstellungen" von Google Analytics)' 36 | GOOGLEDESCRIPTION: 'Zeigt ein Google Analytics Diagramm für eine bestimmte Seite' 37 | GRIDFIELDPANELTITLE: 'Grid Field Editor' 38 | GRIDFIELDPANELDESCRIPTION: 'Übersicht der GridField Objekte einer Seite' 39 | LOCATION: Ort 40 | MENUTITLE: Dashboard 41 | MODELADMINCLASS: 'Model Admin Tab' 42 | MODELADMINMODEL: Model 43 | MODELADMINPANELDESCRIPTION: 'Übersicht aller Model-Admin Objekte' 44 | MODELADMINPANELTITLE: 'Model Admin Editor' 45 | PANELSIZE: '' 46 | NONESHOWALL: 'Nicht filtern. Zeige Analytics für die gesamte Seite' 47 | NORECORDS: 'Keine Datensätze' 48 | PAGEINLIST: 'Nach bestimmter Seite im Seitenbaum filtern' 49 | PAGEVIEWS: Seitenzugriffe 50 | PATH: Pfad 51 | PLEASESELECT: 'Bitte auswählen' 52 | PREVIOUSSEVENDAYS: '7 Tage' 53 | PREVIOUSTHIRTYDAYS: '30 Tage' 54 | PREVIOUSYEAR: '365 Tage' 55 | QUICKLINKSLABEL: 'Quick Links' 56 | RSSFEED: 'RSS Feed' 57 | RSSFEEDDESCRIPTION: 'RSS feed einer öffentlichen URL hinzufügen' 58 | SAVE: Speichern 59 | SECTIONEDTIORLABEL: 'Bereichs Editor' 60 | SELECTLOCATION: 'Bitte einen Ort zur Wetterdarstellung auswählen.' 61 | SETASDEFAULT: 'Zum Standard-Dashboard machen' 62 | SETASDEFAULTSUCCESS: 'Erfolg! Diese Dashboard Konfiguration wurde als Standard für alle neuen Mitglieder festgelegt.' 63 | TITLE: Titel 64 | TODAY: Heute 65 | TOMORROW: Morgen 66 | UNITS: Einheiten 67 | VIEWALL: 'Alle %s ansehen' 68 | VIEWFORECAST: 'Gesamte Vorhersage ansehen' 69 | WEATHER: Wetter 70 | WEATHERDESCRIPTION: 'Zeigt das Wetter für den angegebenen Ort.' 71 | WEATHERNORESPONSE: 'Der Wetter-Server antwortet nicht. Versuchen Sie es in wenigen Minuten erneut.' 72 | UncleCheese\Dashboard\DashboardGoogleAnalyticsPanel: 73 | PLURALNAME: 'Dashboard Google Analytics Felder' 74 | SINGULARNAME: 'Dashboard Google Analytics Feld' 75 | UncleCheese\Dashboard\DashboardModelAdminPanel: 76 | PLURALNAME: 'Model Admins' 77 | SINGULARNAME: 'Model Admin' 78 | UncleCheese\Dashboard\DashboardPanel: 79 | PLURALNAME: 'Dashboard Felder' 80 | SINGULARNAME: 'Dashboard Feld' 81 | UncleCheese\Dashboard\DashboardPanelDataObject: 82 | PLURALNAME: 'Dashboard Felder Data Objects' 83 | SINGULARNAME: 'Dashboard Feld Data Object' 84 | UncleCheese\Dashboard\DashboardQuickLink: 85 | LINK: 'Link (mit http://)' 86 | LINKTEXT: 'Link text' 87 | NEWWINDOW: 'Öffnet link in einem neuen Fenster' 88 | PLURALNAME: 'Dashboard Quick Links' 89 | SINGULARNAME: 'Dashboard Quick Link' 90 | UncleCheese\Dashboard\DashboardQuickLinksPanel: 91 | PLURALNAME: 'Dashboard Quick Links Felder' 92 | SINGULARNAME: 'Dashboard Quick Links Feld' 93 | UncleCheese\Dashboard\DashboardRSSFeedPanel: 94 | PLURALNAME: 'Dashboard R S S Feed Felder' 95 | SINGULARNAME: 'Dashboard R S S Feed Feld' 96 | UncleCheese\Dashboard\DashboardRecentEdits: 97 | COUNT: 'Anzahl der Seiten zur Anzeige' 98 | UncleCheese\Dashboard\DashboardRecentEditsPanel: 99 | PLURALNAME: 'Dashboard Zuletzt Editiert Felder' 100 | SINGULARNAME: 'Dashboard Zuletzt Editiert Felder' 101 | UncleCheese\Dashboard\DashboardRecentFile: 102 | COUNT: 'Anzahl der Dateien zur Anzeige' 103 | UncleCheese\Dashboard\DashboardRecentFilesPanel: 104 | PLURALNAME: 'Dashboard Letzte Dateien Felder' 105 | SINGULARNAME: 'Dashboard Letzte Dateien Felde' 106 | UncleCheese\Dashboard\DashboardSectionEditorPanel: 107 | PLURALNAME: 'Dashboard Bereichs-Editor Felder' 108 | SINGULARNAME: 'Dashboard Bereichs-Editor Felde' 109 | UncleCheese\Dashboard\DashboardWeatherPanel: 110 | PLURALNAME: 'Dashboard Wetter Felder' 111 | SINGULARNAME: 'Dashboard Wetter Feld' 112 | SilverStripe\Security\Permission: 113 | CMS_ACCESS_CATEGORY: 'CMS Zugriff' 114 | RecentEdits: 115 | DESCRIPTION: 'Zeigt eine verlinkte Liste der zuletzt bearbeiteten Seiten' 116 | LABEL: 'Zuletzt bearbeitet' 117 | RecentFiles: 118 | DESCRIPTION: 'Zeigt eine verlinkte Liste der zuletzt bearbeiteten Dateien' 119 | LABEL: 'Letze Dateien' 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Dashboard Module for SilverStripe 4 2 | 3 | The Dashboard module provides a splash page for the CMS in SilverStripe 4 with configurable widgets that display relevant information. Panels can be created and extended easily. The goal of the Dashboard module is to provide users with a launchpad for common CMS actions such as creating specific page types or browsing new content. 4 | 5 | 6 | ## Screenshot & Videos 7 | Images and videos about this module can be found [in this blog post.](https://www.silverstripe.org/blog/the-dashboard-module-make-a-splash-in-silverstripe-3/) 8 | 9 | 10 | ## Included panels 11 | **No included panels at the moment. These could be upgraded and brought back from the SS3 version of this module:** 12 | * Recently edited pages 13 | * Recently uploaded files 14 | * RSS Feed 15 | * Quick links 16 | * Section editor 17 | * Google Analytics 18 | * Weather 19 | 20 | ## Installation 21 | 22 | * Install the contents of this repository in the root of your SilverStripe project in a directory named "dashboard". 23 | * Run /dev/build?flush=1 24 | 25 | 26 | ## Creating a Custom Dashboard Panel 27 | 28 | Dashboard panels have their own MVC architecture and are easy to create. In this example, we'll create a panel that displays recent orders for an imaginary website. The user will have the option to configure the panel to only show orders that are shipped. 29 | 30 | ### Creating the model 31 | 32 | First, create a class for the panel as a descendant of DashboardPanel. We'll include the database fields that define the configurable properties, and create the configuration fields in the getConfiguration() method. 33 | 34 | 35 | **mysite/code/RecentOrders.php** 36 | ```php 37 | 'Int', 46 | 'OnlyShowShipped' => 'Boolean' 47 | ]; 48 | 49 | 50 | private static $icon = "mysite/images/dashboard-recent-orders.png"; 51 | 52 | 53 | public function getLabel() { 54 | return _t('Mysite.RECENTORDERS','Recent Orders'); 55 | } 56 | 57 | 58 | public function getDescription() { 59 | return _t('Mysite.RECENTORDERSDESCRIPTION','Shows recent orders for this fake website.'); 60 | } 61 | 62 | 63 | public function getConfiguration() { 64 | $fields = parent::getConfiguration(); 65 | $fields->push(TextField::create("Count", "Number of orders to show")); 66 | $fields->push(CheckboxField::create("OnlyShowShipped","Only show shipped orders")); 67 | return $fields; 68 | } 69 | 70 | 71 | 72 | public function Orders() { 73 | $orders = Order::get()->sort("Created DESC")->limit($this->Count); 74 | return $this->OnlyShowShipped ? $orders->filter(['Shipped' => true]) : $orders; 75 | } 76 | } 77 | 78 | ``` 79 | 80 | ### Creating the Template 81 | 82 | The panel object will look for a template that matches its class name. 83 | 84 | **mysite/templates/Includes/DashboardRecentOrdersPanel.ss** 85 | ```html 86 |
    87 | 92 |
    93 | ``` 94 | 95 | Run /dev/build?flush=1, and you can now create this dashboard panel in the CMS. 96 | 97 | ### Customizing with CSS 98 | 99 | The best place to inject CSS and JavaScript requirements is in the inherited PanelHolder() method of the DashboardPanel subclass. 100 | 101 | **mysite/code/DashboardRecentOrdersPanel.php** 102 | ```php 103 | nextRecord()) { 125 | $chart->addData($row['Date'], $row['OrderCount']); 126 | } 127 | } 128 | return $chart; 129 | } 130 | 131 | ``` 132 | 133 | **mysite/code/DashboardRecentOrdersPanel.ss** 134 | ```html 135 | $Chart 136 | ``` 137 | 138 | ### Custom templates for ModelAdmin / GridField panels 139 | 140 | You can create your own templates for either of these panel types which will override the default templates. Due to the naming structure the custom templates will be specific to that partiular panel, thus you can have a seperate template for each ModelAdmin / GridField panel. 141 | 142 | You can access all the properties of your model in the template as normal along with a EditLink method which will contain the CMS edit link for that item. 143 | 144 | 145 | For model admin panels, create a templated called DashboardModelAdminPanel\_**ModelAdminClass**\_**ModelAdminModel**.ss and place it in your _mysite/templates/Includes folder_. 146 | eg; 147 | **DashboardModelAdminPanel\_MyAdmin\_Product.ss** 148 | 149 | A gridfield panel uses a similar convention, DashboardGridFieldPanel\_**PageClassName**\_**GridFieldName**.ss 150 | 151 | eg; 152 | **DashboardGridFieldPanel\_ContactPage\_Submissions.ss** 153 | 154 | ## Note on Google Analytics Panel 155 | 156 | You need to add your Google Analytics config information to the project config.yml: 157 | ```yaml 158 | DashboardGoogleAnalyticsPanel: 159 | email: [XXXXX]@developer.gserviceaccount.com 160 | profile: 123456 161 | key_file_path: google_oauth.p12 162 | ``` 163 | To locate your profile ID, visit the Google Analytics website, login and select the website. At the end of the URL will be fragment similar to this: 164 | ``` 165 | #report/visitors-overview/a5559982w55599512p12345678 166 | /a[6 digits]w[8 digits]p[8 digits] 167 | ``` 168 | The 8 digits that follow the "p" are your profile ID. In the example above, this would be 12345678. 169 | 170 | NOTE: To use the Google Analytics panel, you have to enable access for less secure apps in the account permissions section of [https://www.google.com/settings/security](https://www.google.com/settings/security). 171 | 172 | For more information about settting up a developer account and obtaining a key file, visit https://github.com/erebusnz/gapi-google-analytics-php-interface#instructions-for-setting-up-a-google-service-account-for-use-with-gapi 173 | -------------------------------------------------------------------------------- /src/panels/DashboardPanel.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class DashboardPanel extends DataObject { 23 | 24 | private static $table_name = 'DashboardPanel'; 25 | 26 | private static $db = [ 27 | 'Title' => 'Varchar(50)', 28 | 'PanelSize' => "Enum(array('small','normal','large'),'normal')", 29 | 'SortOrder' => 'Int' 30 | ]; 31 | 32 | 33 | 34 | private static $has_one = [ 35 | 'Member' => 'SilverStripe\Security\Member', 36 | 'SiteConfig' => 'SilverStripe\SiteConfig\SiteConfig' 37 | ]; 38 | 39 | 40 | 41 | private static $default_sort = "SortOrder ASC"; 42 | 43 | 44 | /** 45 | * @var string The size of the dashboard panel. Options: "small", "normal", and "large" 46 | */ 47 | private static $size = "normal"; 48 | 49 | 50 | 51 | /** 52 | * @var string The path to the icon image that represents this dashboard panel type 53 | */ 54 | private static $icon = "unclecheese/dashboard:images/dashboard-panel-default.png"; 55 | 56 | 57 | 58 | 59 | /** 60 | * @var int The "weight" of the dashboard panel when listed in the available panels. 61 | * Higher is lower in the list. 62 | */ 63 | private static $priority = 100; 64 | 65 | 66 | 67 | 68 | /** 69 | * @var bool Show the configure form after creating. Used for panels that require 70 | * configuration in order to show data 71 | */ 72 | private static $configure_on_create = false; 73 | 74 | 75 | 76 | 77 | /** 78 | * @var string The name of the template used for the contents of this panel. 79 | */ 80 | protected $template; 81 | 82 | 83 | 84 | /** 85 | * @var string the name of the template used for the wrapper of this panel 86 | */ 87 | protected $holderTemplate = "UncleCheese\Dashboard\DashboardPanel"; 88 | 89 | 90 | 91 | 92 | /** 93 | * @var string The name of the request handler class that the Dashboard controller 94 | * will use to communicate with a given panel 95 | */ 96 | protected $requestHandlerClass = DashboardPanelRequest::class; 97 | 98 | 99 | /** 100 | * Allows the panel to be added 101 | * 102 | * @return string 103 | */ 104 | public function registered() { 105 | if (is_bool(self::config()->enabled)) { 106 | return self::config()->enabled; 107 | } 108 | return true; 109 | } 110 | 111 | 112 | 113 | /** 114 | * Gets the template, falls back on a default value of the class name 115 | * 116 | * @return string 117 | */ 118 | protected function getTemplate() { 119 | return $this->template ? $this->template : static::class; 120 | } 121 | 122 | 123 | 124 | /** 125 | * Gets the holder template 126 | * 127 | * @return string 128 | */ 129 | public function getHolderTemplate() { 130 | return $this->holderTemplate; 131 | } 132 | 133 | 134 | 135 | 136 | /** 137 | * Gets the request handler class 138 | * 139 | * @return string 140 | */ 141 | public function getRequestHandlerClass() { 142 | return $this->requestHandlerClass; 143 | } 144 | 145 | 146 | 147 | /** 148 | * Essentially an abstract method. Every panel must have this method defined to provide 149 | * a title to the panel selection window 150 | * 151 | * @return string 152 | */ 153 | public function getLabel() { 154 | 155 | } 156 | 157 | 158 | 159 | /** 160 | * Essentially an abstract method. Every panel must have this method defined to provide 161 | * a description to the panel selection window 162 | * 163 | * @return string 164 | */ 165 | public function getDescription() { 166 | 167 | } 168 | 169 | 170 | 171 | /** 172 | * An accessor to the Dashboard controller 173 | * 174 | * @return Dashboard 175 | */ 176 | public function getDashboard() { 177 | return Injector::inst()->get(Dashboard::class); 178 | } 179 | 180 | 181 | 182 | /** 183 | * Renders the panel to its template 184 | * 185 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 186 | */ 187 | public function render() { 188 | return $this->renderWith($this->holderTemplate); 189 | } 190 | 191 | 192 | 193 | /** 194 | * A template accessor for the icon of this panel 195 | * 196 | * @return string 197 | */ 198 | public function Icon() { 199 | return ModuleResourceLoader::resourceURL(static::config()->icon); 200 | } 201 | 202 | 203 | 204 | /** 205 | * Renders the inner contents of the panel. Similar to $Layout in pages. 206 | * 207 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 208 | */ 209 | public function Content() { 210 | return $this->renderWith($this->getTemplate()); 211 | } 212 | 213 | 214 | 215 | 216 | /** 217 | * The link to this panel through the Dashboard controller 218 | * 219 | * @return string 220 | */ 221 | public function Link($action = null) { 222 | return Controller::join_links($this->getDashboard()->Link("panel/{$this->ID}"),$action); 223 | } 224 | 225 | 226 | 227 | 228 | /** 229 | * The link to delete this panel from the dashboard 230 | * 231 | * @return string 232 | */ 233 | public function DeleteLink() { 234 | return $this->Link("delete"); 235 | } 236 | 237 | 238 | 239 | 240 | /** 241 | * The link to create this panel on the dashboard 242 | * 243 | * @return string 244 | */ 245 | public function CreateLink() { 246 | return Controller::join_links($this->getDashboard()->Link("panel/new"), "?type=" . static::class); //TODO: Should the class name be escaped? At least Convert::raw2url() is not suitable because it removes backslashes completely, not escaping them. 247 | } 248 | 249 | 250 | 251 | 252 | /** 253 | * Template accessor for the $configure_on_create boolean 254 | * 255 | * @return boolean 256 | */ 257 | public function ShowConfigure() { 258 | return $this->config()->get('configure_on_create'); 259 | } 260 | 261 | 262 | 263 | 264 | /** 265 | * Gets the {@link FieldList} object that is used to configure the fields on this panel. 266 | * Similar to getCMSFields(). 267 | * 268 | * @return FieldList 269 | */ 270 | public function getConfiguration() { 271 | $default_size_title = ' '; //Cannot be an empty string because SilverStripe\i18n\i18n::_t() would yell that a default should be defined. So use a space as a workaround. 272 | return FieldList::create( 273 | DashboardButtonOptionsField::create("PanelSize",_t('UncleCheese\Dashboard\Dashboard.PANELSIZE', $default_size_title), [ 274 | 'small' => '', 275 | 'normal' => '', 276 | 'large' => '' 277 | ])->setSize("small"), 278 | 279 | TextField::create("Title", _t('Dashboard.TITLE','Title')) 280 | ); 281 | } 282 | 283 | 284 | 285 | /** 286 | * Gets the primary actions, which may appear in the top of the panel 287 | * 288 | * @return ArrayList 289 | */ 290 | public function getPrimaryActions() { 291 | return ArrayList::create([]); 292 | } 293 | 294 | 295 | 296 | 297 | /** 298 | * Gets the secondary actions, which may appear in the bottom of the panel 299 | * 300 | * @return ArrayList 301 | */ 302 | public function getSecondaryActions() { 303 | return ArrayList::create([]); 304 | } 305 | 306 | 307 | 308 | /** 309 | * Renders the entire panel. Similar to {@link FormField::FieldHolder()} 310 | * 311 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 312 | */ 313 | public function PanelHolder() { 314 | return $this->renderWith($this->holderTemplate); 315 | } 316 | 317 | 318 | 319 | /** 320 | * For backward compatibility to the old static $size property. 321 | * 322 | * @return string 323 | */ 324 | public function Size() { 325 | return $this->PanelSize; 326 | } 327 | 328 | 329 | 330 | /** 331 | * Gets the configuration form for this panel 332 | * 333 | * @return Form 334 | */ 335 | public function Form() { 336 | return DashboardPanelRequest::create($this->getDashboard(), $this)->ConfigureForm(); 337 | } 338 | 339 | 340 | // /** 341 | // * Duplicates this panel. Drills down into the has_many relations 342 | // * 343 | // * We don't need this method anymore after upgrading to SS4. If any relations need to be duplicated, a DashboardPanel subclass should define a private static $cascade_duplicates config variable which should contain the relation names that should be duplicated. 344 | // * 345 | // * @return DashboardPanel 346 | // */ 347 | // public function duplicate($dowrite = true) { 348 | // $clone = parent::duplicate(true); 349 | // foreach($this->has_many() as $relationName => $relationClass) { 350 | // foreach($this->$relationName() as $relObject) { 351 | // $relClone = $relObject->duplicate(false); 352 | // $relClone->DashboardPanelID = $clone->ID; 353 | // $relClone->write(); 354 | // } 355 | // } 356 | // return $clone; 357 | // } 358 | 359 | 360 | 361 | 362 | public function canCreate($member = null, $context = []) { 363 | return Permission::check("CMS_ACCESS_DashboardAddPanels"); 364 | } 365 | 366 | 367 | 368 | public function canDelete($member = null) { 369 | $m = $member ? $member : Security::getCurrentUser(); 370 | return Permission::check("CMS_ACCESS_DashboardDeletePanels") && $this->MemberID == $m->ID; 371 | } 372 | 373 | 374 | 375 | public function canEdit($member = null) { 376 | $m = $member ? $member : Security::getCurrentUser(); 377 | return Permission::check("CMS_ACCESS_DashboardConfigurePanels") && $this->MemberID == $m->ID; 378 | } 379 | 380 | 381 | 382 | public function canView($member = null) { 383 | $m = $member ? $member : Security::getCurrentUser(); 384 | return Permission::check("CMS_ACCESS_Dashboard") && $this->MemberID == $m->ID; 385 | } 386 | 387 | public function IsConfigured() { 388 | return true; 389 | } 390 | 391 | } -------------------------------------------------------------------------------- /src/DashboardHasManyRelationEditor.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class DashboardHasManyRelationEditor extends FormField { 29 | 30 | private static $allowed_actions = [ 31 | "handleItem" 32 | ]; 33 | 34 | private static $url_handlers = [ 35 | 'item/$ID' => 'handleItem', 36 | '$Action!' => '$Action', 37 | ]; 38 | 39 | 40 | 41 | /** 42 | * @var DashboardPanel The {@link DashboardPanel} that owns this editor 43 | */ 44 | protected $controller; 45 | 46 | 47 | 48 | /** 49 | * @var string The name of the relationship that is managed by this editor 50 | */ 51 | protected $relationName; 52 | 53 | 54 | 55 | /** 56 | * @var string The class of the related object 57 | */ 58 | protected $relationClass; 59 | 60 | 61 | 62 | /** 63 | * @var DataList The current list of records in the relation 64 | */ 65 | protected $records; 66 | 67 | 68 | 69 | /** 70 | * @var string The template that renders the editor 71 | */ 72 | protected $template = "UncleCheese\Dashboard\DashboardHasManyRelationEditor"; 73 | 74 | 75 | 76 | 77 | /** 78 | * The contructor for the editor. Sets member properties and checks for major errors. 79 | * 80 | * @param DashboardPanel The owner of the editor 81 | * @param string The name of the relation managed by the editor 82 | * @param string The class of the related object managed by the editor 83 | * @param string The title (label) of the editor 84 | */ 85 | public function __construct($controller, $relationName, $relationClass, $title = null) { 86 | $this->controller = $controller; 87 | $this->relationName = $relationName; 88 | $this->relationClass = $relationClass; 89 | $this->title = $title; 90 | 91 | if(!$this->controller instanceof DashboardPanel) { 92 | user_error("DashboardHasManyRelationEditor must be passed an instance of DashboardPanel", E_USER_ERROR); 93 | } 94 | if (!isset($this->controller->hasMany()[$this->relationName])) { 95 | user_error("DashboardHasManyRelationEditor must be passed a valid has_many relation for the panel. $relationName is not in the has_many array.", E_USER_ERROR); 96 | } 97 | if(!is_subclass_of($relationClass, DashboardPanelDataObject::class)) { 98 | user_error("DashbordHasManyRelationEditor can only manage subclasses of DashboardPanelDataObject", E_USER_ERROR); 99 | } 100 | 101 | $this->records = $this->controller->$relationName(); 102 | 103 | parent::__construct($relationName, $title); 104 | 105 | } 106 | 107 | 108 | 109 | 110 | /** 111 | * Sets the template of the editor 112 | * 113 | * @param string The name of the template 114 | */ 115 | public function setTemplate($template) { 116 | $this->template = $template; 117 | } 118 | 119 | 120 | 121 | /** 122 | * Gets all of the items in the relation and provides edit/delete links for the table 123 | * 124 | * @return ArrayList 125 | */ 126 | public function Items() { 127 | $items = ArrayList::create([]); 128 | $labelField = Config::inst()->get($this->relationClass, "label_field"); 129 | foreach($this->records as $record) { 130 | $items->push(ArrayData::create([ 131 | 'Label' => $record->$labelField, 132 | 'DeleteLink' => Controller::join_links($this->Link("item"),$record->ID,"delete"), 133 | 'EditLink' => $this->Link("item/{$record->ID}"), 134 | 'ID' => $record->ID 135 | ])); 136 | } 137 | return $items; 138 | } 139 | 140 | 141 | 142 | 143 | /** 144 | * Renders the form field 145 | * 146 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 147 | */ 148 | public function FieldHolder($attributes = []) { 149 | return $this->renderWith($this->template); 150 | } 151 | 152 | 153 | 154 | 155 | /** 156 | * Handles a request for a record in the table 157 | * 158 | * @param HTTPRequest 159 | * @return HTTPResponse 160 | * @throws \SilverStripe\Control\HTTPResponse_Exception 161 | */ 162 | public function handleItem(HTTPRequest $r) { 163 | if($r->param('ID') == "new") { 164 | $item = Injector::inst()->create($this->relationClass); 165 | } 166 | else { 167 | $item = DataList::create($this->relationClass)->byID((int) $r->param('ID')); 168 | } 169 | if($item) { 170 | $handler = DashboardHasManyRelationEditorItemRequest::create($this->controller->getDashboard(), $this->controller, $this, $item); 171 | return $handler->handleRequest($r); 172 | } 173 | return $this->httpError(404); 174 | } 175 | 176 | 177 | 178 | 179 | /** 180 | * A default controller action that renders the editor 181 | * 182 | * @param HTTPRequest 183 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 184 | */ 185 | public function index(HTTPRequest $r) { 186 | return $this->FieldHolder(); 187 | } 188 | 189 | 190 | /** 191 | * A controller action that handles the reordering of the list 192 | * 193 | * @param HTTPRequest 194 | * @return HTTPResponse 195 | * @throws \SilverStripe\ORM\ValidationException 196 | */ 197 | public function sort(HTTPRequest $r) { 198 | if($items = $r->getVar('item')) { 199 | foreach($items as $position => $id) { 200 | if($item = DataList::create($this->relationClass)->byID((int) $id)) { 201 | $item->SortOrder = $position; 202 | $item->write(); 203 | } 204 | } 205 | return new HTTPResponse("OK"); 206 | } 207 | } 208 | 209 | 210 | 211 | } 212 | 213 | 214 | 215 | /** 216 | * Defines the {@link RequestHandler} object that handles an item belonging to the editor 217 | * 218 | * @package Dashboard 219 | * @author Uncle Cheese 220 | */ 221 | class DashboardHasManyRelationEditorItemRequest extends RequestHandler { 222 | 223 | private static $allowed_actions = [ 224 | "edit", 225 | "delete", 226 | "DetailForm" 227 | ]; 228 | 229 | 230 | /** 231 | * @var Dashboard The Dashboard controller in the CMS 232 | */ 233 | protected $dashboard; 234 | 235 | 236 | 237 | /** 238 | * @var DashboardPanel The dashboard panel that owns the editor that is running the request 239 | */ 240 | protected $panel; 241 | 242 | 243 | 244 | /** 245 | * @var DashboardHasManyRelationEditor The editor that is running the request 246 | */ 247 | protected $editor; 248 | 249 | 250 | 251 | /** 252 | * @var DashboardPanelDataObject The object that was requested for edit/create/delete 253 | */ 254 | protected $item; 255 | 256 | 257 | 258 | 259 | private static $url_handlers = [ 260 | '$Action!' => '$Action', 261 | '' => 'edit' 262 | ]; 263 | 264 | 265 | 266 | 267 | public function __construct($dashboard, $panel, $editor, $item) { 268 | $this->dashboard = $dashboard; 269 | $this->panel = $panel; 270 | $this->editor = $editor; 271 | $this->item = $item; 272 | parent::__construct(); 273 | } 274 | 275 | 276 | 277 | 278 | /** 279 | * An action that handles the edit of an object managed by the editor 280 | * 281 | * @param HTTPRequest 282 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 283 | */ 284 | public function edit(HTTPRequest $r) { 285 | return $this->renderWith('DashboardHasManyRelationEditorDetailForm'); 286 | } 287 | 288 | 289 | 290 | 291 | /** 292 | * An action that handles the deletion of an object managed by the editor 293 | * 294 | * @param HTTPRequest 295 | * @return HTTPResponse 296 | */ 297 | public function delete(HTTPRequest $r) { 298 | $this->item->delete(); 299 | return new HTTPResponse("OK"); 300 | } 301 | 302 | 303 | 304 | 305 | /** 306 | * A link to this item as managed by the editor belonging to a dashboard panel 307 | * 308 | * @return string 309 | */ 310 | public function Link($action = null) { 311 | return Controller::join_links($this->editor->Link(),"item",$this->item->ID ? $this->item->ID : "new",$action); 312 | } 313 | 314 | 315 | 316 | /** 317 | * A link to refresh the editor 318 | * 319 | * @return string 320 | */ 321 | public function RefreshLink() { 322 | return $this->Link("edit"); 323 | } 324 | 325 | 326 | 327 | 328 | /** 329 | * Provides a form to edit or create an object managed by the editor 330 | * 331 | * @return Form 332 | */ 333 | public function DetailForm() { 334 | $form = Form::create( 335 | $this, 336 | "DetailForm", 337 | Injector::inst()->get($this->editor->relationClass)->getConfiguration(), 338 | FieldList::create( 339 | FormAction::create('saveDetail',_t('Dashboard.SAVE','Save')) 340 | ->setUseButtonTag(true) 341 | ->addExtraClass('ss-ui-action-constructive small'), 342 | FormAction::create('cancel',_t('Dashboard.CANCEL','Cancel')) 343 | ->setUseButtonTag(true) 344 | ->addExtraClass('small') 345 | ) 346 | ); 347 | $form->setHTMLID("Form_DetailForm_".$this->panel->ID."_".$this->item->ID); 348 | $form->loadDataFrom($this->item); 349 | $form->addExtraClass('dashboard-has-many-editor-detail-form-form'); 350 | return $form; 351 | } 352 | 353 | 354 | 355 | 356 | /** 357 | * Saves the DetailForm and writes or creates a new object managed by the editor 358 | * 359 | * @param array $data The raw POST data from the form 360 | * @param Form $form The DetailForm object 361 | * @return HTTPResponse 362 | * @throws \SilverStripe\ORM\ValidationException 363 | */ 364 | public function saveDetail($data, $form) { 365 | $item = $this->item; 366 | if(!$item->exists()) { 367 | $item->DashboardPanelID = $this->panel->ID; 368 | $sort = DataList::create($item->ClassName)->max("SortOrder"); 369 | $item->SortOrder = $sort+1; 370 | $item->write(); 371 | } 372 | $form->saveInto($item); 373 | $item->write(); 374 | return new HTTPResponse("OK"); 375 | } 376 | 377 | 378 | } -------------------------------------------------------------------------------- /javascript/dashboard.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | 4 | 5 | $('.dashboard-panel').entwine({ 6 | refresh: function() { 7 | var $t = this; 8 | this.addClass('loading'); 9 | $.ajax({ 10 | url: this.attr('data-refresh-url'), 11 | success: function(data) { 12 | $t.replaceWith(data); 13 | } 14 | }) 15 | }, 16 | 17 | showConfigure: function() { 18 | var $t = this; 19 | this.find('.dashboard-panel-inner').flip({ 20 | direction: "rl", 21 | content: $($t).find('.dashboard-panel-configure').html(), 22 | color: "#dfdfdf", 23 | onEnd: function() { 24 | if($t.hasClass("refreshable")) { 25 | $t.refresh(); 26 | } 27 | $t.find('.ui-button').each(function() { 28 | if($(this).find('.ui-button-text').length > 1) { 29 | var text = $(this).text(); 30 | $(this).html(""+text+""); 31 | } 32 | }); 33 | $t.find(".TreeDropdownField").each(function() { 34 | while($(this).find(".treedropdownfield-title").length > 1) { 35 | $(this).find(".treedropdownfield-title:last, .treedropdownfield-toggle-panel-link:last, .treedropdownfield-panel:last").remove() 36 | } 37 | }) 38 | 39 | } 40 | }); 41 | }, 42 | 43 | hideConfigure: function() { 44 | this.find('.dashboard-panel-inner').revertFlip(); 45 | }, 46 | 47 | 48 | }); 49 | 50 | 51 | 52 | $('.dashboard-panel *').entwine({ 53 | getPanel: function() { 54 | return this.parents(".dashboard-panel:first"); 55 | }, 56 | getConfigurationPanel: function() { 57 | return this.getPanel().find('.dashboard-panel-configure:first'); 58 | }, 59 | getConfigurationForm: function() { 60 | return this.getPanel().find('.configure-form:first'); 61 | }, 62 | getHasManyEditors: function() { 63 | return this.getPanel().find('.dashboard-has-many-editor'); 64 | }, 65 | getHasManyFormWrapper: function() { 66 | return this.getPanel().find('.dashboard-has-many-editor-form:first'); 67 | }, 68 | getHasManyForm: function() { 69 | return this.getPanel().find('.dashboard-has-many-editor-detail-form-form:first'); 70 | }, 71 | getConfigurationActions: function() { 72 | return this.getPanel().find('.dashboard-panel-configure-actions:first'); 73 | }, 74 | getPanelInner: function() { 75 | return this.getPanel().find('.dashboard-panel-inner:first'); 76 | } 77 | }) 78 | 79 | 80 | $('.ss-fancy-dropdown').entwine({ 81 | Open: false, 82 | toggle: function() { 83 | if(this.getOpen()) { 84 | this.obscure(); 85 | } 86 | else { 87 | this.reveal(); 88 | } 89 | }, 90 | reveal: function() { 91 | this.find('.ss-fancy-dropdown-options').css({'display':'block'}); 92 | this.setOpen(true); 93 | }, 94 | obscure: function () { 95 | this.find('.ss-fancy-dropdown-options').css({'display':'none'}) 96 | this.setOpen(false); 97 | } 98 | }); 99 | 100 | $('.ss-fancy-dropdown *').entwine({ 101 | getDropdown: function() { 102 | return this.closest(".ss-fancy-dropdown"); 103 | } 104 | 105 | }); 106 | 107 | $('.ss-fancy-dropdown-btn').entwine({ 108 | onclick: function(e) { 109 | e.preventDefault(); 110 | e.stopPropagation(); 111 | this.getDropdown().toggle(); 112 | }, 113 | 114 | }); 115 | 116 | $('.ss-fancy-dropdown-options a').entwine({ 117 | onclick: function(e) { 118 | this._super(e); 119 | this.getDropdown().obscure(); 120 | } 121 | }) 122 | 123 | $('body').entwine({ 124 | onclick: function() { 125 | this._super(); 126 | $('.ss-fancy-dropdown').obscure(); 127 | } 128 | }); 129 | 130 | 131 | $('.manage-dashboard').entwine({ 132 | onclick: function(e) { 133 | e.preventDefault(); 134 | $('.dashboard').createPanel(); 135 | } 136 | }); 137 | 138 | 139 | $('.dashboard-message-link').entwine({ 140 | onclick: function(e) { 141 | e.preventDefault(); 142 | $.ajax({ 143 | url: this.attr('href'), 144 | success: function(data) { 145 | $('#dashboard-message').html(data).slideDown(); 146 | setTimeout(function() { 147 | $('#dashboard-message').slideUp(function() {$(this).html('');}); 148 | },5000); 149 | } 150 | }) 151 | } 152 | }) 153 | 154 | $('.btn-dashboard-panel-delete').entwine({ 155 | onclick: function(e) { 156 | e.preventDefault(); 157 | var $panel = this.getPanel(); 158 | $.ajax({ 159 | url: this.attr('href'), 160 | success: function() { 161 | $panel.fadeOut(function() { 162 | $panel.remove(); 163 | }) 164 | } 165 | }) 166 | } 167 | }); 168 | 169 | 170 | 171 | 172 | $('.btn-dashboard-panel-configure').entwine({ 173 | onclick: function(e) { 174 | e.preventDefault(); 175 | this.getPanel().showConfigure(); 176 | } 177 | }); 178 | 179 | $('.dashboard-panel-configure-actions [name=action_cancel]').entwine({ 180 | onclick: function(e) { 181 | e.preventDefault(); 182 | this.getPanel().addClass("refreshable"); 183 | this.getPanel().hideConfigure(); 184 | } 185 | }); 186 | 187 | 188 | $('.dashboard-panel-configure-actions [name=action_saveConfiguration]').entwine({ 189 | onclick: function(e) { 190 | e.preventDefault(); 191 | var $form = this.getConfigurationForm(); 192 | $.ajax({ 193 | url: $form.attr('action'), 194 | data: $form.serialize(), 195 | type: "POST", 196 | success: function(data) { 197 | $form.getPanel().addClass("refreshable"); 198 | $form.getPanelInner().revertFlip(); 199 | } 200 | }) 201 | } 202 | }) 203 | 204 | 205 | 206 | $('.available-panel').entwine({ 207 | onclick: function(e) { 208 | e.preventDefault(); 209 | configure = this.data('configure'); 210 | var $this = this; 211 | $.ajax({ 212 | url: this.data('create-url'), 213 | success: function(data) { 214 | $this.getPanel().replaceWith(data); 215 | $('.dashboard').setSort($('.dashboard').sortable("serialize")); 216 | if(configure) { 217 | $('.dashboard-panel:first').showConfigure(); 218 | } 219 | } 220 | }) 221 | } 222 | }) 223 | 224 | 225 | 226 | 227 | $('.dashboard-sortable').entwine({ 228 | setSort: function(serial) { 229 | $.ajax({ 230 | url: this.attr('data-sort-url'), 231 | data: serial 232 | }); 233 | } 234 | }) 235 | 236 | $( ".dashboard").entwine({ 237 | onmatch: function() { 238 | this.sortable({ 239 | items: ".dashboard-panel", 240 | handle: ".dashboard-panel-header", 241 | update: function() { 242 | $(".dashboard").setSort($(this).sortable("serialize")); 243 | } 244 | }); 245 | }, 246 | createPanel: function() { 247 | var $newpanel = $('.dashboard-panel-selection:first').clone(); 248 | $newpanel.show().css('width',0); 249 | $('.dashboard-panel-list').prepend($newpanel); 250 | $newpanel.animate({'width':'45%'},function() { 251 | $(this).find('.dashboard-panel-selection-inner').fadeIn(); 252 | }) 253 | } 254 | }); 255 | 256 | 257 | $('.dashboard-create-cancel').entwine({ 258 | onclick: function(e) { 259 | this.getPanel().find('.dashboard-panel-selection-inner').fadeOut(function() { 260 | $(this).getPanel().animate({'width':0}, function() { 261 | $(this).remove(); 262 | }) 263 | }); 264 | } 265 | }) 266 | 267 | 268 | $('.dashboard-has-many-editor *').entwine({ 269 | getFormHolder: function() { 270 | return this.closest(".dashboard-has-many-editor").find('.dashboard-has-many-editor-form:first'); 271 | } 272 | }); 273 | 274 | 275 | $('.dashboard-has-many-editor-header a').entwine({ 276 | onclick: function(e) { 277 | e.preventDefault(); 278 | this.getHasManyFormWrapper().toggle(); 279 | this.getHasManyFormWrapper().loadForm(); 280 | } 281 | }); 282 | 283 | 284 | $('.dashboard-has-many-editor').entwine({ 285 | refresh: function() { 286 | var $t = this; 287 | $.ajax({ 288 | url: this.data('refresh-url'), 289 | success: function(data) { 290 | $t.replaceWith(data); 291 | } 292 | }) 293 | } 294 | }); 295 | 296 | 297 | 298 | $('.dashboard-has-many-editor-form').entwine({ 299 | 300 | Open: false, 301 | 302 | onmatch: function () { 303 | this.css({'width': this.getPanel().innerWidth()-40}); 304 | }, 305 | reveal: function(callback) { 306 | this.animate({height: '248px' }, callback); 307 | this.getPanel().find('.dashboard-panel-configure-actions').hide(); 308 | 309 | }, 310 | obscure: function(callback) { 311 | this.animate({height: 0 }, callback); 312 | this.getPanel().find('.dashboard-panel-configure-actions').show(); 313 | 314 | }, 315 | toggle: function(callback) { 316 | if(this.getOpen()) { 317 | this.obscure(callback); 318 | } 319 | else { 320 | this.reveal(callback); 321 | } 322 | }, 323 | loadForm: function(link) { 324 | var $t = this; 325 | if(!link) { 326 | link = this.data('url'); 327 | } 328 | $.ajax({ 329 | url: link, 330 | success: function(data) { 331 | $t.reveal(function() { 332 | $t.getPanel().append(data); 333 | }) 334 | } 335 | }); 336 | } 337 | 338 | }); 339 | 340 | $('.dashboard-has-many-editor-detail-form-form').entwine({ 341 | onmatch: function() { 342 | this.css({'width': this.getPanel().innerWidth()-50}); 343 | }, 344 | onsubmit: function(e) { 345 | e.preventDefault(); 346 | var $form = this; 347 | $.ajax({ 348 | url: $form.attr('action'), 349 | type: "POST", 350 | data: $form.serialize(), 351 | success: function(data) { 352 | $form.fadeOut(function() { 353 | $(this).getHasManyFormWrapper().obscure(); 354 | }); 355 | $form.getEditor().refresh(); 356 | } 357 | }) 358 | }, 359 | getEditor: function() { 360 | return this.getPanel().find('.dashboard-has-many-editor:first'); 361 | } 362 | }); 363 | 364 | $('.dashboard-has-many-editor-detail-form-actions [name=action_cancel]').entwine({ 365 | onclick: function(e) { 366 | e.preventDefault(); 367 | this.closest("form").fadeOut(function() { 368 | $(this).getEditor().obscure(); 369 | }); 370 | 371 | } 372 | }); 373 | 374 | 375 | 376 | 377 | $('.dashboard-has-many-editor-detail-form-actions [name=action_cancel]').entwine({ 378 | onclick: function(e) { 379 | e.preventDefault(); 380 | this.closest("form").fadeOut(function() { 381 | $(this).getPanel().find('.dashboard-has-many-editor-form').obscure(); 382 | }); 383 | 384 | } 385 | }); 386 | 387 | 388 | $('.dashboard-has-many-list .delete-link').entwine({ 389 | onclick: function(e) { 390 | e.preventDefault(); 391 | var $t = this; 392 | $.ajax({ 393 | url: this.attr('href'), 394 | success: function() { 395 | $t.closest("li").fadeOut(function() { 396 | $(this).remove(); 397 | }) 398 | } 399 | }) 400 | } 401 | }); 402 | 403 | 404 | 405 | $('.dashboard-has-many-list .edit-link').entwine({ 406 | onclick: function(e) { 407 | e.preventDefault(); 408 | this.getFormHolder().toggle(); 409 | this.getFormHolder().loadForm(this.attr('href')); 410 | } 411 | }); 412 | 413 | $('.dashboard-has-many-list').entwine({ 414 | onmatch: function() { 415 | var $t = this; 416 | this.sortable({ 417 | items: "li", 418 | update: function() { 419 | $t.setSort($(this).sortable("serialize")); 420 | } 421 | }); 422 | } 423 | }); 424 | 425 | 426 | 427 | $('.configure-form .dashboard-button-options-btn-group > a').entwine({ 428 | onclick: function(e) { 429 | this.closest(".dashboard-panel") 430 | .removeClass(this.getButtonGroup().getValue()) 431 | .addClass(this.data('value')); 432 | this._super(e); 433 | } 434 | }); 435 | 436 | })(jQuery); -------------------------------------------------------------------------------- /css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* @override 2 | http://dos/dashboard/css/dashboard.css?m=1349719778 3 | http://dos/dashboard/css/dashboard.css?m=1349726313 4 | http://dos/dashboard/css/dashboard.css?m=1350097133 5 | */ 6 | 7 | .dashboard-top-buttons { 8 | position: absolute; 9 | right: 10px; 10 | top: 5px; 11 | } 12 | 13 | .dashboard { 14 | margin: 40px 40px 0 40px; 15 | height: 90%; 16 | overflow: auto; 17 | } 18 | 19 | .dashboard.hover { 20 | background: #f9f9f9; 21 | } 22 | 23 | .dashboard-panel-inner { 24 | background: #f9f9f9; 25 | border: 1px solid #c7c7c7; 26 | height: 312px; 27 | position: relative; 28 | } 29 | 30 | #Title label.left { 31 | padding-top: 0; 32 | } 33 | 34 | .dashboard-panel-list { 35 | margin-left: -20px; 36 | list-style: none; 37 | margin-bottom: 0; 38 | } 39 | 40 | .dashboard-panel { 41 | display: inline-block; 42 | width: 50%; 43 | max-width: 500px; 44 | padding-top: 20px; 45 | padding-left: 20px; 46 | vertical-align: top; 47 | box-sizing: border-box; 48 | } 49 | 50 | .dashboard-panel.large { 51 | width: 100%; 52 | max-width: 1000px; 53 | } 54 | 55 | .dashboard-panel.small { 56 | width: 25%; 57 | max-width: 250px; 58 | } 59 | 60 | .dashboard-panel-inner.loading { 61 | opacity: 0.5; 62 | } 63 | 64 | .dashboard-panel-inner .dashboard-panel-icon { 65 | position: absolute; 66 | top: 10px; 67 | left: 10px; 68 | z-index: 999; 69 | } 70 | 71 | .dashboard-panel-inner .dashboard-panel-header { 72 | padding: 13px 0 13px 40px; 73 | background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#f8f8f8),to(#f1eaea)); 74 | background-image: -moz-linear-gradient(0% 100% 90deg,#f8f8f8,#f1eaea); 75 | border-bottom: 1px solid #dad8d8; 76 | cursor: move; 77 | } 78 | 79 | .dashboard-panel-header h3 { 80 | margin: 0; 81 | padding: 0; 82 | text-align: left; 83 | text-transform: uppercase; 84 | font-size: 13px; 85 | } 86 | 87 | .dashboard-panel-header-actions { 88 | position: absolute; 89 | top: 9px; 90 | right: 10px; 91 | } 92 | 93 | .dashboard-panel-action.ss-ui-button { 94 | font-size: 11px; 95 | padding: 0; 96 | } 97 | 98 | .dashboard-panel .dashboard-panel-content { 99 | height: 213px; 100 | overflow: auto; 101 | padding: 10px; 102 | box-sizing: content-box; 103 | } 104 | 105 | .dashboard-panel.hasActions .dashboard-panel-content { 106 | height: 268px; 107 | } 108 | 109 | .dashboard-panel-footer { 110 | position: relative; 111 | min-height: 25px; 112 | padding: 5px 4px; 113 | background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#f8f8f8),to(#f1eaea)); 114 | background-image: -moz-linear-gradient(0% 100% 90deg,#f8f8f8,#f1eaea); 115 | border-bottom: 1px solid #dad8d8; 116 | box-sizing: content-box; 117 | } 118 | 119 | .dashboard-panel-footer .dashboard-panel-toolbar { 120 | position: absolute; 121 | top: 7px; 122 | right: 10px; 123 | z-index: 999; 124 | } 125 | 126 | .dashboard-panel-footer-actions { 127 | text-align: center; 128 | } 129 | 130 | .dashboard-panel-inner .dashboard-panel-configure { 131 | display: none; 132 | } 133 | 134 | .dashboard-panel-inner .dashboard-panel-configure-fields { 135 | height: 237px; 136 | overflow: auto; 137 | padding: 10px; 138 | box-sizing: content-box; 139 | } 140 | 141 | .dashboard-panel-inner .dashboard-panel-configure-actions { 142 | height: 40px; 143 | position: relative; 144 | text-align: center; 145 | background-color: #d5d5d5; 146 | background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#e4e4e4),to(#bdbdbd)); 147 | background-image: -moz-linear-gradient(0% 100% 90deg,#bdbdbd,#e4e4e4); 148 | -webkit-box-shadow: inset 0 1px 0 #f0f0f0; 149 | -moz-box-shadow: inset 0 1px 0 #f0f0f0; 150 | box-shadow: inset 0 1px 0 #f0f0f0; 151 | z-index: 1!important; 152 | padding: 8px 0; 153 | box-sizing: content-box; 154 | } 155 | 156 | .dashboard-panel-inner .middleColumn { 157 | margin: 0; 158 | } 159 | 160 | .dashboard-panel-inner form .field input[type=text] { 161 | width: 100%; 162 | display: block; 163 | background-color: #fafafa; 164 | border: 1px solid #ccc; 165 | font-size: 12px; 166 | color: #444; 167 | -webkit-box-shadow: inset 0 2px 3px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.35); 168 | -moz-box-shadow: inset 0 2px 3px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.35); 169 | box-shadow: inset 0 2px 3px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.35); 170 | line-height: 19px; 171 | resize: none; 172 | padding: 4px; 173 | } 174 | 175 | .dashboard-panel-inner form .field label.left { 176 | float: none; 177 | text-transform: uppercase; 178 | width: 100%; 179 | } 180 | 181 | .dashboard-panel-inner form .field.checkbox { 182 | padding-left: 0; 183 | } 184 | 185 | .dashboard-panel-inner form .field.dashboardbuttonoptions { 186 | float: right; 187 | box-shadow: none; 188 | -webkit-box-shadow: none; 189 | -moz-box-shadow: none; 190 | border-bottom: 0; 191 | } 192 | 193 | .dashboard-panel-inner form .field.dashboardbuttonoptions label.left { 194 | float: left; 195 | width: auto; 196 | } 197 | 198 | .dashboard-panel-inner form .field.dashboardbuttonoptions .middleColumn { 199 | min-width: 80px; 200 | } 201 | 202 | .dashboard-panel-inner ul.optionset li { 203 | float: none; 204 | width: auto; 205 | } 206 | 207 | .dashboard-panel-buttons { 208 | position: absolute; 209 | bottom: 10px; 210 | text-align: center; 211 | } 212 | 213 | .dashboard-panel-inner ul li { 214 | border-bottom: 1px solid #ddd; 215 | } 216 | 217 | .dashboard-panel-inner ul li:last-child { 218 | border: 0; 219 | } 220 | 221 | .dashboard-panel-inner ul li a { 222 | padding: 4px; 223 | display: block; 224 | } 225 | 226 | .dashboard-panel-inner ul li a:hover { 227 | background: #ddd; 228 | text-decoration: none; 229 | } 230 | 231 | .available-panel { 232 | position: relative; 233 | width: 47%; 234 | height: 75px; 235 | margin: 0 0 10px 0; 236 | background-color: #dcdcdc; 237 | background-image: -webkit-gradient(linear,0% 0,0% 100%,from(#f1f1f1),to(#dcdcdc)); 238 | background-image: -moz-linear-gradient(0% 100% 90deg,#dcdcdc,#f1f1f1); 239 | -webkit-box-shadow: inset 0 1px 0 #f7f7f7; 240 | -moz-box-shadow: inset 0 1px 0 #f7f7f7; 241 | box-shadow: inset 0 1px 0 #f7f7f7; 242 | border-style: solid; 243 | border-width: 1px; 244 | border-color: #b5b5b5 #999 #999 #b5b5b5; 245 | position: relative; 246 | overflow: visible!important; 247 | z-index: auto!important; 248 | padding: 4px; 249 | cursor: pointer; 250 | } 251 | 252 | .available-panel.odd { 253 | float: left; 254 | clear: right; 255 | } 256 | 257 | .available-panel.even { 258 | float: right; 259 | } 260 | 261 | .available-panel:hover { 262 | background: rgba(180,180,180,0.3); 263 | } 264 | 265 | .available-panel h4 { 266 | margin: 5px 0 8px 0; 267 | } 268 | 269 | .available-panel .available-panel-content { 270 | margin: 0 10px 0 25%; 271 | } 272 | 273 | .available-panel .available-panel-icon { 274 | position: absolute; 275 | top: 50%; 276 | margin-top: -34px; 277 | padding: 18px 6px 0 0; 278 | text-align: center; 279 | width: 25%; 280 | } 281 | 282 | .available-panel-icon img { 283 | width: 60%; 284 | max-width: 32px; 285 | } 286 | 287 | .dashboard-panel-selection, 288 | .dashboard-panel-selection-inner { 289 | display: none; 290 | } 291 | 292 | .available-panel-list { 293 | overflow: auto; 294 | } 295 | 296 | .dashboard-has-many-editor { 297 | background: #ccc; 298 | } 299 | 300 | .dashboard-has-many-list { 301 | background: #f9f9f9; 302 | margin: 0 10px; 303 | } 304 | 305 | .dashboard-has-many-list li:after { 306 | content: "."; 307 | visibility: hidden; 308 | display: block; 309 | clear: both; 310 | line-height: 0; 311 | height: 0; 312 | } 313 | 314 | .dashboard-panel-inner .dashboard-has-many-list li a:hover { 315 | background: transparent; 316 | } 317 | 318 | .dashboard-has-many-list .edit-link { 319 | float: left; 320 | width: 80%; 321 | } 322 | 323 | .dashboard-has-many-list .delete-link { 324 | float: right; 325 | width: 16px; 326 | } 327 | 328 | .dashboard-has-many-editor-form { 329 | position: absolute; 330 | height: 0; 331 | background: #f9f9f9; 332 | bottom: 34px; 333 | padding: 10px; 334 | } 335 | 336 | .dashboard-has-many-editor-detail-form { 337 | 338 | } 339 | 340 | .dashboard-has-many-editor-header { 341 | position: relative; 342 | padding: 0 11px; 343 | } 344 | 345 | .dashboard-has-many-editor-header .ss-ui-button { 346 | position: absolute; 347 | top: 7px; 348 | right: 10px; 349 | } 350 | 351 | .dashboard-has-many-editor-detail-form-actions { 352 | text-align: center; 353 | } 354 | 355 | .dashboard-has-many-editor-detail-form-form { 356 | position: absolute; 357 | z-index: 999; 358 | left: 22px; 359 | top: 16px; 360 | } 361 | 362 | .ss-ui-button.ui-button-text-only.small .ui-button-text { 363 | font-size: 11px; 364 | padding: 0.1em 0.5em; 365 | } 366 | 367 | .dashboard-panel-inner .dashboard-panel-quick-links-list li { 368 | border: 0; 369 | } 370 | 371 | .dashboard-panel-quick-link { 372 | margin: 10px auto; 373 | } 374 | 375 | #dashboard-message { 376 | padding: 4px; 377 | margin: 0px; 378 | border: solid 1px #C0F0B9; 379 | background: #D5FFC6; 380 | color: #48A41C; 381 | font-family: Arial, Helvetica, sans-serif; 382 | font-size: 14px; 383 | font-weight: bold; 384 | text-align: center; 385 | display: none; 386 | } 387 | 388 | .ss-fancy-dropdown { 389 | position: relative; 390 | } 391 | 392 | .ss-fancy-dropdown-btn .ui-button-text { 393 | padding-right: 16px; 394 | background: url(../images/dropdown.gif) no-repeat right center; 395 | } 396 | 397 | .ss-fancy-dropdown-options { 398 | position: absolute; 399 | top: 22px; 400 | display: none; 401 | background: #fff; 402 | border: 1px solid #C0C0C2; 403 | min-width: 250px; 404 | -webkit-box-shadow: 0px 1px 8px rgba(50, 50, 50, 0.75); 405 | -moz-box-shadow: 0px 1px 8px rgba(50, 50, 50, 0.75); 406 | box-shadow: 0px 1px 8px rgba(50, 50, 50, 0.75); 407 | } 408 | 409 | .ss-fancy-dropdown-options a { 410 | display: block; 411 | padding: 10px; 412 | } 413 | 414 | .ss-fancy-dropdown.right .ss-fancy-dropdown-options { 415 | right: 1px; 416 | } 417 | 418 | .ss-fancy-dropdown.left .ss-fancy-dropdown-options { 419 | left: 1px; 420 | } 421 | 422 | .ss-fancy-dropdown-options a:hover { 423 | text-decoration: none; 424 | background: rgb(44,122,166); 425 | color: #fff; 426 | } 427 | 428 | @media screen and (max-width: 1180px) { 429 | 430 | .available-panel { 431 | height: 100px; 432 | width: 45%; 433 | } 434 | 435 | } 436 | 437 | @media screen and (max-width:960px) { 438 | 439 | .available-panel-icon { 440 | display: none; 441 | } 442 | 443 | .available-panel .available-panel-content { 444 | margin: 0; 445 | } 446 | 447 | .available-panel { 448 | margin: 0; 449 | } 450 | 451 | } 452 | 453 | .dashboard-button-options-btn-group a{ 454 | width: 32px; 455 | height: 17px; 456 | display: block; 457 | float: left; 458 | cursor: pointer; 459 | } 460 | 461 | .dashboard-button-options-btn-group a.first { 462 | background: url(../images/panel-grid.png) 0 0; 463 | } 464 | .dashboard-button-options-btn-group a.middle { 465 | width: 26px; 466 | background: url(../images/panel-grid.png) -32px 0; 467 | } 468 | .dashboard-button-options-btn-group a.last { 469 | background: url(../images/panel-grid.png) -58px 0; 470 | } 471 | 472 | .dashboard-button-options-btn-group a.first:hover { 473 | background: url(../images/panel-grid.png) 0 -66px; 474 | } 475 | .dashboard-button-options-btn-group a.middle:hover { 476 | background: url(../images/panel-grid.png) -32px -66px; 477 | } 478 | .dashboard-button-options-btn-group a.last:hover { 479 | background: url(../images/panel-grid.png) -58px -66px; 480 | } 481 | 482 | .dashboard-button-options-btn-group a.first.active { 483 | background: url(../images/panel-grid.png) 0 -66px; 484 | } 485 | .dashboard-button-options-btn-group a.middle.active { 486 | background: url(../images/panel-grid.png) -32px -66px; 487 | } 488 | .dashboard-button-options-btn-group a.last.active { 489 | background: url(../images/panel-grid.png) -58px -66px; 490 | } 491 | 492 | 493 | 494 | .dashboard-button-options-btn-group a img{ 495 | display: none; 496 | } 497 | 498 | -------------------------------------------------------------------------------- /javascript/jquery.flip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Flip! jQuery Plugin (http://lab.smashup.it/flip/) 3 | * @author Luca Manno (luca@smashup.it) [http://i.smashup.it] 4 | * [Original idea by Nicola Rizzo (thanks!)] 5 | * 6 | * @version 0.9.9 [Nov. 2009] 7 | * 8 | * @changelog 9 | * v 0.9.9 -> Fix transparency over non-colored background. Added dontChangeColor option. 10 | * Added $clone and $this parameters to on.. callback functions. 11 | * Force hexadecimal color values. Made safe for noConflict use. 12 | * Some refactoring. [Henrik Hjelte, Jul. 10, 2009] 13 | * Added revert options, fixes and improvements on color management. 14 | * Released in Nov 2009 15 | * v 0.5 -> Added patch to make it work with Opera (thanks to Peter Siewert), Added callbacks [Feb. 1, 2008] 16 | * v 0.4.1 -> Fixed a regression in Chrome and Safari caused by getTransparent [Oct. 1, 2008] 17 | * v 0.4 -> Fixed some bugs with transparent color. Now Flip! works on non-white backgrounds | Update: jquery.color.js plugin or jqueryUI still needed :( [Sept. 29, 2008] 18 | * v 0.3 -> Now is possibile to define the content after the animation. 19 | * (jQuery object or text/html is allowed) [Sept. 25, 2008] 20 | * v 0.2 -> Fixed chainability and buggy innertext rendering (xNephilimx thanks!) 21 | * v 0.1 -> Starting release [Sept. 11, 2008] 22 | * 23 | */ 24 | (function($) { 25 | 26 | function int_prop(fx){ 27 | fx.elem.style[ fx.prop ] = parseInt(fx.now,10) + fx.unit; 28 | } 29 | 30 | var throwError=function(message) { 31 | throw({name:"jquery.flip.js plugin error",message:message}); 32 | }; 33 | 34 | var isIE6orOlder=function() { 35 | // User agent sniffing is clearly out of fashion and $.browser will be be deprectad. 36 | // Now, I can't think of a way to feature detect that IE6 doesn't show transparent 37 | // borders in the correct way. 38 | // Until then, this function will do, and be partly political correct, allowing 39 | // 0.01 percent of the internet users to tweak with their UserAgent string. 40 | // 41 | // Not leadingWhiteSpace is to separate IE family from, well who knows? 42 | // Maybe some version of Opera? 43 | // The second guess behind this is that IE7+ will keep supporting maxHeight in the future. 44 | 45 | // First guess changed to dean edwards ie sniffing http://dean.edwards.name/weblog/2007/03/sniff/ 46 | return (/*@cc_on!@*/false && (typeof document.body.style.maxHeight === "undefined")); 47 | }; 48 | 49 | 50 | // Some named colors to work with 51 | // From Interface by Stefan Petre 52 | // http://interface.eyecon.ro/ 53 | 54 | var colors = { 55 | aqua:[0,255,255], 56 | azure:[240,255,255], 57 | beige:[245,245,220], 58 | black:[0,0,0], 59 | blue:[0,0,255], 60 | brown:[165,42,42], 61 | cyan:[0,255,255], 62 | darkblue:[0,0,139], 63 | darkcyan:[0,139,139], 64 | darkgrey:[169,169,169], 65 | darkgreen:[0,100,0], 66 | darkkhaki:[189,183,107], 67 | darkmagenta:[139,0,139], 68 | darkolivegreen:[85,107,47], 69 | darkorange:[255,140,0], 70 | darkorchid:[153,50,204], 71 | darkred:[139,0,0], 72 | darksalmon:[233,150,122], 73 | darkviolet:[148,0,211], 74 | fuchsia:[255,0,255], 75 | gold:[255,215,0], 76 | green:[0,128,0], 77 | indigo:[75,0,130], 78 | khaki:[240,230,140], 79 | lightblue:[173,216,230], 80 | lightcyan:[224,255,255], 81 | lightgreen:[144,238,144], 82 | lightgrey:[211,211,211], 83 | lightpink:[255,182,193], 84 | lightyellow:[255,255,224], 85 | lime:[0,255,0], 86 | magenta:[255,0,255], 87 | maroon:[128,0,0], 88 | navy:[0,0,128], 89 | olive:[128,128,0], 90 | orange:[255,165,0], 91 | pink:[255,192,203], 92 | purple:[128,0,128], 93 | violet:[128,0,128], 94 | red:[255,0,0], 95 | silver:[192,192,192], 96 | white:[255,255,255], 97 | yellow:[255,255,0], 98 | transparent: [255,255,255] 99 | }; 100 | 101 | var acceptHexColor=function(color) { 102 | if(color && color.indexOf("#")==-1 && color.indexOf("(")==-1){ 103 | return "rgb("+colors[color].toString()+")"; 104 | } else { 105 | return color; 106 | } 107 | }; 108 | 109 | $.extend( $.fx.step, { 110 | borderTopWidth : int_prop, 111 | borderBottomWidth : int_prop, 112 | borderLeftWidth: int_prop, 113 | borderRightWidth: int_prop 114 | }); 115 | 116 | $.fn.revertFlip = function(){ 117 | return this.each( function(){ 118 | var $this = $(this); 119 | $this.flip($this.data('flipRevertedSettings')); 120 | }); 121 | }; 122 | 123 | $.fn.flip = function(settings){ 124 | return this.each( function() { 125 | var $this=$(this), flipObj, $clone, dirOption, dirOptions, newContent, ie6=isIE6orOlder(); 126 | 127 | if($this.data('flipLock')){ 128 | return false; 129 | } 130 | 131 | var revertedSettings = { 132 | direction: (function(direction){ 133 | switch(direction) 134 | { 135 | case "tb": 136 | return "bt"; 137 | case "bt": 138 | return "tb"; 139 | case "lr": 140 | return "rl"; 141 | case "rl": 142 | return "lr"; 143 | default: 144 | return "bt"; 145 | } 146 | })(settings.direction), 147 | bgColor: acceptHexColor(settings.color) || "#999", 148 | color: acceptHexColor(settings.bgColor) || $this.css("background-color"), 149 | content: $this.html(), 150 | speed: settings.speed || 500, 151 | onBefore: settings.onBefore || function(){}, 152 | onEnd: settings.onEnd || function(){}, 153 | onAnimation: settings.onAnimation || function(){} 154 | }; 155 | 156 | $this 157 | .data('flipRevertedSettings',revertedSettings) 158 | .data('flipLock',1) 159 | .data('flipSettings',revertedSettings); 160 | 161 | flipObj = { 162 | width: $this.width(), 163 | height: $this.height(), 164 | bgColor: acceptHexColor(settings.bgColor) || $this.css("background-color"), 165 | fontSize: $this.css("font-size") || "12px", 166 | direction: settings.direction || "tb", 167 | toColor: acceptHexColor(settings.color) || "#999", 168 | speed: settings.speed || 500, 169 | top: $this.offset().top, 170 | left: $this.offset().left, 171 | target: settings.content || null, 172 | transparent: "transparent", 173 | dontChangeColor: settings.dontChangeColor || false, 174 | onBefore: settings.onBefore || function(){}, 175 | onEnd: settings.onEnd || function(){}, 176 | onAnimation: settings.onAnimation || function(){} 177 | }; 178 | 179 | // This is the first part of a trick to support 180 | // transparent borders using chroma filter for IE6 181 | // The color below is arbitrary, lets just hope it is not used in the animation 182 | ie6 && (flipObj.transparent="#123456"); 183 | 184 | $clone= $this.css("visibility","hidden") 185 | .clone(true) 186 | .data('flipLock',1) 187 | .appendTo("body") 188 | .html("") 189 | .css({visibility:"visible",position:"absolute",left:flipObj.left,top:flipObj.top,margin:0,zIndex:9999,"-webkit-box-shadow":"0px 0px 0px #000","-moz-box-shadow":"0px 0px 0px #000"}); 190 | 191 | var defaultStart=function() { 192 | return { 193 | backgroundColor: flipObj.transparent, 194 | fontSize:0, 195 | lineHeight:0, 196 | borderTopWidth:0, 197 | borderLeftWidth:0, 198 | borderRightWidth:0, 199 | borderBottomWidth:0, 200 | borderTopColor:flipObj.transparent, 201 | borderBottomColor:flipObj.transparent, 202 | borderLeftColor:flipObj.transparent, 203 | borderRightColor:flipObj.transparent, 204 | background: "none", 205 | borderStyle:'solid', 206 | height:0, 207 | width:0 208 | }; 209 | }; 210 | var defaultHorizontal=function() { 211 | var waist=(flipObj.height/100)*25; 212 | var start=defaultStart(); 213 | start.width=flipObj.width; 214 | return { 215 | "start": start, 216 | "first": { 217 | borderTopWidth: 0, 218 | borderLeftWidth: waist, 219 | borderRightWidth: waist, 220 | borderBottomWidth: 0, 221 | borderTopColor: '#999', 222 | borderBottomColor: '#999', 223 | top: (flipObj.top+(flipObj.height/2)), 224 | left: (flipObj.left-waist)}, 225 | "second": { 226 | borderBottomWidth: 0, 227 | borderTopWidth: 0, 228 | borderLeftWidth: 0, 229 | borderRightWidth: 0, 230 | borderTopColor: flipObj.transparent, 231 | borderBottomColor: flipObj.transparent, 232 | top: flipObj.top, 233 | left: flipObj.left} 234 | }; 235 | }; 236 | var defaultVertical=function() { 237 | var waist=(flipObj.height/100)*25; 238 | var start=defaultStart(); 239 | start.height=flipObj.height; 240 | return { 241 | "start": start, 242 | "first": { 243 | borderTopWidth: waist, 244 | borderLeftWidth: 0, 245 | borderRightWidth: 0, 246 | borderBottomWidth: waist, 247 | borderLeftColor: '#999', 248 | borderRightColor: '#999', 249 | top: flipObj.top-waist, 250 | left: flipObj.left+(flipObj.width/2)}, 251 | "second": { 252 | borderTopWidth: 0, 253 | borderLeftWidth: 0, 254 | borderRightWidth: 0, 255 | borderBottomWidth: 0, 256 | borderLeftColor: flipObj.transparent, 257 | borderRightColor: flipObj.transparent, 258 | top: flipObj.top, 259 | left: flipObj.left} 260 | }; 261 | }; 262 | 263 | dirOptions = { 264 | "tb": function () { 265 | var d=defaultHorizontal(); 266 | d.start.borderTopWidth=flipObj.height; 267 | d.start.borderTopColor=flipObj.bgColor; 268 | d.second.borderBottomWidth= flipObj.height; 269 | d.second.borderBottomColor= flipObj.toColor; 270 | return d; 271 | }, 272 | "bt": function () { 273 | var d=defaultHorizontal(); 274 | d.start.borderBottomWidth=flipObj.height; 275 | d.start.borderBottomColor= flipObj.bgColor; 276 | d.second.borderTopWidth= flipObj.height; 277 | d.second.borderTopColor= flipObj.toColor; 278 | return d; 279 | }, 280 | "lr": function () { 281 | var d=defaultVertical(); 282 | d.start.borderLeftWidth=flipObj.width; 283 | d.start.borderLeftColor=flipObj.bgColor; 284 | d.second.borderRightWidth= flipObj.width; 285 | d.second.borderRightColor= flipObj.toColor; 286 | return d; 287 | }, 288 | "rl": function () { 289 | var d=defaultVertical(); 290 | d.start.borderRightWidth=flipObj.width; 291 | d.start.borderRightColor=flipObj.bgColor; 292 | d.second.borderLeftWidth= flipObj.width; 293 | d.second.borderLeftColor= flipObj.toColor; 294 | return d; 295 | } 296 | }; 297 | 298 | dirOption=dirOptions[flipObj.direction](); 299 | 300 | // Second part of IE6 transparency trick. 301 | ie6 && (dirOption.start.filter="chroma(color="+flipObj.transparent+")"); 302 | 303 | newContent = function(){ 304 | var target = flipObj.target; 305 | return target && target.jquery ? target.html() : target; 306 | }; 307 | 308 | $clone.queue(function(){ 309 | flipObj.onBefore($clone,$this); 310 | $clone.html(' ').css(dirOption.start); 311 | $clone.dequeue(); 312 | }); 313 | 314 | $clone.animate(dirOption.first,flipObj.speed); 315 | 316 | $clone.queue(function(){ 317 | flipObj.onAnimation($clone,$this); 318 | $clone.dequeue(); 319 | }); 320 | $clone.animate(dirOption.second,flipObj.speed); 321 | 322 | $clone.queue(function(){ 323 | if (!flipObj.dontChangeColor) { 324 | $this.css({backgroundColor: flipObj.toColor}); 325 | } 326 | $this.css({visibility: "visible"}); 327 | 328 | var nC = newContent(); 329 | if(nC){$this.html(nC);} 330 | $clone.remove(); 331 | flipObj.onEnd($clone,$this); 332 | $this.removeData('flipLock'); 333 | $clone.dequeue(); 334 | }); 335 | }); 336 | }; 337 | })(jQuery); 338 | -------------------------------------------------------------------------------- /src/Dashboard.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class Dashboard extends LeftAndMain implements PermissionProvider { 34 | 35 | 36 | 37 | private static $menu_title = "Dashboard"; 38 | 39 | 40 | 41 | private static $url_segment = "dashboard"; 42 | 43 | 44 | 45 | private static $menu_priority = 100; 46 | 47 | 48 | 49 | private static $url_priority = 30; 50 | 51 | 52 | 53 | private static $menu_icon = "unclecheese/dashboard:images/dashboard.png"; 54 | 55 | 56 | 57 | private static $tree_class = 'DashboardPanel'; 58 | 59 | 60 | 61 | private static $url_handlers = [ 62 | 'panel/$ID' => 'handlePanel', 63 | '$Action!' => '$Action', 64 | '' => 'index' 65 | ]; 66 | 67 | public function init() { 68 | parent::init(); 69 | Requirements::css("unclecheese/dashboard:css/dashboard.css"); 70 | Requirements::javascript("unclecheese/dashboard:javascript/jquery.flip.js"); 71 | Requirements::javascript("unclecheese/dashboard:javascript/dashboard.js"); 72 | } 73 | 74 | private static $allowed_actions = [ 75 | "handlePanel", 76 | "sort", 77 | "setdefault", 78 | "applytoall" 79 | ]; 80 | 81 | 82 | /** 83 | * Provides custom permissions to the Security section 84 | * 85 | * @return array 86 | */ 87 | public function providePermissions() { 88 | $title = _t("Dashboard.MENUTITLE", LeftAndMain::menu_title_for_class('Dashboard')); 89 | return [ 90 | "CMS_ACCESS_Dashboard" => [ 91 | 'name' => _t('Dashboard.ACCESS', "Access to '{title}' section", ['title' => $title]), 92 | 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 93 | 'help' => _t( 94 | 'Dashboard.ACCESS_HELP', 95 | 'Allow use of the CMS Dashboard' 96 | ) 97 | ], 98 | "CMS_ACCESS_DashboardAddPanels" => [ 99 | 'name' => _t('Dashboard.ADDPANELS', "Add dashboard panels"), 100 | 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 101 | 'help' => _t( 102 | 'Dashboard.ACCESS_HELP', 103 | 'Allow user to add panels to his/her dashboard' 104 | ) 105 | ], 106 | "CMS_ACCESS_DashboardConfigurePanels" => [ 107 | 'name' => _t('Dashboard.CONFIGUREANELS', "Configure dashboard panels"), 108 | 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 109 | 'help' => _t( 110 | 'Dashboard.ACCESS_HELP', 111 | 'Allow user to configure his/her dashboard panels' 112 | ), 113 | ], 114 | "CMS_ACCESS_DashboardDeletePanels" => [ 115 | 'name' => _t('Dashboard.DELETEPANELS', "Remove dashboard panels"), 116 | 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 117 | 'help' => _t( 118 | 'Dashboard.ACCESS_HELP', 119 | 'Allow user to remove panels from his/her dashboard' 120 | ) 121 | ] 122 | ]; 123 | } 124 | 125 | 126 | /** 127 | * Handles a request for a {@link DashboardPanel} object. Can be a new record or existing 128 | * 129 | * @param HTTPRequest $r 130 | * @return HTTPResponse 131 | * @throws \SilverStripe\Control\HTTPResponse_Exception 132 | * @throws \SilverStripe\ORM\ValidationException 133 | */ 134 | public function handlePanel(HTTPRequest $r) { 135 | if($r->param('ID') == "new") { 136 | $class = $r->getVar('type'); 137 | if($class && class_exists($class) && is_subclass_of($class, DashboardPanel::class)) { 138 | /** @var DashboardPanel $panel */ 139 | $panel = new $class(); 140 | if($panel->canCreate()) { 141 | $panel->MemberID = Member::currentUserID(); 142 | $panel->Title = $panel->getLabel(); 143 | $panel->write(); 144 | } 145 | else { 146 | $panel = null; 147 | } 148 | } 149 | } 150 | else { 151 | $panel = DashboardPanel::get()->byID((int) $r->param('ID')); 152 | } 153 | if(isset($panel) && ($panel->canEdit() || $panel->canView())) { 154 | $requestClass = $panel->getRequestHandlerClass(); 155 | /** @var RequestHandler $handler */ 156 | $handler = Injector::inst()->create($requestClass, $this, $panel); 157 | return $handler->handleRequest($r); 158 | 159 | } 160 | return $this->httpError(404); 161 | } 162 | 163 | 164 | /** 165 | * A controller action that handles the reordering of the panels 166 | * 167 | * @param HTTPRequest $r 168 | * @return void 169 | * @throws \SilverStripe\ORM\ValidationException 170 | */ 171 | public function sort(HTTPRequest $r) { 172 | if($sort = $r->requestVar('dashboard-panel')) { 173 | foreach($sort as $index => $id) { 174 | if($panel = DashboardPanel::get()->byID((int) $id)) { 175 | if($panel->MemberID == Member::currentUserID()) { 176 | $panel->SortOrder = $index; 177 | $panel->write(); 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | 185 | /** 186 | * A controller action that handles setting the default dashboard configuration 187 | * 188 | * @param HTTPRequest The current request 189 | * @return HTTPResponse 190 | * @throws \SilverStripe\ORM\ValidationException 191 | */ 192 | public function setdefault(HTTPRequest $r) { 193 | /** @var DashboardPanel $panel */ 194 | foreach(SiteConfig::current_site_config()->DashboardPanels() as $panel) { 195 | $panel->delete(); 196 | } 197 | foreach(Security::getCurrentUser()->DashboardPanels() as $panel) { 198 | $clone = $panel->duplicate(); 199 | $clone->MemberID = 0; 200 | $clone->SiteConfigID = SiteConfig::current_site_config()->ID; 201 | $clone->write(); 202 | } 203 | return new HTTPResponse(_t('Dashboard.SETASDEFAULTSUCCESS','Success! This dashboard configuration has been set as the default for all new members.')); 204 | } 205 | 206 | 207 | /** 208 | * A controller action that handles the application of a dashboard configuration to all members 209 | * 210 | * @param HTTPRequest The current request 211 | * @return HTTPResponse 212 | * @throws \SilverStripe\ORM\ValidationException 213 | */ 214 | public function applytoall(HTTPRequest $r) { 215 | $members = Permission::get_members_by_permission(["CMS_ACCESS_Dashboard", "ADMIN"]); 216 | foreach($members as $member) { 217 | if($member->ID == Member::currentUserID()) continue; 218 | 219 | $member->DashboardPanels()->removeAll(); 220 | /** @var DashboardPanel $panel */ 221 | foreach(Security::getCurrentUser()->DashboardPanels() as $panel) { 222 | $clone = $panel->duplicate(); 223 | $clone->MemberID = $member->ID; 224 | $clone->write(); 225 | } 226 | } 227 | return new HTTPResponse(_t('Dashboard.APPLYTOALLSUCCESS','Success! This dashboard configuration has been applied to all members who have dashboard access.')); 228 | } 229 | 230 | 231 | 232 | 233 | /** 234 | * Gets the current user's dashboard configuration 235 | * 236 | * @return DataList 237 | */ 238 | public function BasePanels() { 239 | return Security::getCurrentUser()->DashboardPanels(); 240 | } 241 | 242 | /** 243 | * Gets the current user's dashboard configuration 244 | * 245 | * @return DataList 246 | */ 247 | public function Panels() { 248 | return Security::getCurrentUser()->DashboardPanels(); 249 | } 250 | 251 | 252 | 253 | 254 | /** 255 | * Gets all the available panels that can be installed on the dashboard. All subclasses of 256 | * {@link DashboardPanel} are included 257 | * 258 | * @return ArrayList 259 | */ 260 | public function AllPanels() { 261 | $set = ArrayList::create([]); 262 | $panels = ClassLoader::inst()->getManifest()->getDescendantsOf(DashboardPanel::class); 263 | if($this->config()->excluded_panels) { 264 | $panels = array_diff($panels,$this->config()->excluded_panels); 265 | } 266 | foreach($panels as $class) { 267 | $SNG = Injector::inst()->get($class); 268 | $SNG->Priority = Config::inst()->get($class, "priority"); 269 | if($SNG->registered() == true){ 270 | $set->push($SNG); 271 | } 272 | } 273 | return $set->sort("Priority"); 274 | } 275 | 276 | 277 | 278 | 279 | /** 280 | * A template accessor to check the ADMIN permission 281 | * 282 | * @return bool 283 | */ 284 | public function IsAdmin() { 285 | return Permission::check("ADMIN"); 286 | } 287 | 288 | 289 | 290 | /** 291 | * Check the permission to make sure the current user has a dashboard 292 | * 293 | * @return bool 294 | */ 295 | public function canView($member = null) { 296 | return Permission::check("CMS_ACCESS_Dashboard"); 297 | } 298 | 299 | 300 | 301 | /** 302 | * Check if the current user can add panels to the dashboard 303 | * 304 | * @return bool 305 | */ 306 | public function CanAddPanels() { 307 | return Permission::check("CMS_ACCESS_DashboardAddPanels"); 308 | } 309 | 310 | 311 | 312 | /** 313 | * Check if the current user can delete panels from the dashboard 314 | * 315 | * @return bool 316 | */ 317 | public function CanDeletePanels() { 318 | return Permission::check("CMS_ACCESS_DashboardDeletePanels"); 319 | } 320 | 321 | 322 | 323 | /** 324 | * Check if the current user can configure panels on the dashboard 325 | * 326 | * @return bool 327 | */ 328 | public function CanConfigurePanels() { 329 | return Permission::check("CMS_ACCESS_DashboardConfigurePanels"); 330 | } 331 | 332 | 333 | 334 | 335 | } 336 | 337 | 338 | 339 | /** 340 | * Defines the {@link RequestHandler} object that is responsible for rendering dashboard panels 341 | * and processing their input. 342 | * 343 | * @package Dashboard 344 | * @author Uncle Cheese 345 | */ 346 | class DashboardPanelRequest extends RequestHandler { 347 | 348 | 349 | 350 | private static $url_handlers = [ 351 | '$Action!' => '$Action', 352 | '' => 'panel' 353 | ]; 354 | 355 | private static $allowed_actions = [ 356 | "panel", 357 | "delete", 358 | "ConfigureForm", 359 | "saveConfiguration" 360 | ]; 361 | 362 | 363 | 364 | protected $dashboard; 365 | 366 | 367 | 368 | protected $panel; 369 | 370 | 371 | 372 | public function __construct(Dashboard $dashboard, DashboardPanel $panel) { 373 | $this->dashboard = $dashboard; 374 | $this->panel = $panel; 375 | parent::__construct(); 376 | } 377 | 378 | 379 | 380 | /** 381 | * Gets the link to this request. Useful for rendering the nested Form. Also provides an easy 382 | * "refresh" link to the panel that is managed by this request 383 | * 384 | * @param null $action Not in use 385 | * @return string 386 | */ 387 | public function Link($action=null) { 388 | return $this->dashboard->Link("panel/{$this->panel->ID}"); 389 | } 390 | 391 | 392 | /** 393 | * Renders the panel in this request 394 | * 395 | * @param HTTPRequest 396 | * @return \SilverStripe\ORM\FieldType\DBHTMLText 397 | * @throws \SilverStripe\Control\HTTPResponse_Exception 398 | */ 399 | public function panel(HTTPRequest $r) { 400 | if($this->panel->canView()) { 401 | return $this->panel->PanelHolder(); 402 | } 403 | return $this->httpError(403); 404 | } 405 | 406 | 407 | 408 | /** 409 | * Delets the panel in this request 410 | * 411 | * @param HTTPRequest 412 | * @return HTTPResponse 413 | */ 414 | public function delete(HTTPRequest $r) { 415 | if($this->panel->canDelete()) { 416 | $this->panel->delete(); 417 | return new HTTPResponse("OK"); 418 | } 419 | } 420 | 421 | 422 | 423 | /** 424 | * Gets the configuration form for this panel and handles the form input 425 | * 426 | * @return Form 427 | */ 428 | public function ConfigureForm() { 429 | $form = Form::create( 430 | $this, 431 | "ConfigureForm", 432 | $this->panel->getConfiguration(), 433 | FieldList::create( 434 | FormAction::create("saveConfiguration",_t('Dashboard.SAVE','Save')) 435 | ->setUseButtonTag(true) 436 | ->addExtraClass('btn btn-primary'), 437 | FormAction::create("cancel",_t('Dashboard.CANCEL','Cancel')) 438 | ->setUseButtonTag(true) 439 | ->addExtraClass('btn btn-secondary') 440 | ) 441 | ); 442 | $form->loadDataFrom($this->panel); 443 | $form->setHTMLID("Form_ConfigureForm_".$this->panel->ID); 444 | $form->addExtraClass("configure-form"); 445 | return $form; 446 | } 447 | 448 | 449 | 450 | 451 | /** 452 | * Processes the form input and writes the panel 453 | * 454 | * @param array $data The raw POST data from the form 455 | * @param Form $form The ConfigurationForm 456 | * @return HTTPResponse 457 | * @throws \SilverStripe\ORM\ValidationException 458 | */ 459 | public function saveConfiguration($data, $form) { 460 | $panel = $this->panel; 461 | $form->saveInto($panel); 462 | $panel->write(); 463 | } 464 | 465 | 466 | 467 | 468 | 469 | 470 | } 471 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {{description}} 294 | Copyright (C) {{year}} {{fullname}} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------