diff --git a/CHANGELOG.md b/CHANGELOG.md index 28175a0..c275930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v3.0.0 + +Experimental release giving two new features + +* User configurable and sharable dashboards +* New widget to share a user configurable datatable in a dashboard + +Further notes and documentation on these super cool features to come in a stable point release. + ## v2.0.7 * Add interception point for rendering dashboard widgets diff --git a/README.md b/README.md index c517bc3..1ad5923 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ This is an extension for PresideCMS that provides APIs and a methodolgy for crea Install with: -```box install preside-ext-admin-dashboards``` +``` +box install preside-ext-admin-dashboards +``` ## User stories @@ -16,6 +18,13 @@ The following user stories describe the functionality of this extension: * As an admin administrator, I want to be able to limit access to particular dashboard widgets so that I can protect sensitive data and functionality * As an admin user, I want to be able to configure widgets and have my configuration persisted so that I can customize my view of a dashboard +### 3.0.0 Experimental + +Version 3.0.0 release introduces some new undocumented features that will be fully released in a point release soon. The stories are: + +* As an admin user, I want to be able to create and share my own dashboards in order to create user and business specific dashboard views +* As an admin user, I want to be able to insert a datatable widget into dashboards, configuring the source object, fields shown and optional filters on the data + ## Rendering a dashboard Admin dashboards are made up of "widgets" that a user can configure to show stats, summaries, etc. To render a dashboard, you can use the `renderAdminDashboard()` helper, providing an _arbitrary but unique_ dashboard ID that identifies a unique dashboard, an array of widget IDs and an optional column count (default 2, valid options are 1, 2, 3 or 4): diff --git a/assets/.gitignore b/assets/.gitignore index 3c3629e..62c37a3 100644 --- a/assets/.gitignore +++ b/assets/.gitignore @@ -1 +1,5 @@ -node_modules +/node_modules +/css/**/*.less.css +/css/**/*.min.css +/js/**/*.min.js +/js/**/*.min.map diff --git a/assets/css/admin/specific/admindashboards/$dashboards.less.css b/assets/css/admin/specific/admindashboards/$dashboards.less.css deleted file mode 100644 index 1685139..0000000 --- a/assets/css/admin/specific/admindashboards/$dashboards.less.css +++ /dev/null @@ -1,4 +0,0 @@ -.widget-dynamic-content { - position: relative; - min-height: 20px; -} diff --git a/assets/css/admin/specific/admindashboards/_4309885c.admindashboards.min.css b/assets/css/admin/specific/admindashboards/_4309885c.admindashboards.min.css deleted file mode 100644 index 7053709..0000000 --- a/assets/css/admin/specific/admindashboards/_4309885c.admindashboards.min.css +++ /dev/null @@ -1 +0,0 @@ -.widget-dynamic-content{position:relative;min-height:20px} \ No newline at end of file diff --git a/assets/css/admin/specific/admindashboards/dashboards.less b/assets/css/admin/specific/admindashboards/dashboards.less index 0b7d05d..feff713 100644 --- a/assets/css/admin/specific/admindashboards/dashboards.less +++ b/assets/css/admin/specific/admindashboards/dashboards.less @@ -1,4 +1,60 @@ .widget-dynamic-content { position : relative; min-height : 20px; -} \ No newline at end of file +} + +.dashboard-column-content { + margin-bottom : 3em; + min-height : 3em; +} + +.dashboard-column-add { + text-align : center; +} + +.widget-placeholder { + margin-bottom : 20px; +} + +.admin-dashboard-widget.fullscreen { + + .chart-canvas { + max-height : ~"calc(100vh - 85px)"; + max-width : ~"calc(100vw - 46px)"; + } + +} + +.admin-dashboard-widget-picker { + + a { + padding : 10px 10px 10px 35px; + position : relative; + display : block; + margin-bottom : .5em; + border : 1px solid #eee; + border-radius : 5px; + + &:hover { + text-decoration : none; + background-color : #f9f9f9; + border-color : #ddd; + } + + .fa { + position : absolute; + left : 10px; + top : 14px; + } + + h4 { + margin : 0; + } + + p { + color : #393939; + margin : .5em 0 0; + } + } + +} diff --git a/assets/js/admin/specific/admindashboards/_7bf99e99.admindashboards.min.js b/assets/js/admin/specific/admindashboards/_7bf99e99.admindashboards.min.js deleted file mode 100644 index e7feeca..0000000 --- a/assets/js/admin/specific/admindashboards/_7bf99e99.admindashboards.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(a){var b,c,d,e,f,g=a(".admin-dashboard-container"),h=a(".admin-dashboard-container .admin-dashboard-widget");b=function(b){var e,f=buildAdminLink("admindashboards","configModal",d(b)),g={title:b.data("configModalTitle"),className:"full-screen-dialog",buttonList:["ok","cancel"]},h={onLoad:function(a){e=a},onok:function(){var f=a.extend({},e.getAdminDashboardWidgetConfig(),d(b),getWidgetContextData(b));a.ajax(buildAdminLink("admindashboards","saveWidgetConfig"),{data:f,complete:function(){c(b,!0)}})}},i=new PresideIframeModal(f,"100%","100%",h,g);i.open()},c=function(b,c){b.data("ajax")?(b.find(".widget-dynamic-content").presideLoadingSheen(!0),a.ajax(buildAdminLink("admindashboards","renderWidgetContent"),{data:a.extend({},d(b),getWidgetContextData(b)),success:function(a){e(b,a)},error:function(){f(b)},complete:function(){b.find(".widget-dynamic-content").presideLoadingSheen(!1)}})):c&&location.reload()},e=function(a,b){a.find(".widget-dynamic-content").html(b)},f=function(a){a.find(".widget-dynamic-content").html(a.closest(".admin-dashboard-container").find(".error-template").html())},d=function(a){return{widgetId:a.data("widgetId"),instanceId:a.data("instanceId"),configInstanceId:a.data("configInstanceId"),dashboardId:a.closest(".admin-dashboard-container").data("dashboardId")}},getWidgetContextData=function(a){var b=a.data("instanceId");return void 0!==b&&void 0!==cfrequest[b]?cfrequest[b]:{}},g.on("click",".admin-dashboard-widget .widget-configuration-link",function(){return b(a(this).closest(".admin-dashboard-widget")),!1}),h.each(function(){c(a(this),!1)})}(presideJQuery); -//# sourceMappingURL=_admindashboards.min.map \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/_admindashboards.min.map b/assets/js/admin/specific/admindashboards/_admindashboards.min.map deleted file mode 100644 index c216380..0000000 --- a/assets/js/admin/specific/admindashboards/_admindashboards.min.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["widgetBehaviour.js"],"names":["$","openWidgetConfigDialog","loadContent","getWidgetDetails","onWidgetContentFetchSuccess","onWidgetContentFetchError","$dashBoardContainer","$widgets","$widgetEl","dialogIframe","iframeSrc","buildAdminLink","modalOptions","title","data","className","buttonList","callbacks","onLoad","iframe","onok","config","extend","getAdminDashboardWidgetConfig","getWidgetContextData","ajax","complete","browserIframeModal","PresideIframeModal","open","refresh","find","presideLoadingSheen","success","error","location","reload","html","closest","widgetId","instanceId","configInstanceId","dashboardId","widgetInstanceId","cfrequest","on","this","each","presideJQuery"],"mappings":"CAAA,SAAYA,GAEX,GAEIC,GAAwBC,EAAaC,EACrCC,EAA6BC,EAH7BC,EAAsBN,EAAG,8BACzBO,EAAsBP,EAAG,qDAI7BC,GAAyB,SAAUO,GAClC,GAkBIC,GAlBAC,EAAkBC,eAAgB,kBAAmB,cAAeR,EAAkBK,IACtFI,GACFC,MAAaL,EAAUM,KAAM,oBAC7BC,UAAa,qBACbC,YAAe,KAAM,WAEnBC,GACFC,OAAS,SAAUC,GAAWV,EAAeU,GAC7CC,KAAO,WACN,GAAIC,GAASrB,EAAEsB,UAAYb,EAAac,gCAAiCpB,EAAkBK,GAAagB,qBAAsBhB,GAE9HR,GAAEyB,KAAMd,eAAgB,kBAAmB,qBACxCG,KAAWO,EACXK,SAAW,WAAaxB,EAAaM,GAAW,QAIlDmB,EAAqB,GAAIC,oBAAoBlB,EAAW,OAAQ,OAAQO,EAAWL,EAGvFe,GAAmBE,QAGpB3B,EAAc,SAAUM,EAAWsB,GAC7BtB,EAAUM,KAAM,SACpBN,EAAUuB,KAAM,2BAA4BC,qBAAqB,GAEjEhC,EAAEyB,KAAMd,eAAgB,kBAAmB,wBACxCG,KAAWd,EAAEsB,UAAYnB,EAAkBK,GAAagB,qBAAsBhB,IAC9EyB,QAAW,SAAUnB,GAASV,EAA6BI,EAAWM,IACtEoB,MAAW,WAAa7B,EAA2BG,IACnDkB,SAAW,WAAalB,EAAUuB,KAAM,2BAA4BC,qBAAqB,OAEjFF,GACXK,SAASC,UAIXhC,EAA8B,SAAUI,EAAWM,GAClDN,EAAUuB,KAAM,2BAA4BM,KAAMvB,IAGnDT,EAA4B,SAAUG,GACrCA,EAAUuB,KAAM,2BAA4BM,KAAM7B,EAAU8B,QAAS,8BAA+BP,KAAM,mBAAmBM,SAG9HlC,EAAmB,SAAUK,GAC5B,OACG+B,SAAW/B,EAAUM,KAAM,YAC3B0B,WAAahC,EAAUM,KAAM,cAC7B2B,iBAAmBjC,EAAUM,KAAM,oBACnC4B,YAAclC,EAAU8B,QAAS,8BAA+BxB,KAAM,iBAG1EU,qBAAuB,SAAUhB,GAChC,GAAImC,GAAmBnC,EAAUM,KAAM,aACvC,YAAiC,KAArB6B,OAA6E,KAAlCC,UAAWD,GAC1DC,UAAWD,OAKpBrC,EAAoBuC,GAAI,QAAS,qDAAsD,WAGtF,MAFA5C,GAAwBD,EAAG8C,MAAOR,QAAS,6BAEpC,IAGR/B,EAASwC,KAAM,WAAY7C,EAAaF,EAAG8C,OAAQ,MAE/CE","file":"_admindashboards.min.js"} \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/configmodal/_37ac76ae.configmodal.min.js b/assets/js/admin/specific/admindashboards/configmodal/_37ac76ae.configmodal.min.js deleted file mode 100644 index 18b59fd..0000000 --- a/assets/js/admin/specific/admindashboards/configmodal/_37ac76ae.configmodal.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(a){window.getAdminDashboardWidgetConfig=function(){return a("#admin-dashboard-widget-config-form").serializeObject()}}(presideJQuery); -//# sourceMappingURL=_configmodal.min.map \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/configmodal/_configmodal.min.map b/assets/js/admin/specific/admindashboards/configmodal/_configmodal.min.map deleted file mode 100644 index efc00f6..0000000 --- a/assets/js/admin/specific/admindashboards/configmodal/_configmodal.min.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["modalConfig.js"],"names":["$","window","getAdminDashboardWidgetConfig","serializeObject","presideJQuery"],"mappings":"CAAA,SAAYA,GACXC,OAAOC,8BAAgC,WACtC,MAAOF,GAAG,uCAAwCG,oBAE/CC","file":"_configmodal.min.js"} \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/editing/editing.js b/assets/js/admin/specific/admindashboards/editing/editing.js new file mode 100644 index 0000000..da4771b --- /dev/null +++ b/assets/js/admin/specific/admindashboards/editing/editing.js @@ -0,0 +1,25 @@ +( function( $ ){ + + $( ".dashboard-column-content" ).sortable( { + connectWith : ".dashboard-column-content" + , handle : ".widget-header" + , placeholder : "widget-placeholder" + , forcePlaceholderSize : true + , tolerance : "pointer" + , update : function( event, ui ) { + if ( ui.sender ) return; + var element = ui.sender || ui.item + , dashboardId = element.closest( ".admin-dashboard-container" ).data( "dashboardId" ) + , column = element.closest( ".dashboard-column" ).data( "column" ) + , widgets = element.closest( ".ui-sortable" ).sortable( "toArray", { attribute: "data-config-instance-id" } ); + + if ( widgets.length ) { + $.ajax( buildAdminLink( "admindashboards", "updateWidgetOrder" ), { + data : { dashboardId:dashboardId, column:column, widgets:widgets } + , method : "POST" + } ); + } + } + } ); + +} )( presideJQuery ); \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/sharing/sharingForm.js b/assets/js/admin/specific/admindashboards/sharing/sharingForm.js new file mode 100644 index 0000000..fef6165 --- /dev/null +++ b/assets/js/admin/specific/admindashboards/sharing/sharingForm.js @@ -0,0 +1,22 @@ +( function( $ ){ + + var $viewAccess = $( "[name=view_access]" ) + , $editAccess = $( "[name=edit_access]" ); + + if ( $viewAccess.length ) { + var $viewAccessRelated = $viewAccess.closest( ".form-group" ).siblings( ".form-group" ); + + $viewAccess.on( "change", function(){ + $viewAccessRelated.toggle( $viewAccess.filter( ":checked" ).val() == "specific" ); + } ).trigger( "change" ); + } + + if ( $editAccess.length ) { + var $editAccessRelated = $editAccess.closest( ".form-group" ).siblings( ".form-group" ); + + $editAccess.on( "change", function(){ + $editAccessRelated.toggle( $editAccess.filter( ":checked" ).val() == "specific" ); + } ).trigger( "change" ); + } + +} )( presideJQuery ); \ No newline at end of file diff --git a/assets/js/admin/specific/admindashboards/widgetBehaviour.js b/assets/js/admin/specific/admindashboards/widgetBehaviour.js index 88dd958..91159bb 100644 --- a/assets/js/admin/specific/admindashboards/widgetBehaviour.js +++ b/assets/js/admin/specific/admindashboards/widgetBehaviour.js @@ -19,7 +19,13 @@ $.ajax( buildAdminLink( "admindashboards", "saveWidgetConfig" ), { data : config - , complete : function() { loadContent( $widgetEl, true ); } + , method : "POST" + , complete : function() { + loadContent( $widgetEl, true ); + if ( config.widget_title ) { + $( ".widget-title span", $widgetEl ).text( config.widget_title ); + } + } } ); } } @@ -46,6 +52,11 @@ onWidgetContentFetchSuccess = function( $widgetEl, data ){ $widgetEl.find( ".widget-dynamic-content" ).html( data ); + + var callback = $widgetEl.data( "ajaxCallback" ); + if ( callback.length ) { + window[ callback ](); + } }; onWidgetContentFetchError = function( $widgetEl ){ @@ -74,6 +85,50 @@ return false; } ); + $dashBoardContainer.on( "click", ".admin-dashboard-widget .widget-fullscreen-link", function(){ + $( this ).closest( ".widget-box" ).toggleClass( "fullscreen" ); + + return false; + } ); + + $dashBoardContainer.on( "click", ".admin-dashboard-widget .widget-delete-link", function( e ){ + e.preventDefault(); + + var $link = $( this ) + , title = "" + , $widgetEl = $link.closest( ".admin-dashboard-widget" ); + + if ( !$link.data( "confirmationPrompt" ) ) { + title = $link.data( "title" ) || $link.attr( "title" ); + title = title.charAt( 0 ).toLowerCase() + title.slice( 1 ); + $link.data( "confirmationPrompt", i18n.translateResource( "cms:confirmation.prompt", { data:[ title ] } ) ); + } + + var $message = $( "
" ) + , $input = $( "" ); + + var confirmationDialog = presideBootbox.dialog( { + title : "Confirmation" + , message : $message + , buttons : { + cancel : { + label: i18n.translateResource( "cms:confirmation.prompt.cancel.button" ) + } + , confirm : { + label: i18n.translateResource( "cms:confirmation.prompt.confirm.button" ) + , callback: function() { + $widgetEl.find( ".widget-dynamic-content" ).presideLoadingSheen( true ); + $.ajax( $link.attr( "href" ), { + success : function() { $widgetEl.remove(); } + , error : function() { $widgetEl.find( ".widget-dynamic-content" ).presideLoadingSheen( false ); } + , complete : function() { confirmationDialog.modal( "hide" ); } + } ); + } + } + } + } ); + } ); + $widgets.each( function(){ loadContent( $( this ), false ); } ); } )( presideJQuery ); \ No newline at end of file diff --git a/box.json b/box.json index 08b0ba7..9dc2ee3 100644 --- a/box.json +++ b/box.json @@ -17,5 +17,13 @@ ".gitignore", ".twgit_features_subject", "jmimemagic.log" - ] + ], + "preside":{ + "dependencies":{ + "preside-ext-chart-js":{ + "installVersion":"^0.2.2", + "minVersion":"0.2.2" + } + } + } } diff --git a/config/Config.cfc b/config/Config.cfc index 2ba2f3d..2c27eef 100644 --- a/config/Config.cfc +++ b/config/Config.cfc @@ -1,9 +1,33 @@ component { public void function configure( required struct config ) { - var conf = arguments.config; + var conf = arguments.config; + var settings = conf.settings ?: {}; + _setupEnums( settings ); + _setupPermissionsAndRoles( settings ); _setupInterceptors( conf ); + + settings.adminConfigurationMenuItems.append( "adminDashboards" ); + } + + private void function _setupEnums( settings ) { + settings.enum.adminDashboardViewAccess = [ "private", "public", "specific" ]; + settings.enum.adminDashboardEditAccess = [ "private", "specific" ]; + } + + private void function _setupPermissionsAndRoles( required struct settings ) { + settings.adminPermissions.adminDashboards = [ "navigate", "read", "add", "edit", "clone", "delete" ]; + + if ( IsArray( settings.adminRoles.sysadmin ?: "" ) ) { + ArrayAppend( settings.adminRoles.sysadmin, "adminDashboards.*" ); + } + if ( IsArray( settings.adminRoles.contentadmin ?: "" ) ) { + ArrayAppend( settings.adminRoles.contentadmin, "adminDashboards.*" ); + } + if ( IsArray( settings.adminRoles.contenteditor ?: "" ) ) { + ArrayAppend( settings.adminRoles.contenteditor, "adminDashboards.*" ); + } } // PRIVATE HELPERS diff --git a/forms/admin/admindashboards/config.xml b/forms/admin/admindashboards/config.xml new file mode 100644 index 0000000..4a6323c --- /dev/null +++ b/forms/admin/admindashboards/config.xml @@ -0,0 +1,8 @@ + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/forms/admin/admindashboards/widget/DashboardDataFilter.xml b/forms/admin/admindashboards/widget/DashboardDataFilter.xml new file mode 100644 index 0000000..58c52ff --- /dev/null +++ b/forms/admin/admindashboards/widget/DashboardDataFilter.xml @@ -0,0 +1,10 @@ + +
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/forms/preside-objects/admin_dashboard.xml b/forms/preside-objects/admin_dashboard.xml new file mode 100644 index 0000000..c169031 --- /dev/null +++ b/forms/preside-objects/admin_dashboard.xml @@ -0,0 +1,10 @@ + +
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/forms/preside-objects/admin_dashboard/sharing.xml b/forms/preside-objects/admin_dashboard/sharing.xml new file mode 100644 index 0000000..6153f2a --- /dev/null +++ b/forms/preside-objects/admin_dashboard/sharing.xml @@ -0,0 +1,15 @@ + +
+ +
+ + + +
+
+ + + +
+
+
\ No newline at end of file diff --git a/handlers/admin/AdminDashboards.cfc b/handlers/admin/AdminDashboards.cfc index 1fd9370..0255edb 100644 --- a/handlers/admin/AdminDashboards.cfc +++ b/handlers/admin/AdminDashboards.cfc @@ -1,6 +1,7 @@ component extends="preside.system.base.AdminHandler" { property name="widgetService" inject="adminDashboardWidgetService"; + property name="siteService" inject="siteService"; public void function renderWidgetContent( event, rc, prc ) { var widgetId = rc.widgetId ?: ""; @@ -75,5 +76,108 @@ component extends="preside.system.base.AdminHandler" { return renderView( view="/admin/admindashboards/_dashboard", args={ widgets=renderedWidgets, dashboardId=args.dashboardId ?: "" } ); } + private string function renderUserGeneratedDashboard( event, rc, prc, args={} ) { + var dashboardId = args.dashboardId ?: ""; + var allowEditing = isTrue( args.allowEditing ?: "" ); + var dashboard = widgetService.renderUserGeneratedDashboard( dashboardId=dashboardId, allowEditing=allowEditing ); + event.include( "/js/admin/specific/admindashboards/" ) + .include( "/css/admin/specific/admindashboards/" ); + + if ( isTrue( dashboard.canEdit ?: "" ) ) { + event.include( "/js/admin/specific/admindashboards/editing/" ); + } + + return renderView( view="/admin/admindashboards/_userGenerated", args=dashboard ); + } + + public void function widgetDialog( event, rc, prc ) { + event.setLayout( "adminModalDialog" ); + prc.widgets = _getSortedAndTranslatedAdminWidgets(); + + event.setView( view="admin/admindashboards/browserDialog", nolayout=true ); + } + + public function addWidget( event, rc, prc, args={} ) { + var dashboardId = rc.dashboard ?: ""; + var widgetId = rc.widget ?: ""; + var column = rc.column ?: 1; + var instanceId = createUUID(); + var nextSlot = widgetService.nextWidgetSlot( dashboardId, column ); + var title = widgetService.getInstanceTitle( dashboardId, widgetId ); + + widgetService.addWidget( + dashboardId = dashboardId + , widgetId = widgetId + , instanceId = instanceId + , column = column + , slot = nextSlot + , title = title + , config = {} + ); + + setNextEvent( url=event.buildAdminLink( objectName="admin_dashboard", recordId=dashboardId ) ); + } + + public function deleteWidget( event, rc, prc, args={} ) { + var dashboardId = args.dashboardId ?: ( rc.dashboardId ?: "" ); + var instanceId = args.instanceId ?: ( rc.instanceId ?: "" ); + + widgetService.deleteWidget( + dashboardId = dashboardId + , instanceId = instanceId + ); + + setNextEvent( url=event.buildAdminLink( objectName="admin_dashboard", recordId=dashboardId ) ); + } + + public string function updateWidgetOrder( event, rc, prc, args={} ) { + var dashboardId = rc.dashboardId ?: ""; + var column = rc.column ?: 1; + var widgets = rc.widgets ?: []; + var dao = getPresideObject( "admin_dashboard_widget" ); + + widgets.each( function( widget, slot ) { + dao.updateData( + filter = { dashboard=dashboardId, instance_id=widget } + , data = { column=column, slot=slot } + ); + } ); + + return "OK"; + } + + +// private helpers + + private query function _getSortedAndTranslatedAdminWidgets() { + // todo, cache this operation (per locale) + var unsortedOrTranslated = widgetService.getWidgets(); + var tempArray = []; + var activeSiteTemplate = siteService.getActiveSiteTemplate(); + var isUserDashboardWidgetHandler = ""; + var isUserDashboardWidget = false; + + for( var id in unsortedOrTranslated ) { + var widget = Duplicate( unsortedOrTranslated[ id ] ); + + if ( widget.siteTemplates == "*" || ListFindNoCase( widget.siteTemplates, activeSiteTemplate ) ) { + if ( ! widgetService.isUserDashboardWidget( widget.id ) ) { + continue; + } + + widget.title = translateResource( uri=widget.title , defaultValue=widget.title ); + widget.description = translateResource( uri=widget.description, defaultValue="" ); + widget.icon = translateResource( uri=widget.icon , defaultValue="fa-magic" ); + + tempArray.append( widget ); + } + } + + tempArray.sort( function( widget1, widget2 ){ + return widget1.title == widget2.title ? 0 : ( widget1.title > widget2.title ? 1 : -1 ); + } ); + + return arrayOfStructsToQuery( "id,title,description,icon", tempArray ); + } } \ No newline at end of file diff --git a/handlers/admin/admindashboards/widget/DashboardDataFilter.cfc b/handlers/admin/admindashboards/widget/DashboardDataFilter.cfc new file mode 100644 index 0000000..288af8f --- /dev/null +++ b/handlers/admin/admindashboards/widget/DashboardDataFilter.cfc @@ -0,0 +1,90 @@ +component extends="preside.system.base.AdminHandler" { + + property name="datamanagerService" inject="datamanagerService"; + property name="presideObjectService" inject="presideObjectService"; + + private string function render( event, rc, prc, args={} ) { + var objectName = args.config.applies_to ?: ""; + var savedFilter = args.config.filter ?: ""; + + if ( isEmptyString( objectName ) ) { + return ""; + } + + var gridFields = listToArray( args.config.grid_fields ?: "" ); + var sortableFields = presideObjectService.getObjectAttribute( + objectName = objectName + , attributeName = "datamanagerSortableFields" + , defaultValue = "" + ); + + if ( !len( gridFields ) ) { + gridFields = datamanagerService.defaultGridFields( objectName ); + } + + gridFields = arrayFilter( gridFields, function( item ) { + return !isEmptyString( item ); + }); + + return renderView( view="/admin/datamanager/_objectDataTable", args={ + objectName = objectName + , useMultiActions = false + , allowSearch = false + , allowFilter = false + , datasourceUrl = event.buildAdminLink( linkTo="ajaxProxy", queryString="action=admindashboards.widget.DashboardDataFilter.getFilterResultsForAjaxDatatables&objectName=#objectName#&gridFields=#arrayToList( gridFields )#&sSavedFilterExpressions=#savedFilter#" ) + , gridFields = gridFields + , sortableFields = listToArray( sortableFields ) + , filterContextData = { widgetInstanceId=args.instanceId ?: "" } + } ); + } + + private boolean function isUserDashboardWidget( event, rc, prc, args={} ) { + return true; + } + + private string function ajaxCallback( event, rc, prc, args={} ) { + return "widget_#replace( args.configInstanceId, "-", "", "all" )#_init"; + } + private void function ajaxIncludes( event, rc, prc, args={} ) { + event.include( "/js/admin/specific/datamanager/object/"); + event.include( "/css/admin/specific/datamanager/object/"); + event.includeData( data={ defaultPageLength=5 } ); + + event.includeInlineJs( "( function( $ ){ widget_#replace( args.configInstanceId, "-", "", "all" )#_init = function() { $( 'div[data-config-instance-id=#args.configInstanceId#].admin-dashboard-widget .object-listing-table' ).dataListingTable(); }; } )( presideJQuery );" ); + } + + public void function getFilterResultsForAjaxDatatables( event, rc, prc ) { + if ( isEmptyString( rc.objectName ?: "" ) ) { + return ""; + } + + runEvent( + event = "admin.DataManager._getObjectRecordsForAjaxDataTables" + , prePostExempt = true + , private = true + , includeActions = false + , eventArguments = { + object = rc.objectName + , actionsView = "/admin/admindashboards/widget/dashboardDataFilter/_gridActions" + , gridFields = rc.gridFields + } + ); + } + + public void function getObjectGridFieldsForAjaxControl( event, rc, prc ) { + var result = []; + + if ( !isEmptyString( rc.object ?: "" ) ) { + var fields = datamanagerService.listGridFields( rc.object ); + + for( var field in fields ){ + arrayAppend( result, { + value = field + , text = translatePropertyName( rc.object, field ) + } ); + } + } + + event.renderData( type="json", data=result );; + } +} \ No newline at end of file diff --git a/handlers/admin/datamanager/admin_dashboard.cfc b/handlers/admin/datamanager/admin_dashboard.cfc new file mode 100644 index 0000000..fb70e69 --- /dev/null +++ b/handlers/admin/datamanager/admin_dashboard.cfc @@ -0,0 +1,250 @@ +component extends="preside.system.base.AdminHandler" { + + property name="dashboardService" inject="adminDashboardService"; + property name="widgetService" inject="adminDashboardWidgetService"; + property name="datamanagerService" inject="datamanagerService"; + property name="presideObjectService" inject="presideObjectService"; + + private boolean function checkPermission( event, rc, prc, args={} ) { + var adminUserId = event.getAdminUserId(); + var recordId = prc.recordId ?: ""; + var objectName = args.object ?: ""; + var allowedOps = datamanagerService.getAllowedOperationsForObject( objectName ); + var disallowedOps = presideObjectService.getObjectAttribute( attributeName="datamanagerDisallowedOperations", objectName=objectName ); + var permissionBase = "adminDashboards"; + var alwaysDisallowed = ListToArray( ListAppend( disallowedOps, "manageContextPerms" ) ); + var permissionKey = "adminDashboards.#args.key#"; + var hasPermission = !alwaysDisallowed.find( args.key ) + && allowedOps.find( args.key ) + && hasCmsPermission( permissionKey ); + + if ( hasPermission && len( recordId ) ) { + switch( args.key ) { + case "read": + case "clone": + hasPermission = dashboardService.userCanViewDashboard( recordId, adminUserId ); + break; + case "edit": + hasPermission = dashboardService.userCanEditDashboard( recordId, adminUserId ); + break; + case "delete": + hasPermission = dashboardService.userCanDeleteDashboard( recordId, adminUserId ); + break; + } + } + + if ( !hasPermission && IsTrue( args.throwOnError ?: "" ) ) { + event.adminAccessDenied(); + } + + return hasPermission; + } + + private void function preFetchRecordsForGridListing( event, rc, prc, args={} ) { + var adminUserId = event.getAdminUserId(); + var adminUserGroups = _getAdminUserGroups( adminUserId ); + + args.extraFilters = args.extraFilters ?: []; + args.extraFilters.append( { + filter = "view_access = 'public' + or admin_dashboard.owner = :adminUserId + or ( view_access = 'specific' and ( view_users.id = :adminUserId or view_groups.id in ( :adminUserGroups ) ) ) + or ( edit_access = 'specific' and ( edit_users.id = :adminUserId or edit_groups.id in ( :adminUserGroups ) ) )" + , filterParams = { + adminUserId = { type="varchar", value=adminUserId } + , adminUserGroups = { type="varchar", value=adminUserGroups, list=true } + } + } ); + } + + private void function postFetchRecordsForGridListing( event, rc, prc, args={} ) { + var records = args.records ?: QueryNew( '' ); + var adminUserId = event.getAdminUserId(); + var adminUserGroups = _getAdminUserGroups( adminUserId ); + + var canView = []; + var canEdit = []; + var canShare = []; + var canDelete = []; + var canViewThis = false; + var canEditThis = false; + + for( var r in records ){ + canEditThis = r.owner_id == adminUserId || ( r.edit_access == "specific" && ( listFind( r.edit_users_list, adminUserId ) || _listFindOneOf( r.edit_groups_list, adminUserGroups ) ) ); + canViewThis = canEditThis || r.view_access == "public" || ( r.view_access == "specific" && ( listFind( r.view_users_list, adminUserId ) || _listFindOneOf( r.view_groups_list, adminUserGroups ) ) ) + canEdit.append( canEditThis ); + canView.append( canViewThis ); + canShare.append( r.owner_id == adminUserId ); + canDelete.append( r.owner_id == adminUserId ); + } + + QueryAddColumn( records, "canView", canView ); + QueryAddColumn( records, "canEdit", canEdit ); + QueryAddColumn( records, "canShare", canShare ); + QueryAddColumn( records, "canDelete", canDelete ); + } + + private array function getRecordActionsForGridListing( event, rc, prc, args={} ) { + var objectName = args.objectName ?: ""; + var record = args.record ?: {}; + var recordId = record.id ?: ""; + + var actions = []; + + if ( isTrue( record.canView ) ) { + actions.append( { + link = event.buildAdminLink( objectName=objectName, recordid=recordId ) + , icon = "fa-eye" + , class = "" + , contextKey = "v" + } ); + } else { + actions.append( '' ); + } + if ( isTrue( record.canEdit ) ) { + actions.append( { + link = event.buildAdminLink( objectName=objectName, recordid=recordId, operation="editRecord" ) + , icon = "fa-pencil" + , class = "" + , contextKey = "e" + } ); + } else { + actions.append( '' ); + } + if ( isTrue( record.canView ) ) { + actions.append( { + link = event.buildAdminLink( objectName=objectName, recordid=recordId, operation="cloneRecord" ) + , icon = "fa-clone" + , class = "" + , contextKey = "c" + } ); + } else { + actions.append( '' ); + } + if ( isTrue( record.canDelete ) ) { + actions.append( { + link = event.buildAdminLink( objectName=objectName, recordid=recordId, operation="deleteRecordAction" ) + , icon = "fa-trash-o" + , class = "confirmation-prompt" + , title = translateResource( uri="cms:datamanager.deleteRecord.prompt", data=[ prc.objectTitle, record.name ] ) + , contextKey = "d" + } ); + } else { + actions.append( '' ); + } + + return actions; + } + + private void function extraTopRightButtonsForViewRecord( event, rc, prc, args={} ) { + var objectName = args.objectName ?: ""; + var recordId = prc.recordId ?: ""; + var record = prc.record ?: {}; + args.actions = args.actions ?: []; + + if ( dashboardService.userCanShareDashboard( recordId, event.getAdminUserId() ) ) { + args.actions.prepend( { + link = event.buildAdminLink( objectName=objectName, operation="sharing", recordId=recordId ) + , btnClass = "btn-default" + , iconClass = "fa-users" + , title = translateResource( "preside-objects.admin_dashboard:sharing.btn" ) + } ); + } + } + + private string function renderRecord( event, rc, prc, args={} ) { + prc.pageTitle = prc.recordLabel ?: prc.pageTitle; + prc.pageSubtitle = len( prc.recordLabel ?: "" ) ? "" : prc.pageSubtitle; + + return renderView( view="/admin/adminDashboards/recordView", args=args ); + } + + private void function preCloneRecordAction( event, rc, prc, args={} ) { + args.formData.view_access = "private"; + args.formData.edit_access = "private"; + } + + public void function sharing() { + var recordId = rc.id ?: ""; + + event.initializeDatamanagerPage( + objectName = "admin_dashboard" + , recordId = recordId + ); + + if ( !dashboardService.userCanShareDashboard( recordId, event.getAdminUserId() ) ) { + event.accessDenied(); + } + + event.addAdminBreadCrumb( + title = translateResource( "preside-objects.admin_dashboard:sharing.breadcrumb.title" ) + , link = "" + ); + event.include( "/js/admin/specific/admindashboards/sharing/" ); + + prc.savedData = queryGetRow( prc.record, prc.record.recordcount ); + prc.formName = "preside-objects.admin_dashboard.sharing"; + prc.pageTitle = translateResource( "preside-objects.admin_dashboard:sharing.page.title" ); + prc.pageSubTitle = translateResource( "preside-objects.admin_dashboard:sharing.page.subtitle" ); + } + + private string function buildSharingLink( event, rc, prc, args={} ) { + var qs = "id=#( args.recordId ?: "" )#"; + + if ( Len( Trim( args.queryString ?: "" ) ) ) { + qs &= "&#args.queryString#"; + } + + return event.buildAdminLink( linkto="datamanager.admin_dashboard.sharing", querystring=qs ); + } + + public void function sharingAction( event, rc, prc, args={} ) { + var recordId = rc.id ?: ""; + + if ( !dashboardService.userCanEditDashboard( recordId, event.getAdminUserId() ) ) { + event.accessDenied(); + } + + if ( ( rc.view_access ?: "" ) != "specific" ) { + rc.view_groups = ""; + rc.view_users = ""; + } + if ( ( rc.edit_access ?: "" ) != "specific" ) { + rc.edit_groups = ""; + rc.edit_users = ""; + } + + runEvent( + event = "admin.datamanager._editRecordAction" + , prepostExempt = true + , private = true + , eventArguments = { + object = "admin_dashboard" + , formName = "preside-objects.admin_dashboard.sharing" + , errorUrl = event.buildAdminLink( objectName="admin_dashboard", recordId=recordId, operation="sharing" ) + , successUrl = event.buildAdminLink( objectName="admin_dashboard", recordId=recordId ) + , audit = true + , auditAction = "edit_sharing_options" + } + ); + } + + +// PRIVATE HELPER METHODS + private boolean function _listFindOneOf( required string list1, required string list2 ) { + for( var item in listToArray( arguments.list1 ) ) { + if ( listFind( arguments.list2, item ) ) { + return true; + } + } + return false; + } + + private string function _getAdminUserGroups( required string adminUserId ) { + return getPresideObject( "security_group" ).selectData( + filter = { "users.id"=arguments.adminUserId } + , selectFields = [ "id" ] + ).valueList( "id" ); + } + +} diff --git a/handlers/formcontrols/FilterWidgetFilterPicker.cfc b/handlers/formcontrols/FilterWidgetFilterPicker.cfc new file mode 100644 index 0000000..59b736f --- /dev/null +++ b/handlers/formcontrols/FilterWidgetFilterPicker.cfc @@ -0,0 +1,45 @@ +component { + + public string function index( event, rc, prc, args={} ) { + var filterObject = args.filterObject ?: ( args.savedData[ listFirst( args.filterBy ) ] ?: "" ); + + var multiple = IsTrue( args.multiple ?: "" ); + var prefetchCacheBuster = CreateUUId(); + var contextData = UrlEncodedFormat( SerializeJson( args.rulesEngineContextData ?: {} ) ); + var preSavedFilters = args.preSavedFilters ?: ""; + var preRulesEngineFilters = args.preRulesEngineFilters ?: ""; + + args.object = "rules_engine_condition"; + args.labelrenderer = "rules_engine_condition"; + args.remoteUrl = event.buildAdminLink( + linkTo = "rulesengine.getFiltersForAjaxSelectControl" + , querystring = "q=%QUERY" + ); + args.prefetchUrl = event.buildAdminLink( + linkTo = "rulesengine.getFiltersForAjaxSelectControl" + , querystring = "maxRows=100&prefetchCacheBuster=#prefetchCacheBuster#" + ); + args.placeholder = args.placeholder ?: "cms:rulesengine.filterPicker.placeholder" + + args.quickAdd = IsTrue( args.quickAdd ?: "" ) && hasCmsPermission( "rulesengine.add" ); + args.quickEdit = IsTrue( args.quickEdit ?: "" ) && hasCmsPermission( "rulesengine.edit" ); + + if ( args.quickAdd ) { + args.quickAddUrl = event.buildAdminLink( + linkTo = "rulesEngine.quickAddFilterForm" + , querystring = "multiple=#multiple#&contextData=#contextData#&preSavedFilters=#preSavedFilters#&preRulesEngineFilters=#preRulesEngineFilters#" + ); + } + if ( args.quickEdit ) { + args.quickEditUrl = event.buildAdminLink( + linkTo = "rulesEngine.quickEditFilterForm" + , querystring = "multiple=#multiple#&contextData=#contextData#&preSavedFilters=#preSavedFilters#&preRulesEngineFilters=#preRulesEngineFilters#&id=" + ); + } + + args.hasQuickAddPermission = booleanFormat( hasCmsPermission( "rulesEngine.add" ) ); + args.hasQuickEditPermission = booleanFormat( hasCmsPermission( "rulesEngine.edit" ) ); + + return renderViewlet( event="formcontrols.objectPicker.index", args=args ); + } +} \ No newline at end of file diff --git a/handlers/formcontrols/FilterWidgetObjectFieldPicker.cfc b/handlers/formcontrols/FilterWidgetObjectFieldPicker.cfc new file mode 100644 index 0000000..e16ff4d --- /dev/null +++ b/handlers/formcontrols/FilterWidgetObjectFieldPicker.cfc @@ -0,0 +1,22 @@ +component { + property name="datamanagerService" inject="datamanagerService"; + + public string function index( event, rc, prc, args={} ) { + var objectName = args.object ?: ( args.savedData[ "applies_to" ] ?: "" ); + + args.values = [ "" ]; + args.labels = [ "" ]; + args.multiple = true; + + args.remoteUrl = event.buildAdminLink( + linkTo = "ajaxProxy" + , querystring = "action=admindashboards.widget.DashboardDataFilter.getObjectGridFieldsForAjaxControl" + ); + args.prefetchUrl = event.buildAdminLink( + linkTo = "ajaxProxy" + , querystring = "action=admindashboards.widget.DashboardDataFilter.getObjectGridFieldsForAjaxControl" + ); + + return renderView( view="formcontrols/objectPicker/index", args=args ); + } +} \ No newline at end of file diff --git a/handlers/formcontrols/FilterWidgetObjectPicker.cfc b/handlers/formcontrols/FilterWidgetObjectPicker.cfc new file mode 100644 index 0000000..d0122c1 --- /dev/null +++ b/handlers/formcontrols/FilterWidgetObjectPicker.cfc @@ -0,0 +1,19 @@ +component { + property name="presideObjectService" inject="presideObjectService"; + + public string function index( event, rc, prc, args={} ) { + args.values = [ "" ]; + args.labels = [ "" ]; + + var objects = presideObjectService.listObjects(); + + for( var object in objects ) { + if ( !presideObjectService.isPageType( object ) ) { + args.values.append( object ); + args.labels.append( translateObjectName( object ) ); + } + } + + return renderView( view="formcontrols/select/index", args=args ); + } +} \ No newline at end of file diff --git a/handlers/generators/AdminDashboard.cfc b/handlers/generators/AdminDashboard.cfc new file mode 100644 index 0000000..d966482 --- /dev/null +++ b/handlers/generators/AdminDashboard.cfc @@ -0,0 +1,7 @@ +component { + + private string function owner( event, rc, prc, args={} ) { + return event.getAdminUserId(); + } + +} \ No newline at end of file diff --git a/helpers/adminDashboardsHelpers.cfm b/helpers/adminDashboardsHelpers.cfm index fb5b7a9..780fea7 100644 --- a/helpers/adminDashboardsHelpers.cfm +++ b/helpers/adminDashboardsHelpers.cfm @@ -1,5 +1,10 @@ + - return getController().renderViewlet( event="admin.admindashboards.renderDashboard", args=arguments ); + if ( arguments.userGenerated ) { + return getController().renderViewlet( event="admin.admindashboards.renderUserGeneratedDashboard", args=arguments ); + } else { + return getController().renderViewlet( event="admin.admindashboards.renderDashboard", args=arguments ); + } \ No newline at end of file diff --git a/i18n/admin/admindashboards/widget/dashboardDataFilter.properties b/i18n/admin/admindashboards/widget/dashboardDataFilter.properties new file mode 100644 index 0000000..9feeb3b --- /dev/null +++ b/i18n/admin/admindashboards/widget/dashboardDataFilter.properties @@ -0,0 +1,7 @@ +title=Filter results +iconClass=fa-list blue +description=Display a grid of relevant information + +field.applies_to.title=Applies to +field.filter.title=Filter +field.grid_fields.title=Grid fields \ No newline at end of file diff --git a/i18n/admindashboards.properties b/i18n/admindashboards.properties index 05804fd..04a7fb4 100644 --- a/i18n/admindashboards.properties +++ b/i18n/admindashboards.properties @@ -1,2 +1,8 @@ widget.failed.to.load.message=There was an error loading your dashboard content. Please try again or report the error to your administrators. -configure.widget.dialog.title=Configure dashboard widget: {1} \ No newline at end of file +configure.widget.dialog.title=Configure dashboard widget: {1} +configform.widget_title.label=Widget title + +no.config=This widget has not yet been configured. +configure.widget.title=Configure this widget +fullscreen.widget.title=Toggle fullscreen mode +delete.widget.confirmation=Delete this widget from the dashboard \ No newline at end of file diff --git a/i18n/enum/adminDashboardEditAccess.properties b/i18n/enum/adminDashboardEditAccess.properties new file mode 100644 index 0000000..a8739f1 --- /dev/null +++ b/i18n/enum/adminDashboardEditAccess.properties @@ -0,0 +1,5 @@ +private.label=Private +private.description=Only you can edit the dashboard + +specific.label=Users & groups +specific.description=Choose specific users and groups who can edit the dashboard \ No newline at end of file diff --git a/i18n/enum/adminDashboardViewAccess.properties b/i18n/enum/adminDashboardViewAccess.properties new file mode 100644 index 0000000..444976d --- /dev/null +++ b/i18n/enum/adminDashboardViewAccess.properties @@ -0,0 +1,8 @@ +private.label=Private +private.description=Only you can view the dashboard + +public.label=Public +public.description=Anyone can view the dashboard + +specific.label=Users & groups +specific.description=Choose specific users and groups who can view the dashboard \ No newline at end of file diff --git a/i18n/preside-objects/admin_dashboard.properties b/i18n/preside-objects/admin_dashboard.properties new file mode 100644 index 0000000..4771d70 --- /dev/null +++ b/i18n/preside-objects/admin_dashboard.properties @@ -0,0 +1,36 @@ +title=Admin dashboards +title.singular=Admin dashboard +description= +iconClass=fa-tachometer +menuTitle=Admin dashboards + +field.name.title=Name +field.description.title=Description +field.column_count.title=Columns +field.owner.title=Owner +field.widgets.title=Widgets + +field.view_access.title=View access +field.view_groups.title=Groups with view access +field.view_users.title=Users with view access +field.edit_access.title=Edit access +field.edit_groups.title=Groups with edit access +field.edit_users.title=Users with edit access + +validation.name.presideObjectUniqueIndex.message=Dashboard name is already in use + +sharing.title=Sharing permissions +sharing.btn=Permissions +sharing.breadcrumb.title=Sharing +sharing.page.title=Share this dashboard +sharing.page.subtitle=Choose which users can view and/or edit this dashboard + +widget.add.btn=Add widget + +sharing.update.error=Unable to save your sharing permissions. Please check and try again. +sharing.update.success=Sharing permissions successfully updated. + +fieldset.view_permissions.title=View permissions +fieldset.view_permissions.description=

Control which users may view this dashboard. They will be able to view and clone the dashboard, but will be unable to edit, share or delete it.

+fieldset.edit_permissions.title=Edit permissions +fieldset.edit_permissions.description=

Control which users may edit this dashboard. They will be able to view, edit and clone the dashboard, but will be unable to share or delete it.

\ No newline at end of file diff --git a/manifest.json b/manifest.json index 60b40ac..736b9e9 100644 --- a/manifest.json +++ b/manifest.json @@ -3,4 +3,7 @@ , "title" : "Admin Dashboards" , "author" : "Pixl8 Group" , "version" : "$VERSION_NUMBER" + , "dependsOn" : [ + "preside-ext-chart-js" + ] } diff --git a/preside-objects/admin_dashboard.cfc b/preside-objects/admin_dashboard.cfc new file mode 100644 index 0000000..c7157b6 --- /dev/null +++ b/preside-objects/admin_dashboard.cfc @@ -0,0 +1,32 @@ +/** + * @labelField name + * @versioned false + * @dataManagerEnabled true + * @dataManagerGridFields name,owner,view_access,edit_access,datecreated + * @dataManagerDefaultSortOrder name + * @dataManagerHiddenGridFields owner_id,view_groups_list,view_users_list,edit_groups_list,edit_users_list + * @dataManagerAllowedOperations navigate,read,add,edit,clone,delete + */ + +component { + property name="name" type="string" dbtype="varchar" maxlength="50" required=true uniqueIndexes="dashboardName"; + property name="description" type="string" dbtype="varchar" maxlength="300"; + property name="column_count" type="numeric" dbtype="int" default=2; + + property name="owner" relationship="many-to-one" relatedTo="security_user" required=true generate="insert" generator="adminDashboard.owner" cloneable=false; + property name="widgets" relationship="one-to-many" relatedto="admin_dashboard_widget" relationshipKey="dashboard" cloneable=true; + + property name="view_access" type="string" dbtype="varchar" maxlength=10 enum="adminDashboardViewAccess" default="private"; + property name="view_groups" adminRenderer="ObjectRelatedRecordsList" relationship="many-to-many" relatedTo="security_group" relatedVia="admin_dashboard_view_group" cloneable=false; + property name="view_users" adminRenderer="ObjectRelatedRecordsList" relationship="many-to-many" relatedTo="security_user" relatedVia="admin_dashboard_view_user" cloneable=false; + + property name="edit_access" type="string" dbtype="varchar" maxlength=10 enum="adminDashboardEditAccess" default="private"; + property name="edit_groups" adminRenderer="ObjectRelatedRecordsList" relationship="many-to-many" relatedTo="security_group" relatedVia="admin_dashboard_edit_group" cloneable=false; + property name="edit_users" adminRenderer="ObjectRelatedRecordsList" relationship="many-to-many" relatedTo="security_user" relatedVia="admin_dashboard_edit_user" cloneable=false; + + property name="owner_id" adminRenderer="none" type="string" formula="${prefix}owner.id"; + property name="view_groups_list" adminRenderer="none" type="string" formula="group_concat( distinct ${prefix}view_groups.id )"; + property name="view_users_list" adminRenderer="none" type="string" formula="group_concat( distinct ${prefix}view_users.id )"; + property name="edit_groups_list" adminRenderer="none" type="string" formula="group_concat( distinct ${prefix}edit_groups.id )"; + property name="edit_users_list" adminRenderer="none" type="string" formula="group_concat( distinct ${prefix}edit_users.id )"; +} \ No newline at end of file diff --git a/preside-objects/admin_dashboard_widget.cfc b/preside-objects/admin_dashboard_widget.cfc new file mode 100644 index 0000000..1f1ddaf --- /dev/null +++ b/preside-objects/admin_dashboard_widget.cfc @@ -0,0 +1,13 @@ +/** + * @versioned false + * @nolabel + */ +component { + property name="dashboard" relationship="many-to-one" relatedto="admin_dashboard" required=true indexes="adminDashboardWidgetDashboard"; + property name="widget_id" type="string" dbtype="varchar" maxlength=100 required=true; + property name="instance_id" type="string" dbtype="varchar" maxlength=100 required=true indexes="adminDashboardWidgetInstance"; + property name="title" type="string" dbtype="varchar" maxlength=50 required=true; + property name="column" type="numeric" dbtype="int"; + property name="slot" type="numeric" dbtype="int"; + property name="config" type="string" dbtype="text"; +} diff --git a/services/AdminDashboardService.cfc b/services/AdminDashboardService.cfc new file mode 100644 index 0000000..1838780 --- /dev/null +++ b/services/AdminDashboardService.cfc @@ -0,0 +1,68 @@ +/** + * @presideService true + * @singleton true + */ +component { + +// CONSTRUCTOR + /** + * + */ + public any function init() { + return this; + } + +// PUBLIC API METHODS + public boolean function userCanViewDashboard( required string dashboardId, string adminUserId=$getAdminLoggedInUserId() ) { + var adminUserGroups = _getAdminUserGroups( arguments.adminUserId ); + + return $getPresideObject( "admin_dashboard" ).dataExists( + filter = { "admin_dashboard.id"=arguments.dashboardId } + , extraFilters = [ { + filter = "view_access = 'public' + or admin_dashboard.owner = :adminUserId + or ( view_access = 'specific' and ( view_users.id = :adminUserId or view_groups.id in ( :adminUserGroups ) ) ) + or ( edit_access = 'specific' and ( edit_users.id = :adminUserId or edit_groups.id in ( :adminUserGroups ) ) )" + , filterParams = { + adminUserId = { type="varchar", value=adminUserId } + , adminUserGroups = { type="varchar", value=adminUserGroups, list=true } + } + } ] + ); + } + + public boolean function userCanEditDashboard( required string dashboardId, string adminUserId=$getAdminLoggedInUserId() ) { + return $getPresideObject( "admin_dashboard" ).dataExists( + filter = { "admin_dashboard.id"=arguments.dashboardId } + , extraFilters = [ { + filter = "admin_dashboard.owner = :adminUserId + or ( edit_access = 'specific' and ( edit_users.id = :adminUserId or edit_groups$users.id = :adminUserId ) )" + , filterParams = { adminUserId={ type="varchar", value=arguments.adminUserId } } + } ] + ); + } + + public boolean function userCanShareDashboard( required string dashboardId, string adminUserId=$getAdminLoggedInUserId() ) { + return $getPresideObject( "admin_dashboard" ).dataExists( + filter = { id=arguments.dashboardId, owner=arguments.adminUserId } + ); + } + + public boolean function userCanDeleteDashboard( required string dashboardId, string adminUserId=$getAdminLoggedInUserId() ) { + return $getPresideObject( "admin_dashboard" ).dataExists( + filter = { id=arguments.dashboardId, owner=arguments.adminUserId } + ); + } + +// PRIVATE HELPERS + private string function _getAdminUserGroups( required string adminUserId ) { + return $getPresideObject( "security_group" ).selectData( + filter = { "users.id"=arguments.adminUserId } + , selectFields = [ "id" ] + ).valueList( "id" ); + } + + +// GETTERS AND SETTERS + +} \ No newline at end of file diff --git a/services/AdminDashboardWidgetService.cfc b/services/AdminDashboardWidgetService.cfc index 361bd97..6387957 100644 --- a/services/AdminDashboardWidgetService.cfc +++ b/services/AdminDashboardWidgetService.cfc @@ -3,19 +3,36 @@ * @singleton true */ component { - // CONSTRUCTOR /** - * @formsService.inject formsService - * + * @configuredWidgets.inject coldbox:setting:widgets + * @dashboardService.inject AdminDashboardService + * @formsService.inject FormsService + * @autoDiscoverDirectories.inject presidecms:directories */ - public any function init( required any formsService ) { + public any function init( + required struct configuredWidgets + , required any dashboardService + , required any formsService + , required array autoDiscoverDirectories + ) { + _setAutoDiscoverDirectories( arguments.autoDiscoverDirectories ); + _setConfiguredWidgets( arguments.configuredWidgets ); + _setDashboardService( arguments.dashboardService ); _setFormsService( arguments.formsService ); + reload(); + return this; } // PUBLIC API METHODS + public struct function getWidgets() { + var widgets = Duplicate( _getWidgets() ); + + return widgets; + } + public string function renderDashboard( required string dashboardId , required array widgets @@ -61,6 +78,7 @@ component { , columnSize = colsize , contextData = _namespaceContextData( widget.contextData ) , configInstanceId = widget.configInstanceId + , title = widget.title ?: "" , ajax = widget.ajax ); } @@ -69,26 +87,89 @@ component { return rendered; } + public struct function renderUserGeneratedDashboard( required string dashboardId, boolean allowEditing=true, struct contextData={} ) { + var dashboard = $getPresideObject( "admin_dashboard" ).selectData( id=arguments.dashboardId ); + var savedWidgets = $getPresideObject( "admin_dashboard_widget" ).selectData( + filter = { dashboard=arguments.dashboardId } + , orderBy = "column,slot" + ); + var columns = isNumeric( dashboard.column_count ) ? dashboard.column_count : 1; + var widget = {}; + var widgets = []; + var dashboardArgs = {}; + var canEdit = arguments.allowEditing && _getDashboardService().userCanEditDashboard( arguments.dashboardId ); + + for( var record in dashboard ) { + dashboardArgs = record; + break; + } + + for( var c=1; c<=columns; c++ ) { + widgets.append( [] ); + } + + for( var savedWidget in savedWidgets ) { + widget = { + id = savedWidget.widget_id + , title = savedWidget.title + , configInstanceId = savedWidget.instance_id + , contextData = isJSON( savedWidget.config ) ? deserializeJSON( savedWidget.config ) : {} + , ajax = true + , column = savedWidget.column <= columns ? savedWidget.column : 1 + , slot = savedWidget.slot + }; + widget.contextData.canEditDashboard = canEdit; + + widgets[ widget.column ].append( renderWidgetContainer( + dashboardId = arguments.dashboardId + , widgetId = widget.id + , contextData = _namespaceContextData( widget.contextData ) + , configInstanceId = widget.configInstanceId + , title = widget.title ?: "" + , ajax = widget.ajax + ) ); + } + + dashboardArgs.widgets = widgets; + dashboardArgs.canEdit = canEdit; + + return dashboardArgs; + } + public string function renderWidgetContainer( required string dashboardId , required string widgetId , numeric columnSize = 6 , struct contextData = {} , string configInstanceId = "" + , string title = "" , boolean ajax = true ) { - var instanceId = "dashboard-widget-" & LCase( Hash( arguments.dashboardId & arguments.widgetId & SerializeJson( arguments.contextData ) ) ); - var menuViewlet = "admin.admindashboards.widget.#arguments.widgetId#.additionalMenu"; - var additionalMenu = ""; - var content = ""; + var instanceId = "dashboard-widget-" & LCase( Hash( arguments.dashboardId & arguments.widgetId & SerializeJson( arguments.contextData ) ) ); + var menuViewlet = "admin.admindashboards.widget.#arguments.widgetId#.additionalMenu"; + var ajaxCallbackViewlet = "admin.admindashboards.widget.#arguments.widgetId#.ajaxCallback"; + var ajaxIncludesViewlet = "admin.admindashboards.widget.#arguments.widgetId#.ajaxIncludes"; + var additionalMenu = ""; + var ajaxCallback = ""; + var content = ""; + var userGeneratedDashboard = _isUserGeneratedDashboard( arguments.dashboardId ); + var canEditDashboard = $helpers.isTrue( arguments.contextData[ "dashboard.widget.data.canEditDashboard" ] ?: "" ); + + var viewletArgs = StructCopy( arguments ); + viewletArgs.contextData = _getContextDataFromRequest( arguments.contextData ); if ( $getColdbox().viewletExists( menuViewlet ) ) { - var args = StructCopy( arguments ); - args.contextData = _getContextDataFromRequest( arguments.contextData ); - additionalMenu = $renderViewlet( event=menuViewlet, args=args ); + additionalMenu &= $renderViewlet( event=menuViewlet, args=viewletArgs ); } - if ( !arguments.ajax ) { + if ( arguments.ajax ) { + if ( $getColdbox().viewletExists( ajaxCallbackViewlet ) ) { + ajaxCallback = $renderViewlet( event=ajaxCallbackViewlet, args=viewletArgs ); + } + if ( $getColdbox().viewletExists( ajaxIncludesViewlet ) ) { + $renderViewlet( event=ajaxIncludesViewlet, args=viewletArgs ); + } + } else { content = renderWidgetContent( dashboardId = arguments.dashboardId , widgetId = arguments.widgetId @@ -99,18 +180,22 @@ component { } var args = { - title = $translateResource( uri="admin.admindashboards.widget.#widgetId#:title" , defaultValue=widgetId ) - , icon = $translateResource( uri="admin.admindashboards.widget.#widgetId#:iconClass" , defaultValue="" ) - , description = $translateResource( uri="admin.admindashboards.widget.#widgetId#:description", defaultValue="" ) - , widgetId = arguments.widgetId - , columnSize = arguments.columnSize - , contextData = arguments.contextData - , ajax = arguments.ajax - , instanceId = instanceId - , configInstanceId = arguments.configInstanceId - , hasConfig = widgetHasConfigForm( arguments.widgetId ) - , additionalMenu = additionalMenu - , content = content + title = !isEmpty( arguments.title ) ? arguments.title : $translateResource( uri="admin.admindashboards.widget.#widgetId#:title", defaultValue=widgetId ) + , icon = $translateResource( uri="admin.admindashboards.widget.#widgetId#:iconClass" , defaultValue="" ) + , description = $translateResource( uri="admin.admindashboards.widget.#widgetId#:description", defaultValue="" ) + , widgetId = arguments.widgetId + , columnSize = arguments.columnSize + , contextData = arguments.contextData + , ajax = arguments.ajax + , ajaxCallback = ajaxCallback + , instanceId = instanceId + , configInstanceId = arguments.configInstanceId + , dashboardId = arguments.dashboardId + , userGeneratedDashboard = userGeneratedDashboard + , hasConfig = widgetHasConfigForm( arguments.widgetId ) && ( !userGeneratedDashboard || canEditDashboard ) + , canDeleteWidget = userGeneratedDashboard && canEditDashboard + , additionalMenu = additionalMenu + , content = content }; $announceInterception( "onRenderAdminWidgetContainer", args ); @@ -120,15 +205,22 @@ component { public string function renderWidgetContent( required string dashboardId, required string widgetId, required string instanceId, required string configInstanceId, required struct requestData ) { return $renderViewlet( event="admin.admindashboards.widget.#widgetId#.render", args={ - dashboardId = arguments.dashboardId - , config = getWidgetConfiguration( arguments.dashboardId, arguments.widgetId, arguments.configInstanceId ) - , contextData = _getContextDataFromRequest( arguments.requestData ) + dashboardId = arguments.dashboardId + , instanceId = arguments.instanceId + , configInstanceId = arguments.configInstanceId + , config = getWidgetConfiguration( arguments.dashboardId, arguments.widgetId, arguments.configInstanceId ) + , contextData = _getContextDataFromRequest( arguments.requestData ) } ); } public string function renderWidgetConfigForm( required string dashboardId, required string widgetId, required string instanceId ) { + var formName = "admin.admindashboards.widget.#widgetId#"; + if ( _isUserGeneratedDashboard( dashboardId ) ) { + formName = _getFormsService().getMergedFormName( formName, "admin.admindashboards.config" ); + } + return _getFormsService().renderForm( - formName = "admin.admindashboards.widget.#widgetId#" + formName = formName , savedData = getWidgetConfiguration( arguments.dashboardId, arguments.widgetId, arguments.instanceId ) ); } @@ -138,13 +230,26 @@ component { } public void function saveWidgetConfiguration( required string dashboardId, required string widgetId, required string instanceId, required struct requestData ) { - var fields = _getFormsService().listFields( formName="admin.admindashboards.widget.#widgetId#" ); - var config = {}; + var fields = _getFormsService().listFields( formName="admin.admindashboards.widget.#widgetId#" ); + var config = {}; for( var field in fields ) { config[ field ] = arguments.requestData[ field ] ?: ""; } + if ( _isUserGeneratedDashboard( dashboardId ) ) { + var data = { config=SerializeJson( config ) }; + if ( structKeyExists( arguments.requestData, "widget_title" ) && len( arguments.requestData.widget_title ) ) { + data.title = arguments.requestData.widget_title; + } + + $getPresideObject( "admin_dashboard_widget" ).updateData( + filter = { dashboard=arguments.dashboardId, widget_id=arguments.widgetId, instance_id=arguments.instanceId } + , data = data + ); + return; + } + var existed = $getPresideObject( "admin_dashboard_widget_configuration" ).updateData( filter = { dashboard_id=arguments.dashboardId, widget_id=arguments.widgetId, instance_id=arguments.instanceId, user=$getAdminLoggedInUserId() } , data = { config = SerializeJson( config ) } @@ -162,29 +267,43 @@ component { } public struct function getWidgetConfiguration( required string dashboardId, required string widgetId, required string instanceId ) { - var configRecord = $getPresideObject( "admin_dashboard_widget_configuration" ).selectData( filter={ - dashboard_id = arguments.dashboardId - , widget_id = arguments.widgetId - , instance_id = arguments.instanceId - , user = $getAdminLoggedInUserId() - } ); - - // backwards compat: attempt to get config - // record where instance ID is null if nothing found for - // specific instance - if ( !configRecord.recordCount ) { + var configRecord = ""; + var result = {}; + var userGeneratedDashboard = _isUserGeneratedDashboard( dashboardId ); + + if ( userGeneratedDashboard ) { + configRecord = $getPresideObject( "admin_dashboard_widget" ).selectData( filter={ + dashboard = arguments.dashboardId + , widget_id = arguments.widgetId + , instance_id = arguments.instanceId + } ); + } else { configRecord = $getPresideObject( "admin_dashboard_widget_configuration" ).selectData( filter={ - dashboard_id = arguments.dashboardId - , widget_id = arguments.widgetId - , instance_id = "" - , user = $getAdminLoggedInUserId() + dashboard_id = arguments.dashboardId + , widget_id = arguments.widgetId + , instance_id = arguments.instanceId + , user = $getAdminLoggedInUserId() } ); - } - var result = {}; + // backwards compat: attempt to get config + // record where instance ID is null if nothing found for + // specific instance + if ( !configRecord.recordCount ) { + configRecord = $getPresideObject( "admin_dashboard_widget_configuration" ).selectData( filter={ + dashboard_id = arguments.dashboardId + , widget_id = arguments.widgetId + , instance_id = "" + , user = $getAdminLoggedInUserId() + } ); + } + } try { result = DeserializeJson( configRecord.config ?: "" ); + + if ( userGeneratedDashboard ) { + result.widget_title = configRecord.title ?: ""; + } } catch( any e ) { result = {}; } @@ -205,9 +324,85 @@ component { ); } - return IsBoolean( result ?: "" ) && result; + return $helpers.isTrue( result ?: "" ); + } + + public boolean function isUserDashboardWidget( required string widgetId ) { + var coldbox = $getColdbox(); + var isUserDashboardWidgetEvent = "admin.admindashboards.widget.#widgetId#.isUserDashboardWidget"; + var result = false; + + if ( coldbox.handlerExists( isUserDashboardWidgetEvent ) ) { + result = coldbox.runEvent( + event = isUserDashboardWidgetEvent + , private = true + , prePostExempt = true + ); + } + + return $helpers.isTrue( result ?: "" ); } + public numeric function nextWidgetSlot( required string dashboardId, required numeric column ) { + var currentMax = $getPresideObject( "admin_dashboard_widget" ).selectData( + filter = { dashboard=arguments.dashboardId, column=arguments.column } + , selectFields = [ "max( slot ) as slot" ] + ); + return val( currentMax.slot ) + 1; + } + + public string function getInstanceTitle( required string dashboardId, required string widgetId ) { + var baseTitle = $translateResource( uri="admin.admindashboards.widget.#arguments.widgetId#:title", defaultValue=arguments.widgetId ); + var title = baseTitle; + var dao = $getPresideObject( "admin_dashboard_widget" ); + + var filter = { dashboard=arguments.dashboardId, title=title }; + var increment = 0; + + while( dao.dataExists( filter=filter ) ) { + title = baseTitle & " (#++increment#)"; + filter.title = title; + } + + return title; + } + + public string function addWidget( + required string dashboardId + , required string widgetId + , required string instanceId + , required string title + , required numeric column + , required numeric slot + , struct config = {} + ) { + return $getPresideObject( "admin_dashboard_widget" ).insertData( data={ + dashboard = arguments.dashboardId + , widget_id = arguments.widgetId + , instance_id = arguments.instanceId + , title = arguments.title + , column = arguments.column + , slot = arguments.slot + , config = serializeJson( arguments.config ) + } ); + } + + public void function deleteWidget( + required string dashboardId + , required string instanceId + ) { + $getPresideObject( "admin_dashboard_widget" ).deleteData( filter={ + dashboard = arguments.dashboardId + , instance_id = arguments.instanceId + } ); + } + + public void function reload() { + _autoDiscoverWidgets(); + _loadWidgetsFromConfig(); + } + + // PRVIATE HELPERS private struct function _namespaceContextData( required struct data ) { var namespaced = {}; @@ -231,8 +426,153 @@ component { return contextData; } + private boolean function _isUserGeneratedDashboard( required string dashboardId ) { + return $getPresideObject( "admin_dashboard" ).dataExists( id=arguments.dashboardId ); + } + + + private void function _loadWidgetsFromConfig() { + var widgets = _getWidgets(); + var configuration = _getConfiguredWidgets(); + + for( var widgetId in configuration ){ + widgets[ widgetId ] = Duplicate( configuration[ widgetId ] ); + + widgets[ widgetId ].id = widgetId; + widgets[ widgetId ].configForm = widgets[ widgetId ].configForm ?: _getFormNameByConvention( widgetId ); + widgets[ widgetId ].viewlet = widgets[ widgetId ].viewlet ?: _getViewletEventByConvention( widgetId ); + widgets[ widgetId ].icon = widgets[ widgetId ].icon ?: _getIconByConvention( widgetId ); + widgets[ widgetId ].title = widgets[ widgetId ].title ?: _getTitleByConvention( widgetId ); + widgets[ widgetId ].description = widgets[ widgetId ].description ?: _getDescriptionByConvention( widgetId ); + } + + _setWidgets( widgets ); + } + + private void function _autoDiscoverWidgets() { + var widgets = {}; + var viewsPath = "/views/admin/adminDashboards/widget"; + var handlersPath = "/handlers/admin/adminDashboards/widget"; + var ids = {}; + var autoDiscoverDirectories = _getAutoDiscoverDirectories(); + var siteTemplateMap = {}; + + for( var dir in autoDiscoverDirectories ) { + dir = ReReplace( dir, "/$", "" ); + var views = DirectoryList( dir & viewsPath , false, "query" ); + var handlers = DirectoryList( dir & handlersPath, false, "query", "*.cfc" ); + var siteTemplate = _getSiteTemplateFromPath( dir ); + + for ( var view in views ) { + var id = ""; + if ( views.type eq "Dir" ) { + id = views.name; + } else if ( views.type == "File" && ReFindNoCase( "\.cfm$", views.name ) && !views.name.reFind( "^_" ) ) { + id = ReReplaceNoCase( views.name, "\.cfm$", "" ); + } else { + continue; + } + + ids[ id ] = 1; + siteTemplateMap[ id ] = siteTemplateMap[ id ] ?: []; + siteTemplateMap[ id ].append( siteTemplate ); + } + + for ( var handler in handlers ) { + if ( handlers.type eq "File" ) { + var id = ReReplace( handlers.name, "\.cfc$", "" ); + ids[ id ] = 1; + + siteTemplateMap[ id ] = siteTemplateMap[ id ] ?: []; + siteTemplateMap[ id ].append( siteTemplate ); + } + } + } + + for( var id in ids ) { + widgets[ id ] = { + id = id + , configForm = _getFormNameByConvention( id ) + , viewlet = _getViewletEventByConvention( id ) + , placeholderViewlet = _getPlaceholderViewletEventByConvention( id ) + , icon = _getIconByConvention( id ) + , title = _getTitleByConvention( id ) + , description = _getDescriptionByConvention( id ) + , siteTemplates = _mergeSiteTemplates( siteTemplateMap[id] ) + , categories = _getWidgetCategoriesFromForm( id ) + }; + } + + _setWidgets( widgets ); + } + + private array function _getWidgetCategoriesFromForm( required string widgetId ) { + var formName = _getFormNameByConvention( arguments.widgetId ); + + if ( _getFormsService().formExists( formName ) ) { + var theForm = _getFormsService().getForm( formName ); + + return ListToArray( theForm.categories ?: "" ); + } + + return []; + } + + private string function _getSiteTemplateFromPath( required string path ) { + var regex = "^.*[\\/]site-templates[\\/]([^\\/]+)$"; + + if ( !ReFindNoCase( regex, arguments.path ) ) { + return "*"; + } + + return ReReplaceNoCase( arguments.path, regex, "\1" ); + } + + private string function _getViewletEventByConvention( required string widgetId ) { + return "admin.admindashboards.widget." & widgetId; + } + + private string function _getPlaceholderViewletEventByConvention( required string widgetId ) { + return "admin.admindashboards.widget." & widgetId & ".placeholder"; + } + + private string function _getIconByConvention( required string widgetId ) { + return "admin.admindashboards.widget.#widgetId#:iconclass"; + } + + private string function _getTitleByConvention( required string widgetId ) { + return "admin.admindashboards.widget.#widgetId#:title"; + } + + private string function _getDescriptionByConvention( required string widgetId ) { + return "admin.admindashboards.widget.#widgetId#:description"; + } + private string function _getFormNameByConvention( required string widgetId ) { + return "admin.admindashboards.widget." & widgetId; + } + private string function _mergeSiteTemplates( required array templates ) ouptut=false { + var merged = ""; + + for( var template in arguments.templates ) { + if ( template == "*" ) { + return "*"; + } + merged = ListAppend( merged, template ); + } + + return merged; + } + + // GETTERS AND SETTERS + private any function _getDashboardService() { + return _dashboardService; + } + private void function _setDashboardService( required any dashboardService ) { + _dashboardService = arguments.dashboardService; + } + private any function _getFormsService() { return _formsService; } @@ -240,4 +580,25 @@ component { _formsService = arguments.formsService; } + private struct function _getWidgets() { + return _widgets; + } + private void function _setWidgets( required struct widgets ) { + _widgets = arguments.widgets; + } + + private struct function _getConfiguredWidgets() { + return _configuredWidgets; + } + private void function _setConfiguredWidgets( required struct configuredWidgets ) { + _configuredWidgets = arguments.configuredWidgets; + } + + private array function _getAutoDiscoverDirectories() { + return _autoDiscoverDirectories; + } + private void function _setAutoDiscoverDirectories( required array autoDiscoverDirectories ) { + _autoDiscoverDirectories = arguments.autoDiscoverDirectories; + } + } \ No newline at end of file diff --git a/views/admin/admindashboards/_userGenerated.cfm b/views/admin/admindashboards/_userGenerated.cfm new file mode 100644 index 0000000..c93a32a --- /dev/null +++ b/views/admin/admindashboards/_userGenerated.cfm @@ -0,0 +1,43 @@ + + dashboardId = args.id ?: ""; + widgets = args.widgets ?: []; + columns = args.column_count ?: 1; + columns = val( columns ) ? columns : 1; + columnWidth = int( 12 / columns ); + canEditDashboard = isTrue( args.canEdit ?: "" ); + + addTitle = translateResource( "preside-objects.admin_dashboard:widget.add.btn" ); + addLink = event.buildAdminLink( linkTo="adminDashboards.widgetDialog", queryString="dashboard=#dashboardId#&column={column}" ); + + + +
+
+ +
+
+ #ArrayToList( widgets[ c ], "" )# +
+ + + + +
+
+
+ + +
+
\ No newline at end of file diff --git a/views/admin/admindashboards/browserDialog.cfm b/views/admin/admindashboards/browserDialog.cfm new file mode 100644 index 0000000..7adb53a --- /dev/null +++ b/views/admin/admindashboards/browserDialog.cfm @@ -0,0 +1,26 @@ + + widgets = prc.widgets ?: QueryNew(''); + dashboardId = rc.dashboard ?: ""; + column = rc.column ?: 1; + linkQs = "dashboard=#dashboardId#&column=#column#"; + baseLink = event.buildAdminLink( linkTo="AdminDashboards.addWidget", queryString="#linkQs#&widget={widgetid}" ); + + + + + + + \ No newline at end of file diff --git a/views/admin/admindashboards/recordView.cfm b/views/admin/admindashboards/recordView.cfm new file mode 100644 index 0000000..5fb90b5 --- /dev/null +++ b/views/admin/admindashboards/recordView.cfm @@ -0,0 +1,12 @@ + + dashboardId = args.recordId ?: ""; + canEditDashboard = isTrue( args.canEditDashboard ?: "" ); + + + + #renderAdminDashboard( + dashboardId = dashboardId + , userGenerated = true + , allowEditing = true + )# + \ No newline at end of file diff --git a/views/admin/admindashboards/widget/dashboardDataFilter/_gridActions.cfm b/views/admin/admindashboards/widget/dashboardDataFilter/_gridActions.cfm new file mode 100644 index 0000000..12481b7 --- /dev/null +++ b/views/admin/admindashboards/widget/dashboardDataFilter/_gridActions.cfm @@ -0,0 +1,12 @@ + + objectName = args.objectName ?: ""; + recordId = args.id ?: ""; + + + +
+ + + +
+
\ No newline at end of file diff --git a/views/admin/admindashboards/widgetContainer.cfm b/views/admin/admindashboards/widgetContainer.cfm index 4065c7f..1a79376 100644 --- a/views/admin/admindashboards/widgetContainer.cfm +++ b/views/admin/admindashboards/widgetContainer.cfm @@ -3,40 +3,62 @@ + + + + + + - editModalTitle = translateResource( uri="admindashboards:configure.widget.dialog.title", data=[ args.title ] ); + editModalTitle = translateResource( uri="admindashboards:configure.widget.dialog.title", data=[ args.title ] ); + configureTitle = translateResource( uri="admindashboards:configure.widget.title" ); + fullscreenTitle = translateResource( uri="admindashboards:fullscreen.widget.title" ); + deletePrompt = translateResource( uri="admindashboards:delete.widget.confirmation" ); event.includeData( { "#args.instanceId#"=args.contextData } ); -
-
+ +
+ +

- #args.title# + #args.title#

#args.additionalMenu# + - + + + +
- +

#args.description#


@@ -49,5 +71,7 @@
-
+ +
+ diff --git a/views/admin/datamanager/admin_dashboard/sharing.cfm b/views/admin/datamanager/admin_dashboard/sharing.cfm new file mode 100644 index 0000000..24ad9c9 --- /dev/null +++ b/views/admin/datamanager/admin_dashboard/sharing.cfm @@ -0,0 +1,34 @@ + + savedData = rc.formData ?: prc.savedData; + validationResult = prc.validationResult ?: ""; + dashboardId = prc.recordId ?: ""; + formId = "dashboard-sharing-#dashboardId#"; + formName = prc.formName ?: ""; + formAction = event.buildAdminLink( linkTo="datamanager.admin_dashboard.sharingAction", queryString="id=#dashboardId#" ); + + + +
+ #renderForm( + formName = formName + , formId = formId + , savedData = savedData + , validationResult = validationResult + , context = "admin" + )# + +
+
+ + + #translateResource( "cms:cancel.btn" )# + + + +
+
+
+
\ No newline at end of file diff --git a/views/admin/layout/configurationMenu/adminDashboards.cfm b/views/admin/layout/configurationMenu/adminDashboards.cfm new file mode 100644 index 0000000..808b0b5 --- /dev/null +++ b/views/admin/layout/configurationMenu/adminDashboards.cfm @@ -0,0 +1,14 @@ + + objectName = "admin_dashboard"; + iconClass = translateResource( "preside-objects.#objectName#:iconClass" ); + linkTitle = translateResource( "preside-objects.#objectName#:menuTitle" ); + + + +
  • + + + #linkTitle# + +
  • +
    \ No newline at end of file