-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch-doc.json
More file actions
1 lines (1 loc) · 247 KB
/
search-doc.json
File metadata and controls
1 lines (1 loc) · 247 KB
1
[{"title":"Introduction","type":0,"sectionRef":"#","url":"docs/","content":"","keywords":""},{"title":"What is divbloxPHP?","type":1,"pageTitle":"Introduction","url":"docs/#what-is-divbloxphp","content":"divbloxPHP is a full stack web and mobile app development framework that allows anyone, from designers and analysts, to hard-core developers, to collaborate and create amazing experiences in no time. With divbloxPHP you can build progressive web apps that can also seamlessly be converted to native mobile apps, all with ONE code base. "},{"title":"Core Ingredients","type":1,"pageTitle":"Introduction","url":"docs/#core-ingredients","content":""},{"title":"Backend","type":1,"pageTitle":"Introduction","url":"docs/#backend","content":"PHP divbloxPHP's server-side is driven by PHP. It is designed to be headless, returning data in JSON format. This means that, although divbloxPHP ships as a full stack application development solution, developers can plug in any frontend with very little effort. MySQL divbloxPHP provides you with a very sophisticated ORM design, and automates most of the database connections and entity CRUD functionality. Both MySQL and MariaDB are supported. "},{"title":"Frontend","type":1,"pageTitle":"Introduction","url":"docs/#frontend","content":"HTML 5 HTML is used only as a shell, or starting point, which we will fill in and manipulate using JavaScript. Most, if not all of the HTML in a divbloxPHP application will be generated and handled on the front end. CSS CSS preprocessed using SASS allowing for quick and easy setting up of global theming. This allows both developers and designers to easily map out colour palettes and styles for the whole application. Bootstrap Bootstrap is our UI framework of choice, providing convenient grid layouts, a variety of basic components, as well as amazing mobile responsiveness out of the box. JavaScript JavaScript (and mostly JQuery) run the front end of any divbloxPHP application and is used to execute logic, manipulate the UI and manage communication with the backend. "},{"title":"Advanced Training Evaluation","type":0,"sectionRef":"#","url":"docs/advanced-training-evaluation","content":"You should only attempt this evaluation after working through and feeling comfortable with the following sections: The BasicsHello World ExampleBasic Training Exercise and EvaluationThe Advance Training Exercise The exercise below is intended to evaluate your level of understanding of the more advanced concepts of divbloxPHP. If you are able to build this exercise successfully, your skill will be considered proficient. info If you would like to have your exercise graded for certification, you can submit it to us at support@divblox.com. Please note, certification may carry a cost. Please send us: Link to your GitHub project (access given to @DaniS0312)Link to a divbloxPHP sandbox with your functional application, as well as admin and user credentials to log in with. Exercise Brief - Build a personal finance tracker. You should be able to create monthly budgets, and each can have multiple budget items. Each of these budget items may be a parent of further budget items (e.g. Utilities -> Water + Electricity + Internet). Budget items should have at least a name and amount. For each budget item, you can add expenses. Expenses should have at least a name, amount, description and date captured. The expenses can have multiple receipts attached to them. Each receipt should have either, data of issue, amount or document. As a user you should be able to quickly log an expense, and either immediately or later be able to link it to a budget item. You should be able to see unallocated expenses (not linked to a budget item) separated from the financial tracker. You should also have a dashboard summarizing your expenses progress over a month, both as a monthly single percentage and separately by budget items (categories). You should try split this into 3 distinct areas: An admin page to configure budget items, a new expense page to quickly add expenses (and possibly update them further) and the dashboard to summarize any metrics you would deem fitting for a budget.","keywords":""},{"title":"Basic Training Evaluation","type":0,"sectionRef":"#","url":"docs/basic-training-evaluation","content":"You should only attempt this evaluation once you have worked through the following sections: The BasicsBasic Training Exercise The exercise below is intended to evaluate whether you have an understanding of the basic concepts of divbloxPHP. If you are able to build this exercise successfully, your skill-level will be considered \"basic\" and you will be ready to attempt the advanced training exercise. info If you would like to have your exercise graded for certification, you can submit it to us at support@divblox.com. Please note, certification may carry a cost. Please send us: Link to your GitHub project (access given to @DaniS0312)Link to a Divblox sandbox with your functional application, as well as admin and user credentials to log in with. Exercise Brief - Build a simple personal expense tracker. You must be able to do the following: Because this is a personal expense tracker, you do not need to concern yourself with building your application for multiple users. We will however, cater for two different user roles as part of this exercise. Make sure to add your personal touch to the final application as some points will be allocated for creative freedom. It is also important to apply good basic programming principles. As as User: Log expenses quickly on a dedicated pageAn expense needs to have a categoryHave another page where: You can view a list of expensesYou should be able to open an existing expense and edit or delete itCategories and their current grand totals are listed When an expense is saved (or removed), it should update a total that is stored for the selected category As an Administrator: The sole purpose of the administrator is to manage categories. Categories must be configurable on a dedicated page General functionality: Expose a custom api operation that allows a user to specify a date range and get an expense total, per category, as a result","keywords":""},{"title":"Media Library and Image Viewer","type":0,"sectionRef":"#","url":"docs/common-examples-media-library-and-image-viewer","content":"divbloxPHP allows you to handle all of your images from one place. This is done in the \"Media Library\" tab in the setup page, as show below. There are three (maybe more, if you are creative) ways to add an image into your divbloxPHP page. Use a basic divbloxPHP component (Simple HMTL image tag) and fill in the necessary file path. This is perfectly fine for individual images here and there.Use the default image viewer component, and make sure to update the image path in the page's reset() function. Note how we define the image viewer's UID so that we can call the updateImage() function on it uniquely. (If you do not specify it, the UID will be a unique random string)Use the default image viewer component, and pass it the relative image path as an input parameter i.e. \"arguments:{\"image_path\": \"project/uploads/media/filename.png\"}\". The page's component.js is given below. Copy if(typeof component_classes['pages_image_page']===\"undefined\"){ classpages_image_pageextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions= [{ \"component_load_path\":\"ungrouped/imageviewer\", \"parent_element\":\"jrPkd\", \"arguments\":{ // Set a UID for this component \"uid\":\"imageviewer_1\", } }, { \"component_load_path\":\"ungrouped/imageviewer\", \"parent_element\":\"01DIl\", // Set the image_path load argument for the image viewer \"arguments\":{\"image_path\":\"project/uploads/media/fbd1be7af8321d83f139005dae538b23.jpg\"} }]; // Sub component config end } reset(inputs, propagate){ setActivePage(\"image_page\",\"image_page\"); super.reset(inputs, propagate); // Update the image every time the page refreshes getRegisteredComponent(\"imageviewer_1\").updateImage(\"project/uploads/media/3a3f3a509b33b16bdb5c9bf362c63f8a.jpg\"); } } component_classes['pages_image_page']= pages_image_page; } Below is a video runthrough of the 3 suggested ways of using the media library and using images in your project. Using the image viewer component may not make sense if you are adding images ad hoc, as it is simpler to just add it using plain HTML. If you would like to theme your images with consistent borders, sizing and shadows for example, using a single image viewer component to display all of your system images would save you a lot of time. The developer is encouraged to create a copy of the default image viewer and experiment with this.","keywords":""},{"title":"1. Starting with a Sandbox","type":0,"sectionRef":"#","url":"docs/getting-started-1-sandbox","content":"","keywords":""},{"title":"Initializing a new Sandbox","type":1,"pageTitle":"1. Starting with a Sandbox","url":"docs/getting-started-1-sandbox#initializing-a-new-sandbox","content":"Creating a new sandbox is easy: Projects PageCreate ProjectSandbox Start by creating your project at basecamp.Divblox.com "},{"title":"Downloading a Sandbox","type":1,"pageTitle":"1. Starting with a Sandbox","url":"docs/getting-started-1-sandbox#downloading-a-sandbox","content":"Sandboxes are great for prototyping and even to deploy small projects, but if you are serious about your project, at some point you will need to download your sandbox progress and continue on a local development environment. To download your sandbox, all you need is a local installation of divbloxPHP with your project's API key configured. From here you can click on the \"Sandboxes\" setup block to download your sandbox or deploy your local changes to your sandbox. Sandboxes are controlled by your local deployment environments. This means that you can deploy to your sandboxes from any local deployment environment as well as download any changes that were made in your sandboxes to your local environments "},{"title":"Sandbox best practices","type":1,"pageTitle":"1. Starting with a Sandbox","url":"docs/getting-started-1-sandbox#sandbox-best-practices","content":"divbloxPHP makes it super easy to transfer content between your local deployment environments and your sandboxes, but this can also be risky. When downloading your sandbox to a local environment, always make sure that you have backed up your local project files. The following is a shortlist of tips to help you get the most out of sandboxes: Always make use of a code versioning service such as GitHub, GitLab or BitBucketWhen downloading sandbox changes to a local environment, make sure to do this in a feature branch which can be reviewed before merging with your master project branch.Re-initializing a sandbox every now and then can be a good idea to ensure data integrity.Make regular backups of your sandbox data through the Data Modeler \"Data Export\" tool "},{"title":"divbloxPHP Best Practices","type":0,"sectionRef":"#","url":"docs/divblox-best-practices","content":"","keywords":""},{"title":"Default pages","type":1,"pageTitle":"divbloxPHP Best Practices","url":"docs/divblox-best-practices#default-pages","content":"This example covers creating a default page from which to work from to maintain functionality and aesthetics across a site without making changes to exsisting components that you want to use at a later time. Using the a page with a bottom navigation bar, we can create our own page that inherits all of the existing pages components. Create a new page component from the default \"blank_page_with_bottom_nav\". For any future pages, you now have a default page from which to create all of your new pages from. This ensures consistency in both looks and feel. It makes the overall experience of the page a more imersive . For instance navigation items remain the same across pages, or your chosen background is now on all the pages without having to add it every time a new page is created. This practice leaves the original pages untouched incase you wish to create a new page from them. "},{"title":"Customizing Components","type":1,"pageTitle":"divbloxPHP Best Practices","url":"docs/divblox-best-practices#customizing-components","content":"divbloxPHP comes with default components like pages and navigation bars, but you might not want yours to look the same or behave the same. To do this we can create custom versions of the existing components and to load them in place of the default ones. This will reduce the amount of time it would take to make a custom item by easing the process required to create new components. This is illustrated in the example below, where an existing navigation bar is duplicated, edited and then used to replace the existing navigation bar on a default page (see Default pages ). Start by creating a new navigation bar from the existing \"top_navbar\" component. All the changes you want can now be made to your new navigation bar, like adding new menu items or changing the look and feel without changing a default item. In this example the item titles have been altered. Now that the custom navigation bar has been created it can be placed in your default page that will be used as a template for pages that require a top navigation bar. Start by creating the page from the existing blank page that contains the top navigation bar. To replace the already present navigation bar with your custom one, open the page and edit the component. Inside the \"Js\" tab there is a method named \"constructor()\" that is used to populate the page with its sub-components. The inclusion of the navigation bar component is located here. Replace the component load path for the current \"top_navbar\" with the custom navigation bar and save the changes made. Your custom navigation bar in now in your default page. To make changes to the navigation bar, you can edit the custom navigation component in the component builder and it will be loaded across all pages made from your new default page with a custom navigation bar. "},{"title":"Overwriting default functions","type":1,"pageTitle":"divbloxPHP Best Practices","url":"docs/divblox-best-practices#overwriting-default-functions","content":"In Divblox it is common place to use the components provided as default, seen in Customizing Components. These components have been created with this purpose in mind and each component, such as the navigation bars, are filled with function place holders waiting for the a developer to fill them up with functionality. This example will uncover these place holder functions, specifically the ones located in the instance navigation bar, and to show where the functionality should be placed. Create a new navigation bar from the existing \"top_instance_navbar\". Open and edit your new component. Under the \"Js\" tab are the functions that register that an action has been performed and which function to call when this event has occurred. \"RegisterDomEvents()\" handles the interaction and the \"cancelFunction()\" and \"confirmFunction()\" are the functions that get called as a result of a button click. By default there isn't any functionality present, just console logs. By overwriting these functions you can add new behavior to your buttons. "},{"title":"2. Starting with VirtualBox","type":0,"sectionRef":"#","url":"docs/getting-started-2-virtualbox","content":"","keywords":""},{"title":"VirtualBox Download","type":1,"pageTitle":"2. Starting with VirtualBox","url":"docs/getting-started-2-virtualbox#virtualbox-download","content":"info Note: the virtual machine image is a 7 GB download. The latest version of Oracle's VirtualBox can be downloaded here to ensure that the VirtualBox image runs properly. The VM (.ova) file that contains divbloxPHP and all the accompanying software can be downloadedhere. "},{"title":"Loading the Virtual machine","type":1,"pageTitle":"2. Starting with VirtualBox","url":"docs/getting-started-2-virtualbox#loading-the-virtual-machine","content":"After installing VirtualBox on your device and running the application, add the image by opening \"File\" -> \"Import Appliance\". From here, find and add the divbloxPHP VM image file (.ova). On the following window you will be prompted to \"Import\". Adding the image may take several minutes to install and configure. Once your VM has been loaded, it can be started by \"double-clicking\" on the newly added machine in the VirtualBox application. This will open a separate window and begin booting up the divbloxPHP image. info The boot sequence is complete when you are greeted by the Welcome page. "},{"title":"Initialize divbloxPHP","type":1,"pageTitle":"2. Starting with VirtualBox","url":"docs/getting-started-2-virtualbox#initialize-divbloxphp","content":" The Welcome page is necessary to read through in order to operate the virtual machine. It contains information such as: Username and passwords for the VMRelevant file locations and permissionsQuick start linksInformation about the VM software After you have familiarised yourself with the introduction page, you can open the 'local starter project' from the link located under 'Quick Start links' on the welcome page. This will open up the set up page of a local installation of divbloxPHP (a clean slate for you to work with). You can log in with your Basecamp details. "},{"title":"Basecamp","type":0,"sectionRef":"#","url":"docs/getting-started-basecamp","content":"","keywords":""},{"title":"Projects","type":1,"pageTitle":"Basecamp","url":"docs/getting-started-basecamp#projects","content":"Below you can see how to manage your projects, from updating project details to adding collaborators to setting up deployment environments. DetailsCollaboratorsEnvironments You can review and edit project details here "},{"title":"Organisations","type":1,"pageTitle":"Basecamp","url":"docs/getting-started-basecamp#organisations","content":"In this section you can view and update your \"organisations\". Organisations and subscriptions are inherently linked, with each organisation having a single subscription. You can edit organisation details as well as add organisation administrators. Organisation administrators have admin privileges on all projects that belong to that organisation. "},{"title":"Subscriptions","type":1,"pageTitle":"Basecamp","url":"docs/getting-started-basecamp#subscriptions","content":"In this section you can view and upgrade any of your organisation's subscriptions. You can see all necessary information such as the subscription status, tier and renewal date, as well as relevant subscription project, sandbox and environment limits. Now that you are familiar with Basecamp, we can continue and see how to get your hands dirty with Divblox. "},{"title":"3. Starting from scratch","type":0,"sectionRef":"#","url":"docs/getting-started-3-from-scratch","content":"","keywords":""},{"title":"Prerequisites","type":1,"pageTitle":"3. Starting from scratch","url":"docs/getting-started-3-from-scratch#prerequisites","content":"info The following sections are only relevant for local and/or self provisioned server setup. The divbloxPHP server-side functionality is built in php and therefore requires a php environment to be setup in order to function correctly. Download your favourite Apache/PHP/MySQL distribution (MAMP, WAMP, XAMPP etc...) and configure to use the following: Latest version of ApachePhp 7.3 or laterMySQL 5.7 or later or MariaDB 10.3 or laterThe recommended server software is MAMP for either windows or mac: https://www.mamp.info/en/Ensure that you have created a database for use with your divbloxPHP project Some of the core divbloxPHP code is encoded using IonCube. To ensure that your divbloxPHP installation functions correctly, download and install the IonCube loader for Php 7.3 or later for your operating system. Download hereDon't worry if you have some trouble installing this. The Installation checker will guide you through this process a bit later on as well. For an example of how to do this with Ubuntu 18.04, click here "},{"title":"Download divbloxPHP","type":1,"pageTitle":"3. Starting from scratch","url":"docs/getting-started-3-from-scratch#download-divbloxphp","content":"You can download or fork the divbloxPHP public repo on github here: https://github.com/divblox/divbloxAdd the downloaded content to your apache root folder. Common name are \"public_html\", \"ht-docs\", and \"www\"Ensure that your web server is running and navigate to http://localhost/ or http://localhost/[your-project-folder] (if you placed divbloxPHP within a sub folder)divbloxPHP will check your installation and, if needed, provide further guidelines on how to finish the installationTo open the divbloxPHP setup page, browse to http://localhost/divblox or http://localhost/[your-project-folder]/divblox, depending on your installationOpen the installation checker to ensure that all systems indicate an OK status. Once the installation checker indicates all is OK, you are ready to build with divbloxPHP "},{"title":"Installation checker","type":1,"pageTitle":"3. Starting from scratch","url":"docs/getting-started-3-from-scratch#installation-checker","content":"The divbloxPHP installation checker is designed to ensure that your divbloxPHP project meets all the prerequisites described above. It will also provide useful guidelines on how to solve installation related problems. The main checks performed are listed below: Checks for php >= 7.3Checks for mariadb >= 10.3 or mysql >= 5.7Checks your IonCube loader is installed. Learn why IonCube is required in the prerequisites section info The installation checker might fail if your environments have not yet been configured properly "},{"title":"Other Common Examples","type":0,"sectionRef":"#","url":"docs/common-examples-other-common-examples","content":"","keywords":""},{"title":"Update the look and feel","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#update-the-look-and-feel","content":"The aesthetics of a page is primarily controlled by CSS. Editing the way a page looks can be done by first opening \"UI configuration\" from the Divblox setup page. Either add new CSS class, or edit the existing ones, inside \"theme.css\". To view the statements click the \"bi-directional arrow\" to expand the code folds. By using these classes we can change the look of a certain item. When a component is placed on a page, it calls the CSS class from its HTML script. For instance inside the \"side_navbar\" component's HTML tab, and inside the HTML \"navigation tag\" is where the CSS class is referenced. Changing this will result in style changes to the navigation bar. Using the CSS classes in \"theme.css\", the navigation bar can be changed from dark to light using the classes \"sidebar-dark\" or \"sidebar-light\". "},{"title":"Adding Custom fonts to your project","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#adding-custom-fonts-to-your-project","content":"In this example we will add a new font to our project and then have that font present for offline applications as well. Start by finding a font for your project. This example will add Google's fonts as they're free to use and easily accessible. See here. Once you've chosen the font that will work, select it by clicking the \" + \" in the corner of the font tile. By selecting it, a window should open in the bottom right of the page containing a link that could normally be added to your HTML header, but if you want to export your application to a native environment, or even just to have it functioning offline, the CSS and font files will need to be added manually. By opening the link in the HTML snippet in a new tab, we can see the font's CSS code. The fonts can now be placed into your project by copying the CSS code and pasting it in the \"Custom Global Styles\" section found at the top of the \"UI configuration\" in the \"theme.css\" file in the Divblox setup. You have now added a custom font to your project, but this is only going to work for online apps that are able to retrieve the font files. To have a native app the font files need to be downloaded. To get these files copy the URL of the font file, which is found in the CSS code that was just copied, into a new browser tab. Opening the URL will start the download of the file. This should be saved in \"divblox-master/projects/assets/fonts\". Repeat this step for all the file links found in the CSS code that was copied as the links are all unique. The saved files need to be linked to the CSS code. Rather than having the URL to the file, it can be replaced with : Copy ../assets/fonts/[ PLACE_FONT_FILE_NAME_HERE ] Your project should have locally stored fonts. Remember to save the UI configuration before moving on. "},{"title":"Global variables for non-SPA apps","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#global-variables-for-non-spa-apps","content":"A benefit of having a single page application (SPA) is that the variables that are defined keep their value when the page refreshes. This is because the user side the application is rewriting the same page instead of receiving new pages from a server. This ability to maintain your variable values between pages is lost when designing a non-SPA app. The solution Divblox has incorporated are global functions that can store and retrieve variables. These functions can be found in \"/assets/js/divblox.js\". Copy /** * Adds a key:value pairing to the global_vars array and stores it in the app state * @param {String} name The name of the variable to store * @param {String} value The value to store * @return {Boolean|*} false if a name was not specified. */ function setGlobalVariable(name,value); /** * Returns a global variable from the global_vars array by name * @param {String} name The name of the variable to return * @return {String} The value to return */ function getGlobalVariable(name); /** * Sets a global id that is used to constrain for a specified entity * @param {String} entity The name of the entity to which this constrain id applies * @param {Number} constraining_id The id to constain by * @return {Boolean|*} false if a name was not specified. */ function setGlobalConstrainById(entity,constraining_id); /** * Returns a global id that is used to constrain for a specified entity * @param {String} entity The name of the entity to which this constrain id applies * @return {Number} The id to constain by. -1 If not set */ function getGlobalConstrainById(entity) ; "},{"title":"Changing the logo","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#changing-the-logo","content":"For a standard web app# For a standard web app or site changing the main logo and icon can be done by: Opening the \"UI configuration\" tile on the Divblox setup page. Drag and drop your logo or icon of choice into the upload areas. Divblox updates the logo upon adding the new image. Opening up a page with the logo on it will show that it has been updated. For a progressive web application# A progressive web application (PWA) is one that behaves more \"app-like\" by having features such as being independent of connectivity, can be installed to a home screen and have push notifications. Since these applications can be placed on a home screen the app requires features that are outside of a app's pages, meaning that it will have both a launch icon and a splash page that gets displayed when the app is launched. The icon can be set by changing the name of icon you have to that of the default icon, this way your logo will be loaded in. First, in \"manifest.json\" are the names for the app icons. Change the name of the images that you want to add to match these so that they can be used. Place the images inside your projects image folder, \"/project/assets/images\". 3) The splash page is changed similarly. In the \"index.html\", inside of the header are all the file paths to the splash page and mobile application launch icons. Change the names the images that you have to match the name of the images used in this file. Place your images in \"/project/assets/images\". note Do not change the file paths in \"manifest.json\" and \"index.html\" to match your images. "},{"title":"Displaying your logo","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#displaying-your-logo","content":"Displaying your application's logo on a page is done by simply adding the \"app_logo\" class into your HTML file where you need it to be displayed. The following HTML line is used to place the logo on your page. Copy <divclass=\"app_logo\"/> A way of adding it to a container is to start by opening the component you want to have a logo on in the \"Component Builder\". In a row of a container click \" + Add Component\" Click \"Add Custom Html\" to insert the code for the logo into the container. Add in the line, that includes the logo from step 1, into the HTML section. Save and close to view the page. The logo should now be present in the container that was just created. By inspecting the new item we can see that it uses the \"img-fluid\" class, meaning the logo will adjust to the container's width when the original image's width is larger than that of the container's width, otherwise it will remain the same size as the original image. "},{"title":"User Profile image","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#user-profile-image","content":"This example will focus on placing the user profile image on any page. Choose and open a page in the \"Component Builder\". Inside a container on the page, place a new component. Find and select \"imageViewer\". Before adding the image viewer to the page it will need a unique identifier, or \"UID\", so that it is identifiable and when placing the profile picture in it, it won't disrupt other image viewer components on the page. Create the image viewer and take note that there is no image to be viewed. Edit the page and open the \"Js\" tab. You'll be able to see the \"imageViewer\" has been loaded in as a \"sub-component\". To add the profile picture to the viewer as the page is loaded we use \"subComponentLoadedCallback()\" which is called when each of the sub-components on the page have been loaded. In this function we can check for a specific UID to select a specific sub-component from the page, and then get the current user's profile picture path and update the image viewer's image. Copy subComponentLoadedCallBack(component){ super.subComponentLoadedCallBack(component); if(component.getUid()===\"[Place_Your_Uid_Here]\"){ getCurrentUserAttribute(\"ProfilePicturePath\",function(logo_path){ component.updateImage(logo_path); }) } } Save and reload the page. The user profile picture has now be loaded into the image viewer as the page loads. "},{"title":"Sending an email","type":1,"pageTitle":"Other Common Examples","url":"docs/common-examples-other-common-examples#sending-an-email","content":"Divblox implements PHPMailer (https://github.com/PHPMailer/PHPMailer) to send emails. Divblox will automatically log all email sending activity in a table called EmailMessage. Therefore, it is important to ensure that your database is correctly set up and you have done a synchronization on the data model before continuing. Before sending emails you need to configure your email settings within Divblox. Fill in the necessary parameters in the EmailSettings class located in the file /project/assets/php/project_classes.php. Copy abstractclassEmail_SettingsextendsEmailSettings_Framework{ // Example using Gmail publicstatic$SMTPSERVER='smtp.gmail.com'; publicstatic$SMTPUsername='user.divblox@gmail.com'; publicstatic$SMTPPassword='secret_password'; publicstatic$SMTPPort=587; // To disable verbose debug output, use DEBUG_OFF publicstatic$SMTPDebugMode= \\PHPMailer\\PHPMailer\\SMTP::DEBUG_SERVER; // Enable TLS Encryption; also accepts 'ENCRYPTION_STARTSMTPS' publicstatic$SMTPSecure= \\PHPMailer\\PHPMailer\\PHPMailer::ENCRYPTION_STARTTLS; publicstatic$SMTPForceSecurityProtocol=true; publicstatic$SMTPAUtoTLS=false; } We are overriding the default parameters set in the EmailSettings_Base class. This is part of the Divblox best practices, as your changes to Divblox base files may be deleted as Divblox replaces the files with updated ones. The chain of inheritance is as follows: For some email servers such as Gmail, security protocols are expected. For the above Gmail example, TLS is mandatory and hence we can either set $SMTPAutoTLS = true; which will always set the security protocol to TLS, or we can define which protocol to use,$SMTPSecure = \\PHPMailer\\PHPMailer\\PHPMailer::ENCRYPTION_STARTTLS; and then force the protocol to match the previously defined one, using $SMTP ForceSecurityProtocol = true; We are now ready to use the EmailManager class to prepare and send emails. Prepare your email with EmailManager::prepareEmail(\"Test Subject\", \"A test message\");Add one or multiple recipient addresses with EmailManager::addRecipientAddress(\"recipient.address@gmail.com\", \"Recipient Name\");Optionally, add CC addresses with EmailManager::addCCAddress(\"recipient.address@gmail.com\");Optionally, add BCC addresses with EmailManager::addBCCAddress(\"recipient.address@gmail.com\");Optionally, add attachments with EmailManager::addAttachment(\"file_path_from_root\", \"file_name_to_display\");Send your email with EmailManager::sendEmail($ErrorInfo);. $ErrorInfo is a variable that is passed by reference and will result in an array that is populated with any additional information regarding the success or failure of the function. Copy // Step 1: Prepare the email EmailManager::prepareEmail(\"Test Subject\",\"A test message\"); // Step 2: Add recipient/s EmailManager::addRecipientAddress(\"recipient.address@gmail.com\",\"Recipient Name\"); // Step 3-5: (Optional) Add CC/BCC addresses and/or attachments // EmailManager::addCCAddress(\"recipient.address@gmail.com\"); // EmailManager::addBCCAddress(\"recipient.address@gmail.com\"); // EmailManager::addAttachment(\"file_path_from_root\", \"file_name_to_display\"); // Step 6: Send the email if(EmailManager::sendEmail($ErrorInfo)){ // This means the email was successfully sent, // additional info can be found in the array $ErrorInfo }else{ //This means the email was NOT successfully sent, // additional info can be found in the array $ErrorInfo } "},{"title":"Hello World Example","type":0,"sectionRef":"#","url":"docs/common-examples-hello-world-example","content":"","keywords":""},{"title":"Step 1 - A New Component","type":1,"pageTitle":"Hello World Example","url":"docs/common-examples-hello-world-example#step-1---a-new-component","content":"Let's create a new component that will serve as our app's page. Login to the Divblox setup page at /divblox in your project's folder on localhost. For example: http://localhost/my-app/divblox or http://localhost/divblox (if you project is in the webserver's root path)Open the Component Builder and create a new custom component called \"hello world\". This component should be placed in the grouping \"pages\" in order to be used as a page. "},{"title":"Step 2 - Adding Content","type":1,"pageTitle":"Hello World Example","url":"docs/common-examples-hello-world-example#step-2---adding-content","content":"Let's open the newly created component to work on it using the Component Builder You can click on the link provided when the component is created or you can find your component by searching \"hello world\" on the default Component Builder pageAdd a container to the page and make it full widthAdd a row with 1 column to the container. This column will serve as the wrapper for our contentAdd the \"imageviewer\" component as a sub component by clicking \"+ Component\" and selecting it from the list of available componentsAdd a basic input component by clicking \"+ Component\" and selecting \"Basic Component -> Input Types -> Form Control Email\"Add a basic button component by clicking \"+ Component\" and selecting \"Basic Component -> Buttons -> Simple Primary Button\" "},{"title":"Step 3 - Component Customization","type":1,"pageTitle":"Hello World Example","url":"docs/common-examples-hello-world-example#step-3---component-customization","content":"Let's do some basic customization on our image to display our logo The imageviewer component has a function called updateImage() which we can use to display our logo. Let's update our hello_world component javascript to do this.Add the following code to the bottom of the hello_world component class to override the subComponentLoadedCallBack function to update the image: Copy subComponentLoadedCallBack(component){ super.subComponentLoadedCallBack(component); if(component.getComponentName()==='ungrouped_imageviewer'){ component.updateImage('https://divblox.github.io/_media/divblox-logo-1.png'); } } Our hello_world component javascript should now look like this: Copy if(typeof component_classes[\"pages_hello_world\"]===\"undefined\"){ classpages_hello_worldextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[ { component_load_path:\"ungrouped/imageviewer\", parent_element:\"qJTep\", arguments:{}, }, ]; // Sub component config end } initCustomFunctions(){ // n3CEV_button Related functionality /////////////////////////////////////////////////////////////////////// getComponentElementById(this,\"n3CEV_btn\").on( \"click\", function(){ // Add the trigger element to the loading element array. // This shows a loading animation on the trigger // element while it waits for a response or function return let element_id =addTriggerElementToLoadingElementArray( $(this).attr(\"id\"), \"Nice Loading text\" ); // Example: once your function has executed, // call removeTriggerElementFromLoadingElementArray to remove // loading animation setTimeout(function(){ removeTriggerElementFromLoadingElementArray(element_id); },3000); }.bind(this) ); /////////////////////////////////////////////////////////////////////////// } subComponentLoadedCallBack(component){ super.subComponentLoadedCallBack(component); if(component.getComponentName()===\"ungrouped_imageviewer\"){ component.updateImage( \"https://divblox.github.io/_media/divblox-logo-1.png\" ); } } } component_classes[\"pages_hello_world\"]= pages_hello_world; } "},{"title":"Step 4 - Processing Input","type":1,"pageTitle":"Hello World Example","url":"docs/common-examples-hello-world-example#step-4---processing-input","content":"Let's now send the input to the server and get a response We will adapt the functionality that is currently handling the button click by using Divblox's built in request function to send the data to the server and handle the responseThe code below handles the click action for our button. Right now it simply mimicks that something is happening when you click the button Copy // n3CEV_button Related functionality /////////////////////////////////////////////////////////////////////////// getComponentElementById(this,\"n3CEV_btn\").on( \"click\", function(){ // Add the trigger element to the loading element array. // This shows a loading animation on the trigger // element while it waits for a response or function return let element_id =addTriggerElementToLoadingElementArray( $(this).attr(\"id\"), \"Nice Loading text\" ); // Example: once your function has executed, call // removeTriggerElementFromLoadingElementArray to remove // loading animation setTimeout(function(){ removeTriggerElementFromLoadingElementArray(element_id); },3000); }.bind(this) ); //////////////////////////////////////////////////////////////////////////// Let's change the click handler function to achieve our goal: Copy // n3CEV_button Related functionality /////////////////////////////////////////////////////////////////////////// getComponentElementById(this,\"n3CEV_btn\").on( \"click\", function(){ dxRequestInternal( // Divblox's wrapper function for doing a POST request to the server // Get's the path on the server where this component's .php file resides getComponentControllerPath(this), { f:\"checkEmailAddress\",// Indicates the function that .php file should execute email_address:getComponentElementById( this, \"baNsD_FormControlInput\" ).val(),// We are also // sending the email address as input to the .php file. // NB! CHECK YOUR ELEMENT ID. Divblox AUTO-GENERATES // THIS ID, MEANING YOURS MIGHT NOT BE \"baNsD_FormControlInput\" }, function(data_obj){ // The function that is called when the server provides a \"Success\" response showAlert( \"Success! The server responded with: \"+ data_obj.Message, \"success\", \"OK\", false ); }.bind(this), function(data_obj){ // The function that is called when the server // does NOT provide a \"Success\" response showAlert( \"Oh no! The server responded with: \"+ data_obj.Message, \"error\", \"OK\", false ); }.bind(this) ); }.bind(this) ); //////////////////////////////////////////////////////////////////////////////// We are currently not getting a successful response from the server because we still need to implement a function on the server to handle our request. We can do this by adding the following as a function to our component's .php file Copy publicfunctioncheckEmailAddress(){ $EmailAddressStr=$this->getInputValue('email_address'); if(is_null($EmailAddressStr)||(strlen($EmailAddressStr)<2)){ $this->setResult(false); $this->setReturnValue(\"Message\",\"No email address provided\"); $this->presentOutput(); } $this->setResult(true); $this->setReturnValue(\"Message\",\"You provided the email: $EmailAddressStr\"); $this->presentOutput(); } "},{"title":"Step 5 - Finishing Up","type":1,"pageTitle":"Hello World Example","url":"docs/common-examples-hello-world-example#step-5---finishing-up","content":"Divblox will show a landing page for the anonymous user by default. If we want to rather load our newly created page as the default we need to tell Divblox this by updating our project.js file: Copy // Update the user_role_landing_pages array to force the anonymous user to load our new page: let user_role_landing_pages ={ anonymous:\"hello_world\", Administator:\"system_account_management\", //\"User\":\"the_desired_landing_page\" }; And that's it! We just created a basic app that touches on the core Divblox concepts. We can now view our app at:http://localhost/my-app/ or http://localhost/ (if you project is in the webserver's root path) "},{"title":"Ways to get started","type":0,"sectionRef":"#","url":"docs/getting-started-ways-to-get-started","content":"","keywords":""},{"title":"Sandbox vs local deployment","type":1,"pageTitle":"Ways to get started","url":"docs/getting-started-ways-to-get-started#sandbox-vs-local-deployment","content":"In a local deployment environment, more advanced features are available: You can configure your server variables and modulesYou can configure your database connectionsYou can export to nativeYou have full access to your entire code baseYou can deploy your app to another deployment environment A deployment environment is defined as an instance of a project, that is hosted on a web server, which is accessible by a user. These can include, but are not limited to: A local web server on a development computerA cloud based web server "},{"title":"Linux Deployment","type":0,"sectionRef":"#","url":"docs/linux-deployment","content":"","keywords":""},{"title":"Linux Deployment","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#linux-deployment","content":"This section will take you through how to get a linux (Ubuntu 18.04 LTS) system up and running to build with Divblox. We will configure a LAMP stack, necessary permissions and IonCube to run Divblox. "},{"title":"Install Ubuntu 18.04 image","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#install-ubuntu-1804-image","content":"At time of writing, the latest stable version is Ubuntu 18.04, found here. Download the .iso file (approx. 5 GB). "},{"title":"Create a bootable flashdrive","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#create-a-bootable-flashdrive","content":"You will need a USB stick or portable hard drive with at least 8 GB of memory. Note that your memory device will be formatted upon completion of this step. danger All previous data will be erased! Download Rufus (recommended) here, or another similar application which will enable you to turn your memory stick into a bootable device. Continuing with Rufus: When you insert your USB stick, the device field should automatically update. You want to select the chosen memory stick, and FreeDOS as your boot selection. Now you can select the image you want to use. The default search folder is Downloads, so if this is your first attempt you should only see the newly downloaded image file. Leave all the other settings as default. You may rename the device as you wish. Below are some snapshots of the process. Without USB stickWith USB stick and .iso selectedComplete installation Once you are done, you can click Close and eject your newly bootable flash disk. "},{"title":"Restart machine with flashdrive inserted","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#restart-machine-with-flashdrive-inserted","content":"To boot with Linux Ubuntu, you need to restart your chosen computer with the flash drive inserted. Your computer may automatically boot up from the drive, but if not, you need to press F1, F2, ESC, F8 or F10 as your PC is starting (depending on system). Below are screenshots of the Ubuntu installation process. Install UbuntuSelect LanguageInstall optionsInstall TypeLocationPersonalInstall ProcessInstall CompleteSign In Select Install Ubuntu. You are also able to try Ubuntu without installing it. "},{"title":"Configure base Ubuntu OS","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#configure-base-ubuntu-os","content":"In this step we will make minor changes to suite our needs. As you log in you will be prompted to do a few things: WelcomeLivepatchUbuntu FeedbackApp StoreUbuntu Update Note the changes to UX in newer version of Ubuntu "},{"title":"Configuring a deployment environment","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#configuring-a-deployment-environment","content":"To do this, we need a webserver, backend support as well as database system. Webserver: Apache2# There are two main web servers you can use, namely Apache and Nginx, and either would work as they are both fast, secure, reliable and most importantly well-supported. We will be using Apache as it is the most popular. We will now be working in the Linux terminal, which can be found in your applications or opened by pressing CTRL+ALT+T. It is important to note that Linux works differently to Windows when it comes to permissions. When running any memory-changing commands like install, update or delete you will not be able to unless you do it as a superuser. Luckily this is very simple to do. All you have to do is type sudo before any of the commands. Another thing to note is that to cancel any given command while it is still running, all you need to do is press CTRL+C. The following commands will: Install Apache Copy sudo apt install apache2 You will be prompted to confirm whether you want to continue, and will be shown how much memory will be stored. You will also see all the files being unpacked. Start the Apache Server Copy sudo systemctl start apache2 Enable Apache to automatically start up when the server boots up Copy sudo systemctl enable apache2 Check the current status of the web service Copy sudo systemctl status apache2 And now we can press CTRL+C to exit the status command. We can see that Apache is up and running. Server Language: PHP 7.3# For this step we will use Ondřej Surý’s PPA (Personal Package Archive) as it has everything we will need. To do this we will firstly need to install software-properties-common and python-software-properties package. Copy sudo apt install software-properties-common python-software-properties Once the installations are complete we can proceed to add the PPA: Copy LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php You can read about what is included in the PPA and click enter to start the download. We then update the sources to reflect any changes. Copy apt update We now install PHP 7.3 by running and confirming the following command. Copy sudo apt install php7.3 php7.3-cli php7.3-common We can also check if we already have PHP 7.3 by checking the current version: Copy php -v This should give us: Now, we will install the most commonly used PHP modules with the following command: Copy sudo apt install php-pear php7.3-curl php7.3-dev php7.3-gd php7.3-mbstring php7.3-zip php7.3-mysql php7.3-xml php7.3-fpm libapache2-mod-php7.3 php7.3-imagick php7.3-recode php7.3-tidy php7.3-xmlrpc php7.3-intl If you want to install any further modules you can search for them with this command: Copy sudo apt-cache search php7.3 Or if you want to see all PHP modules available in Ubuntu, you can do so with: Copy sudo apt-cache search --names-only ^php To make PHP 7.3 the default (although it should already be it), run: Copy sudo update-alternatives --set php /usr/bin/php7.3 To reflect any changes, we must always resstart the apache server: Copy sudo systemctl restart apache2 Our PHP should now be working, but we can't yet test it as Linux permissions will not let us write into files so easily. Lets install a different File Explorer which can open up files as a superuser. The original file explorer is called Nautilus and we will be replacing it with Nemo. To download Nemo: Copy sudo apt install nemo Once it is installed you can find it with your other applications. Both Nautilus and Nemo are called 'Files', but you can distiguish them by the icons. We want to use Nemo, which is the orange file icon. Once opened, navigate to File System and then /var/www/html . Right-click and open as root. You will be prompted to input your root password, after which you have writing access to that folder. You can now create an empty document with extension .php. (Ours is called phpTest.php) Copy the following code into your test php file: <?php phpinfo(); ?>. This function just displays current php version and information when called. If the file states 'read-only' it means you did not enter the folder with root access. Once the code is saved, you can navigate to any browser, and search the URL http://localhost/phpTest.php and you should be able to see the following: If this is not the page you see, you have done something wrong. Check each step and make sure you have saved the file in the correct folder. Once PHP is installed and configured, we can move on and setup our database management. Database Server: MySQL# MySQL is a relational database management system based off of the language SQL. We create databases to structure collections of data. To install MySQL and all the relevant dependacies we run the following commands: Copy sudo apt-get update sudo apt-get install mysql-server To set up some configurations we run: Copy sudo mysql_secure_installation utility The following options can be answered on the preference of the user, but here is what settings we used: Validate Password Plugin: NOEnter own passwordConfirm new passwordRemove Anonymous Users: YESDisallow root login remotely: YESRemove Test Database and access to it: YESReload Privilege Tables Now: YES To ensure that the database server launches automatically even after a reboot: Copy sudo systemctl enable mysql Now start the mysql shell (the most basic way of working with the mysql server): Copy sudo mysql -u root -p You will be prompted for a password, this is the root password you set up on installation. Once you are in the mysql shell, you can execute mysql commands and queries. Note that using all caps for sql query commands is standard, but is not syntax-enforced. Lets start with how to reset the root password: Copy UPDATE mysql.user SET authentication_string = PASSWORD('new_password') WHERE User = 'root'; Whenever we need to change user information, we must run the following command to reflect the changes: Copy FLUSH PRIVILEGES; To see what users are stored in the database, we run the following command: Copy SELECT User, Host, authentication_string FROM mysql.user; We select the columns User, Host and authentication_string from the table user in the database mysql. This is the expected result: YOu can exit the MySQL shell at any time by entering 'exit' and hitting enter. Database Manager: phpMyAdmin# phpMyAdmin is a web interface for database management. It is by far the most popular MySQL administration tool. To install it we run: Copy sudo apt update sudo apt install phpmyadmin php-mbstring php-gettext For server selection: Select Apache 2. Note that no option is selected by default. You need to press SPACE, TAB and then ENTER to confirm Apache as the option. Select ServerDB-Config Select Apache Server.Note that no option is selected by default. You need to press `SPACE`, `TAB` and then `ENTER` to confirm Apache as the option. Once the installation is done, we need to explicitly enable the mbstring Php extension, Copy sudo phpenmod mbstring And now restart the Apache2 server to reflect changes. Copy sudo systemctl restart apache2 Now that phpMyAdmin is installed and configured, the last step is to make sure that your MySQL users have the permissions to interact with the program. Ubuntu systems running newer MySQL versions (5.7 or later) authenticate the root user using an auth-socket instead of a password. This methos is more secure and allows for more usability in some cases, but is also troublesome when trying to give external programs like phpMyAdmin access to the user. To circumvent this and be able to log into phpMyAdmin as your current user, we need to set the authentication method to mysql_native_password. Enter the MySQL shell and enter your password Copy sudo mysql -u root -p We can check the authentication type of each of your MySQL users running the following command: Copy SELECT user,authentication_string,plugin,host FROM mysql.user; "},{"title":"Permissions","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#permissions","content":"Now we have a basic LAMP stack set up, and can proceed to download the latest version of Divblox here. Once the zipped file is downloaded, copy it into /var/www/html and unzip it. To allow Divblox and ourselves to edit and write to that destination, we will have to edit the linux user and group permissions to that directory. This is done by giving the web server access to local storage. First we add our current user to the 'www-data' group Copy sudo usermod -a -G www-data divblox Then we give recursive ownership of /var/www/html to 'www-data'. Copy sudo chown -R www-data /var/www/html We then set the permissions of the /var/www/html folder recursively to give read, write and execute permissions to the group of /var/www/html. This is necessary as we need to have the permission to edit files from a local editor, as well as let Divblox auto-generate files from the web server. Copy sudo chmod -R 2770 /var/www/html sudo chmod -R g+rwxs /var/www/html "},{"title":"Download and configure Divblox","type":1,"pageTitle":"Linux Deployment","url":"docs/linux-deployment#download-and-configure-divblox","content":"You should now be ready to install Divblox. Go to Github and download the zip file. Extract it in the /var/www/html folder. Ensure that your web server is running and navigate to http://localhost/ or http://localhost/[your-project-folder] (if you placed Divblox within a sub folder). You should create an account with a secure password. Creating a project is explained here. Remember to copy the free Divblox license key to authenticate yourself in the setup page. To open the Divblox setup page, browse to http://localhost/Divblox or http://localhost/[your-project-folder]/Divblox, depending on your installation. You should be able to log in as the user Divblox using the password \"1\". Once in the Divblox setup page, we want to go to the Installation Checker. You will be prompted to log in as Divblox admin to proceed. The page provided is there to monitor and make sure all relevant dependencies are installed and configured to run Divblox, providing useful feedback on how to solve installation related problems. Start with the right side of the page, downloading the IonCube zip and file requested. Place them in the required folders and restart the Apache2 server. If everything was installed correctly, you should have green boxes indicating that the Divblox config files are all there, correct versions of PHP, MySQL and Apache2 are installed and configured, as well as the fact that we have the necessary permissions needed for Divblox basecamp to operate. BeforeAfter Select Apache Server.Note that no option is selected by default. You need to press `SPACE`, `TAB` and then `ENTER` to confirm Apache as the option. That's it! Everything is now configured and you are ready to start building apps with Divblox. "},{"title":"Configuration","type":0,"sectionRef":"#","url":"docs/the-basics-configuration","content":"","keywords":""},{"title":"Modules","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#modules","content":"Divblox allows you to define multiple modules for your project. Modules are useful for grouping related data objects. At least one module (The Main module) is required. Modules are essentially separate databases that ring-fence certain data objects. "},{"title":"Maintenance Mode","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#maintenance-mode","content":"Divblox has a built-in maintenance mode toggle, with the option to whitelist IP addresses, giving them access to the system during maintenance. When maintenance mode is active, access to the system is blocked on component and API level. "},{"title":"Environments","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#environments","content":"Divblox allows you to define multiple environments for your project (local, staging, testing, production, etc). When you start up Divblox for the first time, it will automatically generate the default (local) environment for your current project. info For an environment to function correctly, the following needs to be configured: Environment Name - Can be anything. This is just used to identify the environmentApp Name - The name of your app. This will be displayed as the document titleMaintenance Password - This is a password used internally by Divblox for sensitive operations, for example to drop a databaseThe server's host name or IP address - The url or IP address where this environment is deployedThe document root - The path to your web server's www folderSubdirectory - The sub directory in which your Divblox project resides (can be empty)The Database configuration for each module - The connection information for every module's database "},{"title":"UI Config","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#ui-config","content":"The UI configuration panel gives you quick access to your project logo and icon as well as your projects global theme CSS. Divblox uses SCSS to allow the user to define variables in the _theme-variables.scss file, and define the global themes in the theme.scss file. These files are located in the 'project/assets/css/scss/' folder. Upon saving these files in the setup page, the final theme.css file is compiled. info Please note that to generate your final CSS you need to click 'save' in the setup page. If you edited the files in an IDE, the changes will only reflect once you have instructed Divblox to compile the CSS. "},{"title":"JS Config","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#js-config","content":"The main configurable items for the Divblox javascript engine can be configured from the JS Config setup block. "},{"title":"SPA Mode","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#spa-mode","content":"Single Page Application mode is ideal for apps that will function in multiple forms, from web, to progressive, to native. Divblox allows you to configure your web app to run as a single page application or as a normal web application. When in SPA mode, Divblox does not open a new web page when loading a new page, but rather updates the DOM with the new page content. Divblox also handles the rooting challenges in the background. info If you are building for web only, it is recommended to turn SPA mode off. "},{"title":"Service Worker","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#service-worker","content":"Divblox allows you to configure a service worker to handle and cache requests. You can decide to toggle it on or off. It is also sometimes useful to force the service worker to reload when assets are modified. The service worker is the premise on which the idea of progressive applications are built. It acts as a form of advanced cache manager allowing offline browsing, push notifications and other 'native app' functionality. info When the service worker is on during development (debug), ensure that you have the option to \"Update on reload\" enabled in your browser. It is recommended to disable the service worker during development, since this can cause assets to be loaded from cache. "},{"title":"Debug Mode","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#debug-mode","content":"Debug mode enables robust logging of your web application. This is useful when in development mode, but can slow down your app in production environments. Disabling this mode disables the Divblox function dxLog();, removing all development logs with a switch of a button when your app is ready. Debug mode also changes the way the loadComponent() function works: when enabled, all caching is disabled. info It is recommended to turn debug mode off for production environments, but to keep it on in local/development environments. "},{"title":"Allow Feedback","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#allow-feedback","content":"Divblox allows you to toggle the project-wide feedback functionality on/off. When this is on, the user will always see a feedback button on the right of the screen. This will allow you to collect feature requests and bug reports for your pages. You can choose to have this enabled for use by end users, but it's strength lies in the fact that it can log the feedback to a specific component on a specific page, allowing developers, testers and the business side of the project to integrate more seamlessly. info Feedback is stored at basecamp.Divblox.com. This means that feedback is accessible project-wide, for any environment. "},{"title":"Global Functions","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#global-functions","content":"This set of files represents all the functions (set up into classes) that need to be available globally in your project. It is split up into 5 main files, as seen in the screenshot below. This includes both front-end javascript files as well as back-end php files. As mentioned before, whatever is edited in the project folder will override default Divblox functionality. The bulk of your project code will be either in these files, or in component-specific files. "},{"title":"Divblox.js","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#divbloxjs","content":"info Divblox.js is located at /Divblox/assets/js/Divblox.js Divblox.js is the main javascript function library that provides all of the core front-end Divblox functionality as globally available functions. It is required for Divblox to run and does the following: Manages the core dependenciesInitializes and prepares the DOMManages the state of your appManages the routing of your appManages the loading of components and component eventsProvides functions for components to communicate with their server side scriptsProvides various helper functions to simplify your app development Some commonly used Divblox.js functions are: Copy dxLog(Message, show_stack_trace); // A wrapper for console.log that provides more advanced logging capability // and can be turned off when in production mode dxRequestInternal( url, parameters, on_success, on_fail, queue_on_offline, element, loading_text ); // A wrapper for the jQuery $.post method with some additional Divblox functionality: // - Ensures that the on_success and on_fail callbacks receive a structured object // - Provides for queuing of request, either on- or offline // - Provides for disabling the calling element and displaying an appropriate message while // the request is handled // - Also manages authentication tokens between the front-end and back-end showAlert( AlertStr, Icon, ButtonArray, AutoHide, TimeUntilAutoHide, ConfirmFunction, CancelFunction ); // A wrapper for the sweetalert library that provides for nicer alerts showToast( title, toast_message, position, icon_path, toast_time_stamp, auto_hide ); // Allows for presenting a Bootstrap toast type message on the screen info Divblox.js should not be modified since the framework relies on its integrity. The developer should rather use project.js to override specific functions as required "},{"title":"Project.js","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#projectjs","content":"info project.js is located at /project/assets/js/project.js project.js is where the developer can add variables and functions that should always be globally available. It is a core dependency for Divblox and is always loaded directly after Divblox.js. This means that it can also be used to override specific Divblox.js functions as required. "},{"title":"Divblox Global Functions","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#divblox-global-functions","content":"info The API endpoint for the global functions is located at /project/api/global_functions.php The main purpose of the global functions is to handle any system-wide server requests. This is very useful when you want to create a general server function that can be reused in multiple places, instead of individually implemented per component. The script should always return a JSON string with at least one parameter called \"Result\". You can send a request to this script by using the following snippet: Copy dxRequestInternal( // The path to the global request handler on the server getServerRootPath()+\"project/assets/php/global_request_handler.php\", // The function to execute, along with additional inputs { f:\"aFunctionToExecute\", additional_input_variable:\"example\", }, function(data_obj){ // If the request was successful }, function(data){ // If the request was not successful } ); "},{"title":"Project Functions","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#project-functions","content":"info ProjectFunctions is an abstract class that is located at /project/assets/php/project_functions.php ProjectFunctions is intended to be used as a space where php functions that should be globally available can be housed. It extends the core Divblox class \"FrameworkFunctions\" that provides many of the core Divblox server side features. This means that you can call the following from any php script the requires Divblox.php: Copy ProjectFunctions::myFunction(); "},{"title":"Project Classes","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#project-classes","content":"info project_classes.php is a collection of globally available php classes and is located at /project/assets/php/project_classes.php. You can add any custom classes to the project_classes.php script to make them globally available. The core project classes are: ProjectAccessManager, which extends AccessManager and is responsible for managing access to components and objects, based on the currently logged in user info By default, component access is open to ANY user to allow the developer to get started quickly. This should be removed as soon as possible to enforce proper security for your solution: Copy publicfunctiongetComponentAccess($AccountId=-1,$ComponentName=''){ $InitialReturn=parent::getComponentAccess($AccountId,$ComponentName); if($InitialReturn==true){ returntrue; } // THIS LINE SHOULD BE REMOVED returntrue; // TODO: This is a temporary measure to allow you to get // started quickly without restrictions. Remove this and implement correctly // for your solution. NB! THIS GIVES ACCESS TO ALL COMPONENTS TO ANY USER!!! } ProjectComponentController, which extends ComponentController and allows for component php scripts to function correctlyFileUploader, which deals with storing files as data objects once they are uploaded to the serverPublicApi, which extends PublicApi_Base and provides the outline for how to expose a Divblox api via an endpoint "},{"title":"Media Library","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#media-library","content":"This is a visual tool to help with importing media for those who prefer a 'drag and drop' approach. Uploading a fileEditing an uploadUpload info "},{"title":"Navigation","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#navigation","content":"Divblox tries to simplify the generic processes of app creation as much as possible. As such, the navigation tab is where you can create menus which can be later placed into navigation components across your project easily. Below is a screenshot of how to use the navigation tab in the setup page. Menus are defined here, and later placed into navigation components. You can create the menus here, defining how many navigation items you have and what text and/or icons are displayed. It is then as simple as editing the class name in the navigation component to include or remove any project-wide menu you have created. The suggestion approach to handling navigation components is as follows: Create your desired menu (via navigation tab in setup page).Pick a suitable navigation bar and duplicate it using the component builder (naming it according to your project).Edit the classname in your newly created navigation component to menu-your-menu-name. Duplicating the navigation component is suggested so that the default navigation remain as templates. More importantly, this means that if any future Divblox updates change the default navigation, your project will not be affected. "},{"title":"Updates","type":1,"pageTitle":"Configuration","url":"docs/the-basics-configuration#updates","content":"When Divblox updates become available, you will be notified in the bottom right corner of the setup page. To prevent unexpected data loss, it is important to understand how the Divblox auto-updater works. When the auto-updater is run, core Divblox files will be replaced on your local machine with newer versions. This is why it is highly encouraged to leave Divblox-specific files untouched and use project-specific files to override any functionality. This is also a good time to stress the importance of using some sort of version control system like git to make sure that you have the ability to revert unintended changes. For users who would like to review each file change, or if the updater seems to not be working, you can update Divblox using a patch. This is found in basecamp. All that is required is for you to enter your current Divblox version. A zipped patch file will then be downloaded containing only the files changed between your version and the current version. "},{"title":"Divblox APIs","type":0,"sectionRef":"#","url":"docs/the-basics-divblox-apis","content":"","keywords":""},{"title":"Exposing a custom API","type":1,"pageTitle":"Divblox APIs","url":"docs/the-basics-divblox-apis#exposing-a-custom-api","content":"To expose any of your app's functionality as an api to the outside world, you simply need to create an endpoint in the following manner: Create a new php script that will contain your API operations and save it to the folder \"/project/api/\". Include \"divblox.php\" to use Divblox API classes. Copy require(\"../../divblox/divblox.php\"); Declare a new operation by calling the \"addApiOperation()\" function from the \"PublicApi\" class. The \"Expected outputs\", and \"operation description\" will be used when Divblox auto-generates the documentation for the newly created API, so do not overlook the importance of these description fields even though they don't directly effect functionality. The \"User readable name\" (API operation name) is later used to link operations to other API functionality. Copy PublicApi::addApiOperation( [\"Function Name\"], [\"Input Parameters\"], [\"Expected Outputs\"], [\"User readable name\"], [\"API operation description\"]); Initialize the API after your declarations using \"initApi()\" function from the \"PublicApi\" class. Copy PublicApi::initApi( [\"API description\"], [\"API Title\"]); Functionality is added to the API by adding functions with titles that correspond with the API Operation's \"Function Name\" parameter from step 3. Input parameters are retrieved using \"getInputParameter()\" along with the name of a parameter chosen in step 3.The API operation output is created using \"addApiOutput()\" for which there is no restriction to the amount of outputs you can have but should be inline with the operation deceleration's expected output.The API operation must be concluded by setting a return result to true or false, using \"setApiResult([\"Boolean\"])\", to say if the request was successful and by printing the response with \"printApiResult()\". Copy function[\"Function Name\"](){ $Variable= PublicApi::getInputParameter([\"Input Parameter\"]); // *** Add User Code Here *** PublicApi::addApiOutput(\"The Value of variable is\",\"$Variable\"); PublicApi::setApiResult([\"Boolean\"]); PublicApi::printApiResult(); } Using the steps provided a dummy operation has been created to demonstrate the creation and testing. The following API has an operation that concatenates a first and last name. A second operation, that shows the systems current time, is added to show how multiple operations and functions can occupy the same file. An example of a new API would look as follows: Copy require(\"../../divblox/divblox.php\"); // Start by declaring your operations and then calling the initApi function PublicApi::addApiOperation( \"dummyOperation\", [\"first_name\", \"last_name\"], [\"Message\"=>\"You called the example operation\", \"FullName\"=>\"[The resulting full name]\"], \"Example Operation\", \"This is simply an example api operation that takes a first name and a last name and concatenates them\"); PublicApi::addApiOperation( \"dummyOperation2\", [], [\"Message\"=>\"Current system time is [system_time]\"], \"Example Operation 2\", \"A simple operation that returns the current system time\"); PublicApi::initApi(\"Example API endpoint to illustrate the basics of divblox APIs\",\"Example Endpoint\"); // Operations functiondummyOperation(){ $Name= PublicApi::getInputParameter(\"first_name\"); $Surname= PublicApi::getInputParameter(\"last_name\"); PublicApi::addApiOutput(\"Message\",\"You called the example operation.\"); PublicApi::addApiOutput(\"FullName\",\"$Name$Surname\"); PublicApi::setApiResult(true);// You need to set this to true to indicate that the API executed successfully PublicApi::printApiResult(); } functiondummyOperation2(){ PublicApi::addApiOutput(\"Message\", \"Current system time is \".dxDateTime::Now()->format(DATE_TIME_FORMAT_PHP_STR.' H:i:s')); PublicApi::setApiResult(true);// You need to set this to true to indicate that the API executed successfully PublicApi::printApiResult(); } Divblox provides comprehensive documentation for your API's that can be viewed at \"/api/[your API name]/doc\". In the case of this example, where our API is called \"api_example\", the documentation is generated and can be viewed at \"/api/api_example/doc\". By following this documentation the newly exposed API's operation can be accessed by making a request to the given URL endpoint. The documentation will indicate to the reader whether or not the request requires an API key to access the operation, and in our case this API requires no authentication but does require other parameters such as a header, \"content-type\", and input parameters, \"first_name\" and \"last_name\", which are placed in the POST request body. After the request has been made the expected output should be inline with the expected output. Using an application, such as Postman as a test platform to see whether or not the API was successfully created, or alternatively by using the URL \"/api/api_example/dummyOperation/first_name=ACoolName/last_name=ACoolSurname\" we can compare the collected output to the expected response. Lastly , in the documentation, a dropdown menu is provided containing snippets of code that can be copied and placed into your project and language of choice to ease the integration. These snippets come in the most common languages that handle API requests. "},{"title":"Securing API operations with an API key","type":1,"pageTitle":"Divblox APIs","url":"docs/the-basics-divblox-apis#securing-api-operations-with-an-api-key","content":"When creating your own API it will be completely exposed and wont require and API key to be accessed. To secure your API operations we can add them to the list of API operations that are under \"api_operation_administration\" and create a key to control its accessibility. Using the \"api_example\" (see Exposing an API) to demonstrate this. It is shown in the documentation that the custom API does not require a key which is what we want to change. Add the API operation to the list of controlled operations by opening \"api_operation_administration\" component in the \"Component Builder\". This component allows you to add or remove API operations from the list of access controlled API operations. Add your operation by clicking the \"+ Api Operation\" Add the \"User Readable Name\" (API operation name)to the box and click \"Create\". This name is from your \"addApiOperation()\" (in your API's .php file) and is case and white space sensitive. By checking the API documentation we can compare the changes. Note that now the API does require a key to access the operation, and a API key can now be placed in the request body when the request is made to access the operation. To get a key that will link to your operation and allow you to access it, open the \"api_key_administration\" component from the \"Component Builder\" page and add a new API key by clicking the \"+ API key\". The key itself is auto-generated to avoid any internal clashes, as well as providing a strong key string. A start date is required. The end date can be left open which results in the key remaining active indefinitely. Fill in any additional information you may require and click \"Create\" to finish making the key. Your newly generated key should be visible in the \"api_key_administration\" component. Open up the new key to start adding operations to this key. Start adding operations to this key by clicking \"+ API Operation\" Select the operation you want to link with the API key. Operations that have been newly added are place at the bottom of the list. To verify that the operation has been added, it should be visible when viewing the API key. It will show the activity status of the operation which can be toggled to manage access. Testing the API operation with the API key should complete the API operation and return a \"success\" status message. "},{"title":"Creating default CRUD API's","type":1,"pageTitle":"Divblox APIs","url":"docs/the-basics-divblox-apis#creating-default-crud-apis","content":"Making and exposing access controlled CRUD API operations for new data model entities. Open the \"Data Modeler\" from the Divblox setup page. This is the Default data model for Divblox. New Entities are added by \"Double-clicking\" in empty space. Name your new data model entity and click \"Create\". The name of the data model entity is used as the CRUD API path as well. For this example an entity named \"Car\" will be created. Start by selecting the module where you want your new entity to be placed. Add new entity attributes by filling in the attribute \"name\" and \"data type\" and then add it with the green \" + \" at the end of the row. Add relationships in the same way, by selecting the entity you need a relationship with and add it using the green \" + \" at the end of the row. Finish by saving the changes made to the entity Sync your data model. Ensure that Divblox's data model is synced so that the CRUD operations can be used. View the auto-generated documentation for the newly created entity using the entity name from before and the path \"/api/[your API name]/doc\". These are the default CRUD operations that get created with new data model entities. Divblox automatically requires an API key for any auto generated operation as they get added and listed in \"api_operation_adminstration\" (For more see Securing API operations with an API key). Therefore we must create an API key and link it to the operations we want to expose, thus still keeping them secure with a key. Create the key in the \"api_key_administration\" component. Select the new key and add the operations using the \" + Api Operation\" button and select the operations you want from the drop down menu. The API operations are now exposed and can be accessed using the key that was just created. Testing the \"Create Car\" operation we will get back a confirmation that the data entry was successful and the \"List Car\" operation will show the new car that was created. "},{"title":"Dealing with Data Model Changes","type":0,"sectionRef":"#","url":"docs/common-examples-data-model-changes","content":"","keywords":""},{"title":"Adapting to Data Model Changes","type":1,"pageTitle":"Dealing with Data Model Changes","url":"docs/common-examples-data-model-changes#adapting-to-data-model-changes","content":"This section will briefly go over how to reuse and adjust components as needed even after you update the data model (e.g. You have added or removed some entity attributes). For this example we will have a very simple test entity called Car. Initially we will include the attributes 'Make', 'Model' and 'Year' and then sync and gen the data model. We create a full CRUD component from this entity, and show below how to update your components with new attributes. Below is the 'car_crud_create' component.html file. Here you can see the div with id=AdditionalInputFieldsWrapper where all additional attributes will automatically be placed. Copy <divid=\"ComponentWrapper\"class=\"component-wrapper\"> <divid=\"ComponentPlaceholder\"class=\"component_placeholder component_placeholder_data_view\"> <divid=\"ComponentFeedback\"></div> </div> <divid=\"ComponentContent\"class=\"component-content\"style=\"display:none\"> <divclass=\"container-fluid container-no-gutters\"> <divclass=\"row\"> <divclass=\"col-sm-6 col-md-4 col-xl-3\"> <divid=\"MakeWrapper\"class=\"entity-instance-input-field\"> {Make} </div> </div> <divclass=\"col-sm-6 col-md-4 col-xl-3\"> <divid=\"ModelWrapper\"class=\"entity-instance-input-field\"> {Model} </div> </div> <divclass=\"col-sm-6 col-md-4 col-xl-3\"> <divid=\"YearReleasedWrapper\"class=\"entity-instance-input-field\"> {YearReleased} </div> </div> </div> <divid=\"AdditionalInputFieldsWrapper\"class=\"row\"> <!-- Fields that are included by the component, but not provided for by wrappers will be rendered here... --> </div> <divclass=\"row\"> <divclass=\"col-md-6\"></div> <divclass=\"col-md-6\"> <divclass=\"row\"> <divclass=\"col-md-6\"></div> <divclass=\"col-md-6\"> <buttonid=\"btnSave\"type=\"button\"class=\"btn btn-primary fullwidth mt-1\">Create</button> </div> </div> </div> </div> </div> </div> </div> If you want to include them with custom bootstrap sizing and/or styles, just copy the structure of existing attribute fields, namely in the form of: Copy <divclass=\"col-sm-6 col-md-4 col-xl-3\"> <divid=\"AttributeNameWrapper\"class=\"entity-instance-input-field\"> {AttributeName} </div> </div> Where you replace the '{AttributeName}' token as well as the div id with the relevant attribute name (In our case it will be \"Weight\"). You can also then add CSS classes as needed. Any remaining included attributes without token placeholders will be placed in the AdditionalInputFieldsWrapper section. The final step would be to customise the way dxRenderer actually displays attribute fields in the DOM. By default it will create the input field based on the data type of the attribute. But, there are many data types available. Below is a screenshot of the renderInputField method of the dxRenderer object. The case statements are all possible input types, which the developer can manipuate as they like. Let's use an example. Let's say we know that all the cars on our system are released between the years 2010 and 2020. We want to restrict a user from being able to select a date in the 1200s for example. This is done in the file entity_definition.json located at project/assets/configurations/data_model/entity_definitions.json. The generic process is as follows: Define any lists you will be using in the file data_lists.json located at project/assets/configurations/data_model/generated/data_lists.jsonRefer to the file entity_definitions_base.json (which is generated by Divblox) to see the structure of entity definitions.Copy the entity's definition you would like to override into entity_definitions.json and make the necessary modifications.all note Do not edit in the base file as this will be regenerated every time you synchronise the database. "},{"title":"Customising the full CRUD component wrapper","type":1,"pageTitle":"Dealing with Data Model Changes","url":"docs/common-examples-data-model-changes#customising-the-full-crud-component-wrapper","content":"This section will briefly discuss how Divblox handles CRUD component interactions. When you create a full CRUD component in the component builder, this creates an inherently linked set of 4 components: The CRUD_create componentthe CRUD_update componentThe CRUD_data_series (or data_table) componentAnd a wrapper to house them, as well as relevant buttons and links. Deleting any of the children components will break the wrapper component. If you need individual components, you can create them as such. If, for example, you just want to remove the CRUD_create component, you can also do so, but there are a few things to notice. Firstly, you can go ahead and delete your CRUD_create component. Then you need to do some cleaning up. The highlighted code below signifies sections we will be removing. Remove the \"Create\" button as well as the div wrapper for the CRUD_create component from the wrapper CRUD component HTML. Copy <divid=\"ComponentWrapper\"class=\"component-wrapper\"> <divid=\"ComponentPlaceholder\"class=\"component_placeholder component_placeholder_general\"> <divid=\"ComponentFeedback\"></div> </div> <divid=\"ComponentContent\"class=\"component-content\"style=\"display:none\"> <divclass=\"container-fluid\"> <divclass=\"row mt-n4 crud-component-wrapper-data-list\"> <divclass=\"col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12\"> <divid=\"data_series_wrapper\"style=\"display:none;\"> <divid=\"data_series\"/> <divclass=\"row\"> <divclass=\"col-md-4\"></div> <divclass=\"col-md-4\"></div> <divclass=\"col-md-4\"> <buttonid=\"button_create\"type=\"button\"class=\"btn btn-outline-primary fullwidth mt-1\"><iclass=\"fa fa-plus\"aria-hidden=\"true\"></i> Car</button> </div> </div> </div> <divid=\"data_update_wrapper\"style=\"display:none;\"> <divclass=\"row\"> <divclass=\"col-md-4\"> <buttonid=\"button_update_back\"type=\"button\"class=\"btn btn-link crud-component-back-button-data-list\"><iclass=\"fa fa-long-arrow-left\"aria-hidden=\"true\"></i> Back</button> </div> <divclass=\"col-md-4\"></div> <divclass=\"col-md-4\"></div> </div> <divid=\"data_update\"/> </div> <divid=\"data_create_wrapper\"style=\"display:none;\"> <divclass=\"row\"> <divclass=\"col-md-4\"> <buttonid=\"button_create_back\"type=\"button\"class=\"btn btn-link crud-component-back-button-data-list\"><iclass=\"fa fa-long-arrow-left\"aria-hidden=\"true\"></i> Back</button> </div> <divclass=\"col-md-4\"></div> <divclass=\"col-md-4\"></div> </div> <divid=\"data_create\"/> </div> </div> </div> </div> </div> </div> Remove any orphaned JavaScript functionality relating to the CRUD_create component. Copy if(typeof component_classes['data_model_car_crud']===\"undefined\"){ classdata_model_car_crudextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[ { \"component_load_path\":\"data_model/car_crud_data_series\", \"parent_element\":\"data_series\", \"arguments\":{\"uid\":this.getUid()+\"_car_crud_data_series_component\"} }, { \"component_load_path\":\"data_model/car_crud_update\", \"parent_element\":\"data_update\", \"arguments\":{\"uid\":this.getUid()+\"_car_crud_update_component\"} }, { \"component_load_path\":\"data_model/car_crud_create\", \"parent_element\":\"data_create\", \"arguments\":{\"uid\":this.getUid()+\"_car_crud_create_component\"} }]; // Sub component config end } reset(inputs, propagate){ super.reset(inputs, propagate); this.toggleSubView(\"data_series_wrapper\"); } eventTriggered(event_name, parameters_obj){ // Handle specific events here. This is useful if the component needs to update because one of its // sub-components did something switch(event_name){ case'car_crud_create_clicked': this.toggleSubView(\"data_create_wrapper\"); getRegisteredComponent(this.getUid()+\"_car_crud_create_component\").reset(); break; case'car_clicked': this.toggleSubView(\"data_update_wrapper\"); getRegisteredComponent(this.getUid()+\"_car_crud_update_component\").reset(parameters_obj.id,true); break; case'car_created': case'car_deleted': case'car_updated': case'car_crud_back_clicked': this.toggleSubView(\"data_series_wrapper\"); getRegisteredComponent(this.getUid()+\"_car_crud_data_series_component\").reset(); break; default: dxLog(\"Event triggered: \"+ event_name +\": \"+JSON.stringify(parameters_obj)); } // Let's pass the event to all sub components this.propagateEventTriggered(event_name, parameters_obj); } registerDomEvents(){ getComponentElementById(this,\"button_create\").on(\"click\",function(){ pageEventTriggered(\"car_crud_create_clicked\",{}); }); getComponentElementById(this,\"button_create_back\").on(\"click\",function(){ pageEventTriggered(\"car_crud_back_clicked\",{}); }); getComponentElementById(this,\"button_update_back\").on(\"click\",function(){ pageEventTriggered(\"car_crud_back_clicked\",{}); }); } toggleSubView(view_element_id){ // Remove data_create_wrapper from the view_array let view_array =[\"data_series_wrapper\",\"data_update_wrapper\",\"data_create_wrapper\"]; getComponentElementById(this, view_element_id).fadeIn(\"slow\"); view_array.forEach(function(item){ if(item !== view_element_id){ getComponentElementById(this, item).hide(); } }.bind(this)); } } component_classes['data_model_car_crud']= data_model_car_crud; } And that is it! You can use similar logic to remove the update component or further customise the CRUD to your liking. "},{"title":"divbloxPHP Components","type":0,"sectionRef":"#","url":"docs/the-basics-divblox-components","content":"","keywords":""},{"title":"Component Types","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-types","content":"divbloxPHP allows you to create any of the following types of components: Data model related componentsPage componentsCustom componentsBasic components "},{"title":"Data model components","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#data-model-components","content":"Data model components allow for the CRUD behaviour of any of your data model entities. divbloxPHP can generate the following CRUD components from your data model: Create component: Displays input boxes for the relevant attributes of your entity along with a save button.Processes the request to the server to create a new instance of your entity and store it in the database Update component: Displays pre-populated input boxes for the relevant attributes of your entity along with a save & a delete button.Processes the request to the server to update or delete the existing instance of your entity in the database. Data Table component: Displays a tabular view with data from the database for the selected entity. The data can be searched and/or constrained as required.Allows multiple functions to be executed on the data set, including Excel,csv or text export and bulk deletion. Data List component: Displays a list view with data from the database for the selected entity.The data can be searched and/or constrained as required. This is intended to be a more mobile friendly way of viewing data for a specific entity. "},{"title":"Custom components","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#custom-components","content":"Custom components allow you to create any specific type of functionality for your app. Create anything from an image viewer or an html editor, to file uploaders and charting tools. Divblox ships standard with the following custom components: Data Visualization: ChartJS with examplesNavigation: Various types of navbars, including top, bottom and side navbarsSystem: Authentication: Allows for a user to authenticate or log in to your solutionFile upload: Provides a modern file uploader and processes the uploaded file on the serverImage upload: Similar to the file upload component, but for images. Provides basic image manipulation such as crop and resize, etc.Rich text editor: A wysiwyg editor that processes your html input and sends it to the serverNative versions of file and image upload, including native camera accessA basic image viewer "},{"title":"Page Components","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#page-components","content":"Page components are the parent components to any functionality in your application. You have default template options to choose from, each with a different navigation bar type, or even without one. "},{"title":"Basic components","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#basic-components","content":"Basic components are typically components that do not necessarily have a server-side requirement. These can range from simple alerts and buttons, to modals and progress bars. info divbloxPHP ships standard with the majority of bootstrap's UI components "},{"title":"Component DNA","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-dna","content":"Components are typically made up of the following parts: An HTML fileA CSS fileA Javascript fileA Php fileA JSON file info The exception here is basic components. These types of components usually reside within existing components to provide additional specialized functionality. "},{"title":"Component HTML","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-html","content":"The component's HTML file describes the component's layout. The basic structure of any component's HTML follows the following pattern: Copy <!-- The component wrapper is used to contain the content of the entire component --> <divid=\"ComponentWrapper\"class=\"component-wrapper\"> <!-- The component placeholder is shown first while the component is loading--> <divid=\"ComponentPlaceholder\"class=\"component_placeholder component_placeholder_general\"> <!-- Should an error occur, from which the component cannot recover, the component feedback is used to display the error to the user--> <divid=\"ComponentFeedback\"></div> </div> <!-- Once the component has successfully loaded, the component content replaces the component placeholder. Therefor it is hidden to start with--> <divid=\"ComponentContent class=\"component-content\"style=\"display:none\"></div> </div> "},{"title":"Component CSS","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-css","content":"The component's CSS file provides any special styling that is required by the component. By default this is empty "},{"title":"Component Javascript","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-javascript","content":"The component's Javascript file controls the component's behaviour. The basic structure of any component's Javascript follows the following pattern: Copy if(typeof component_classes[\"ungrouped_demo_component\"]===\"undefined\"){ classungrouped_demo_componentextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[]; // Sub component config end } } component_classes[\"ungrouped_demo_component\"]= ungrouped_demo_component; } As seen above, this class ungrouped_demo_component extends the DivbloxDomBaseComponent class from which every divbloxPHP component inherits. This class manages the following behaviour for each component: Loading workflow, which includes checking of prerequisites and dependenciesError handlingComponent & Subcomponent resetsEvent handling and propagation "},{"title":"Component Php","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-php","content":"The component's Php file handles server-side requests for the component. The basic structure of any component's Php follows the following pattern: Copy // We need to require the divbloxPHP initialization in order to // have access to divbloxPHP's classes and functions require(\"../../../../Divblox/Divblox.php\"); /* Every component controller class will inherit from the ProjectComponentController class. The ProjectComponentController handles things like - POST variables, - authentication, - script output, etc */ classDemoComponentControllerextendsProjectComponentController{ publicfunction__construct($ComponentNameStr='Component'){ parent::__construct($ComponentNameStr); } // An example function to show how we can deal with inputs and outputs. // This function is called by the constructor // when the $_POST variable \"f\" is set to \"exampleFunction\" publicfunctionexampleFunction(){ $ExpectedInputValue=$this->getInputValue(\"InputValue\"); if(is_null($ExpectedInputValue)){ // We did not receive the input we were expecting. // Let's fail the request $this->setResult(false); $this->setReturnValue(\"Message\",\"InputValue not provided\"); $this->presentOutput(); } $this->setResult(true); $this->setReturnValue(\"Message\",\"InputValue is $ExpectedInputValue\"); $this->presentOutput(); } } // Let's initialize the class to invoke the constructor. // This will do the initial request processing for us $ComponentObj=newDemoComponentController(\"demo_component\"); "},{"title":"Component JSON","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-json","content":"The component's JSON file is used for component configuration. By default this is empty. "},{"title":"How components work","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#how-components-work","content":"Every divbloxPHP component follows a certain recipe. The extremely high-level view of this is diagrammed below. info Each of Divblox's components communicates between client and server independently. A component is made up of 5 independent files, each responsible for their own unique functionality (as discussed in Component DNA). To make a call from the component JavaScript (front-end) to the component php (back-end) we will make use of the global function dxRequestInternal();. This is the default function to send a request to the server from the Divblox front-end and automatically takes care of some additional heavy lifting: It determines the current state of the connection to the server in order to either queue, deny or process the requestAutomatically disables the DOM node that triggered the request while the request is being processedManages sending and receiving the authentication token that identifies the current sessionEnsures that the result that is returned will always be a valid JSON objectExecutes a success or failure callback function based on the result received from the server Copy // dxRequestInternal() is the global function used to communicate // from the component's JavaScript to its back-end php component dxRequestInternal( // The first parameter tells the function where to send the request // getComponentControllerPath(this) returns the path to current component's php script getComponentControllerPath(this), // Tell component.php which function to execute { f:\"ourFunctionName\"}, function(data_obj){ // Success function. data_obj is a JSON object }.bind(this), function(data_obj){ // Fail function. data_obj is a JSON object }.bind(this) ); Our component.php script extends the class ProjectComponentController which takes care of the heavy-lifting with regards to processing the request on the server side: Processes all the input received from the requestReceives, evaluates and renews the authentication token that identifies the current sessionChecks whether the current request is allowed to be performedExecutes the function specified by the input variable f (\"ourFunctionName\")Deals with, and formats the returned data Copy // The function on our component controller that will be executed. // This function is executed when we pass \"ourFunctionName\" as // the value for \"f\" from our component JavaScript publicfunctionourFunctionName(){ // setReturnValue() sets the values in an array that will be returned as JSON // when the script completes. We always need to set the value for \"Result\" to either // \"Success\" or \"Failed\" in order for the component JavaScript to know // how to treat the response $this->setResult(true); // It is always a good idea to populate a \"Message\" for the front-end $this->setReturnValue(\"Message\",\"Some message about your result\"); // Here we set the value of any additional parameters to return $this->setReturnValue(\"SomeKey\",\"SomeValue\"); // \"presentOutput()\" returns our array as JSON and stops any // further execution of the current php script $this->presentOutput(); } "},{"title":"How Divblox loads a page","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#how-divblox-loads-a-page","content":"Remember, a page in Divblox is also a component and can be loaded into view just like any other component using the loadComponent(); function. However, the loading of the current page component forms part of the way that Divblox is initialized in our index.html file. Below, we will have a look at what this initialization flow looks like. When you navigate to your project root folder, the default index.html file ([project_root]/index.html) is loaded by the webserver. This file loads the initial CSS and JavaScript for Divblox: CSS Bootstrap 4Font-Awesomeproject.cssthemes.cssvariety of icons and splash images JavaScript JQuerydivblox.js To kick off Divblox's initialization workflow, the function initDx(); from divblox.js is called. This function starts the following chain of events: Checks whether we are in native mode or notIt then loads all necessary dependencies via the function loadDependencies();Then the function checkFrameworkReady(); is called: Check config parameters such as debug mode and SPA mode and act on themIf not in native, register eventhandlers for the progressive web app to be installedAlso register eventhandlers for online and offline states of our app The function on_divblox_ready(); is called: Check if it needs to prepare page inputs - based on input parameters in the URLIf we are not in native mode, SPA mode and all pages are prepared, then the page will be processed.The minimum needed for this is a ?view=[page_name] parameter in the URL Divblox will then load up the page in the body of index.html with the relevant page component "},{"title":"Component Builder","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#component-builder","content":"The Divblox component builder allows you to create and manage your project’s components in a visual environment and to combine various different components to create specific components for your project’s needs. From the default component builder page you can do the following: Search for existing components using the search bar at the top right of the pageOpen an existing component in order to work on itCreate a new component, either from an existing one or from scratch "},{"title":"Creating a new component","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#creating-a-new-component","content":"Clicking the + Component button brings up the following modal: Here you can give your new component a name and select the type of component you want to create. Creating a data model component# Selecting a Data Model Component will provide you with the options to select the entity for which you want to generate, as well as the type of data model component to create: Once you selected your entity and component type, you can configure the attributes and/or validations that you would like Divblox to generate for the component. The example below shows a typical configuration modal for a \"CREATE\" type of component, which allows you to configure which attributes to display and to select the specific validations you require for them: The example below shows a typical configuration modal for a \"DATA TABLE\" type of component, which allows you to configure which attributes to display in the table: Creating from an existing component# You can easily copy the functionality from an existing component be selecting the \"From Existing Component\" option. This will allow you to specialize the functionality of that component for your needs. info Notice the \"Grouping\" field above. This field allows you to group certain components together in folders for a better organized project. Creating a custom component# You can also create a custom component by selecting the \"Custom Component\" option. This simply allows you to provide a name and grouping for your component. info Custom components ALWAYS start out as blank components "},{"title":"Visual Builder Interface","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#visual-builder-interface","content":"The component builder visual interface allows you to create any type of layout that you might require. Using Bootstrap's grid system, it allows you to place the following containing elements on the screen to begin with: Containers: Containers are the most basic layout element in Bootstrap and are required when using the grid system. Choose from a responsive, fixed-width container (meaning its max-width changes at each breakpoint) or fluid-width (meaning it’s 100% wide all the time).Rows: Multiple rows can be placed inside each container and they serve the purpose of being wrappers for columnsColumns: A column is the final containing element within which you can place your componentsLearn more about Bootstrap's grid system here Adding a sub component# You can add a component as a sub component to the current page (which in itself is a component). You can also add basic components. See below: Adding custom html# You can add custom html to the current component. See below: Modify a component# The builder interface allows you to modify your component in the browser at any time. See below: "},{"title":"System Components","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#system-components","content":"Now that we have gone through what components actually are and how they work, let us look at the system components that come standard with any Divblox application. These components come by default in the Divblox installation and can be seen when opening the component builder for the first time. "},{"title":"Account Registration and Authentication","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#account-registration-and-authentication","content":"The account registration and authentication components take care of user registration and authentication. These components take care of input validations and they manage the hashing and verification of passwords. Account RegistrationAuthentication Performs basic validation (not empty) on input fieldsTakes care of backend validation checking uniqueness of username/emailManages password hashingCreates full name from first name and last nameDefaults user role to 'User'Creates an instance of the Account entity with provided details "},{"title":"Default File/Image Uploader","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#default-fileimage-uploader","content":"The default file and image uploader components handle the user interface for system storing of uploaded files and images. The difference between these two is only the file type and that images have basic image editing functionality before being saved. Both of the uploaders create an instance of the FileDocument entity, which stores all the relevant information about the uploaded file in the database. "},{"title":"Default Rich Text Editor","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#default-rich-text-editor","content":"A WYSIWYG text editor. You can pre-populate the text, and save the entries as needed. The default behaviour is to log out the input to the php error log. "},{"title":"Entity Select","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#entity-select","content":"In some cases, you need to select a specific entry of an entity from the database in order to link it to something else. When the database table for this entity becomes very large, it can have a performance impact when doing this with a standard drop down. This is where the entity select component comes in handy. It provides an input search box that allows the user to start searching for a specific entry and then displays a list of matching results from which the user can pick one. When the user clicks on a result, the corresponding ID is provided to the component. info The entity select component can be seen as an auto-complete for entities. "},{"title":"Native Camera and File/Image Uploader","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#native-camera-and-fileimage-uploader","content":"These provide the functionlity their names suggest, at a native level. "},{"title":"Profile Picture Uploader","type":1,"pageTitle":"divbloxPHP Components","url":"docs/the-basics-divblox-components#profile-picture-uploader","content":"This is a specific instance of the default image uploader which saves the image directly into your profile. "},{"title":"Data Modeler","type":0,"sectionRef":"#","url":"docs/the-basics-data-modeler","content":"","keywords":""},{"title":"Start at the beginning","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#start-at-the-beginning","content":"Every divbloxPHP project starts with the data model. A well designed data model can be the difference between an app that works brilliantly and one that just doesn't cut it. The divbloxPHP data modeler allows you to create and manage your app's data model in a visual environment. Once a data model is defined, the Data Modeler ensures that the underlying databases are synchronized to its specification and then generates the object relational model classes. This makes communication with your databases seemless and easy to manage in an object oriented way. "},{"title":"Vocabulary","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#vocabulary","content":"info Entity: The definition of an object that will be presented by a class in code and by a table in the database "},{"title":"Basic Data Modeling Concepts","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#basic-data-modeling-concepts","content":"The data model allows you define the following: All of your entities, their attributes, attribute types and their relationships to other entitiesThe user roles that your app will allow for Below is the data model for a simple example we will use iin this section. The data model in the above example describes the following: 4 Entities: \"Person\",\"Project\",\"Task\",\"SubTask\"A Person is described by 3 attributes: \"FirstName\",\"LastName\",\"EmailAddress\" of type \"VARCHAR(50)\",\"VARCHAR(50)\" and \"VARCHAR(50)\"A Project is described by 2 attributes: \"ProjectName\",\"DueDate\" of type \"VARCHAR(50)\" and \"DATE\"A Task is described by 4 attributes: \"TaskName\",\"Description\",\"DueDate\",\"TaskStatus\" of type \"VARCHAR(50)\",\"TEXT\",\"DATE\" and \"VARCHAR(50)\"A SubTask is described by 3 attributes: \"SubTaskName\",\"Description\",\"SubTaskStatus\" of type \"VARCHAR(50)\",\"TEXT\" and \"VARCHAR(50)\"A Task has a single relationship to both Person and Project, meaning a person and/or project can have multiple tasks associated with itA SubTask has a single relationship to a Task, meaning a Task can have multiple SubTasks linked to it The Divblox data model is broken up into 2 main parts: The System Entities - Defined by Divblox and used internally to perform certain core functions (Audit Logs, Authentication, File Management, etc)The Project Entities - Defined by the developer to serve the purposes of their project Below is a visual representation of the Divblox System Entities. These entities should not be edited, but rather reused where needed, since they might be affected by future Divblox updates. The developer is free to create relationships to these entities to leverage their existing functionality. The final data model for your app will be the combination of the system entities and the project entities. Once this is defined, Divblox can generate the app's object relational model and CRUD (Create, Read, Update, Delete) components. In essence, simply by defining a data model, you already have CRUD ability for every entity in your data model. More on this in later topics. Below is a visual representation of a complete Divblox data model for the example provided above. "},{"title":"The Divblox ORM (Server side)","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#the-divblox-orm-server-side","content":""},{"title":"ORM Code Generation","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#orm-code-generation","content":"Based on the data model, the following is generated or can be generated as required: The project's ORM: These are the ORM classes that describe the current underlying database. For each database table there will be corresponding ORM classes that will allow for the CRUD behaviour for that entity. In essence, the ORM caters for the communication with the database without the need for the developer to write sql queries.Data model related components: These are the components that allow for exposing the entity's CRUD functionality to the front-end. More on this can be found in the components section. Code is generated using the following approach: Each entity will have its own base classes for the ORM. These base classes will always be regenerated when code is generated to ensure that the foundation of your solution is always inline with the database. Each entity will then also have implemented classes that inherit from their base classes. These classes allow for the developer to change the way a certain class works from the default way, since the code in these classes will never be regenerated. This base class/implemented class approach is true for every area where Divblox generates ORM code. "},{"title":"ORM example case","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#orm-example-case","content":"Let's take some time to familiarize ourselves with where this ORM code is generated, how it is structured, and what is available. You can find all the ORM-related classes in project/assets/php/data_model_orm. Divblox generates all of the ORM-based code for each entity in EntityNameGen.class.php, and also generates a near-empty EntityName.class.php class for developers to use to override any necessary functionality. We will look through the task entity's TaskGen.class.php file and sift through what functions are available and which a developer will rarely use. The file contains 3 classes. The last two are only used for background functionality and developers should not worry about them too much. The first class, TaskGen is the important one that we will discuss. You are encouraged to peruse through these generated files further to acquant youself with the structure and available functionality. You will see that the first block of code sets all the necessary constants and variables needed in the specific entity. It then initializes each property with default values from the database. Next we will see the following: These are the class-wide load and count methods avaiable for this entity. The most commonly used functions are the last 3, where: Load() loads an instance of the Task entity by ID, allowing for optional clauses.LoadAll() retrieves all instances of the Task entity, again allowing optional clauses.CountAll() returns an integer count of rows (instances) of the Task entity table. We then have Divblox query related methods. The functions BuildQueryStatement(), QueryCursor() and GetSelectedFields() are used in the background by dxQuery. The function QueryArrayCached() is a remnant of old days when PHP was largely inefficient and MySQL queries and results were cached for improved performance. You should not need to use this. The remaining functions are: QuerySingle() QuerryArray()QueryCount() which return a single task object, an array of task objects or an integer count of objects respoectively. These functions will be discussed further in the next section. We then have index-based load methods, which perform 'select' queries by unique ID. These will be different for each entity depending on what relationships you have defined in the data model, as Divblox autogenerates methods for easy loading of relational data as well. As you can see, we are able to call functions LoadArrayByProject() and LoadArrayByPerson() as each task has a singlular relationship to these two entities. You can also perform \"count\" operations in a likewise manner. We then move on to the save, delete and reload methods. We will again skip over the DeleteCache() function as it won't be used in majority of cases. The remaining functions allow us to save and delete instances of tasks individually, as well as delete or truncate entire tables. The reload() function reloads the data from the database to try make sure you have the latest version. You also have access to the public overriders __get()and __set() which can be used to save you time by allowing dynamic access to attributes in the entity. The getVirtualAttribute() function is also not used as Divblox does not currently use virtualisation. We also have a set of functions that return information about the database. The first two are fairly straight-forward. The third function, getDatabaseIndex() is useful when you have more than one module (database connected to this project), as when working with intermodular relationships it makes quering easier. Finally, we have the Iterator() and getJson() functions which work hand in hand. The Iterator() function returns an associative array of the entity attributes, and the `getJson() function converts this to valid JSON and returns that. "},{"title":"Divbox ORM Queries","type":1,"pageTitle":"Data Modeler","url":"docs/the-basics-data-modeler#divbox-orm-queries","content":"The querying logic behind all the Load methods in Divblox ORM classes is powered by dxQuery, or dxQ for short. Put simply, dxQ is a completely object oriented API to perform any SELECT-based query on your database to return any result or hierarchy of your ORM objects. While the ORM classes utilize basic, straightforward SELECT statements in their Load methods, dxQ is capable of infinitely more complex queries. In fact, any SELECT a developer would need to do against a database should be possible with dxQ. At its core, any dxQ query will return a collection of objects of the same type (e.g. a collection of Task objects). But the power of dxQ is that we can branch beyond this core collection by bringing in any related objects, performing any SQL-based clause (including WHERE, ORDER BY, JOIN, aggregations, etc.) on both the core set of Task rows and any of these related objects rows. Every code generated class in your ORM will have the three following static dxQuery methods: QuerySingle: to perform a dxQuery to return just a single object (typically for queries where you expect only one row)QueryArray: to perform a dxQuery to return just an array of objectsQueryCount: to perform a dxQuery to return an integer of the count of rows (e.g. \"COUNT (*)\") All three dxQuery methods can take two parameters, a dxQ Condition and an optional set of dxQ Clauses. dxQ Conditions are typically conditions that you would expect to find in a SQL WHERE clause, including Equal, GreaterThan, IsNotNull, etc.dxQ Clauses are additional clauses that you could add to alter your SQL statement, including methods to perform SQL equivalents of JOIN, DISTINCT, GROUP BY, ORDER BY and LIMIT.And finally, both dxQ Condition and dxQ Clause objects will expect dxQ Node parameters. dxQ Nodes can either be tables, individual columns within the tables, or even association tables. dxQ Node classes for your entire ORM is code generated for you. The next few examples will examine all three major constructs (dxQ Node, dxQ Condition and dxQ Clause) in greater detail. Basic Example# Copy // Retrieve a single Task from the database $TaskObj= Task::QuerySingle(dxQ::All()); // Retrieve all Tasks from the database $TaskArray= Task::QueryArray(dxQ::All()); // Count all Tasks in the database $TaskCount= Task::QueryCount(dxQ::All()); // Retrieve all Tasks from the database where the TaskName is \"A test task\" $TaskArray= Task::QueryArray(dxQ::Equal(dxQN::Task()->TaskName,\"A test task\")); note Notice that dxQuery doesn't have any construct to describe what would normally be your SELECT clause. This is because we take advantage of the code generation process to allow dxQuery to automagically \"know\" which fields should be SELECT-ed based on the query, conditions and clauses you are performing. This will allow a lot greater flexibility in your data model. Because the framework is now taking care of column names, etc., instead of the developer needing to manually hard code it, you can make changes to columns in your tables without needing to rewrite your dxQuery calls. dxQuery Nodes# A dxQ Node is any object table or association table (type tables are excluded), as well as any column within those tables. dxQ Node classes for your entire data model are generated for you during the code generation process. But in addition to this, dxQ Nodes are completely interlinked together, matching the relationships that you have defined as foreign keys (or virtual foreign keys using a relationships script) in your database. To get at a specific dxQ Node, you will need to call dxQN::ClassName(), where \"ClassName\" is the name of the class for your table (e.g. \"Task\"). From there, you can use property getters to get at any column or relationship. Naming standards for the columns are the same as the naming standards for the public getter/setter properties on the object, itself. So just as $TaskObj->TaskName will get you the \"TaskName\" property of a Task object, dxQN::Task()->TaskName will refer to the \"Task.TaskName\" column in the database. Naming standards for relationships are the same way. The tokenization of the relationship reflected in a class's property and method names will also be reflected in the dxQ Nodes. So just as $TaskObj->PersonObject will get you a Person object which is the manager of a given Task, dxQN::Task()->PersonObject refers to the Task table's row where Task.id = Task.person_id. And of course, because everything that is linked together in the database, is also linked together in your dxQ Nodes, dxQN::Task()->PersonObject->FirstName would of course refer to the Person's first name of the relevant Task. dxQuery Conditions# All dxQuery method calls require a dxQ Condition. dxQ Conditions allow you to create a nested/hierarchical set of conditions to describe what essentially becomes your WHERE clause in a SQL query statement. The following lists the dxQ Condition classes and what parameters they take: dxQ::All()dxQ::None()dxQ::Equal(dxQNode, Value)dxQ::NotEqual(dxQNode, Value)dxQ::GreaterThan(dxQNode, Value)dxQ::LessThan(dxQNode, Value)dxQ::GreaterOrEqual(dxQNode, Value)dxQ::LessOrEqual(dxQNode, Value)dxQ::IsNull(dxQNode)dxQ::IsNotNull(dxQNode)dxQ::In(dxQNode, array of string/int/datetime)dxQ::Like(dxQNode, string) For almost all of the above dxQ Conditions, you are comparing a column with some value. The dxQ Node parameter represents that column. However, value can be either a static value (like an integer, a string, a datetime, etc.) or it can be another dxQ Node. And finally, there are three special dxQ Condition classes which take in any number of additional dxQ Condition classes: dxQ::AndCondition()dxQ::OrCondition()dxQ::Not() - \"Not\" can only take in one dxQ Condition class (conditions can be passed in as parameters and/or as arrays). Because And/Or/Not conditions can take in any other condition, including other And/Or/Not conditions, you can embed these conditions into other conditions to create what ends up being a logic tree for your entire SQL Where clause. Below are a few examples of dxQuery in practice. Copy //Select all Tasks where the TaskName is alphabetically \"greater than\" the Description $TaskArray= Task::QueryArray( dxQ::GreaterThan( dxQN::Task()->TaskName, dxQN::Task()->Description) ) ); //Select all Tasks where the person's FirstName is alphabetically // \"greater than\" their LastName, or who's name contains \"Website\" $TaskArray= Task::QueryArray( dxQ::OrCondition( dxQ::GreaterThan( dxQN::Task()->PersonObject->FirstName, dxQN::Task()->PersonObject->LastName), dxQ::Like(dxQN::Task()->Name,'%Website%') ) ); //Select all Tasks where the Task ID <= 2 AND (the person's FirstName // is alphabetically \"greater than\" the Description, or who's name contains \"Website\") $TaskArray= Task::QueryArray( dxQ::AndCondition( dxQ::OrCondition( dxQ::GreaterThan( dxQN::Task()->PersonObject->FirstName, dxQN::Task()->PersonObject->LastName), dxQ::Like(dxQN::Task()->Name,'%Website%') ), dxQ::LessOrEqual(dxQN::Task()->Id,2) ) ); dxQuery Clauses# All dxQuery method calls take in an optional set of dxQ Clauses. dxQ Clauses allow you alter the result set by performing the equivalents of most of your major SQL clauses, including JOIN, ORDER BY, GROUP BY and DISTINCT. The following lists the dxQ Clause classes and parameters they take: dxQ::OrderBy(array/list of dxQNodes or dxQConditions)dxQ::GroupBy(array/list of dxQNodes)dxQ::Having(dxQSubSqlNode)dxQ::Count(dxQNode, string)dxQ::Minimum(dxQNode, string)dxQ::Maximum(dxQNode, string)dxQ::Average(dxQNode, string)dxQ::Sum(dxQNode, string)dxQ::LimitInfo(integer[, integer = 0])dxQ::Distinct()dxQ::Select(array/list of dxQNodes) Explanation OrderBy and GroupBy follow the conventions of SQL ORDER BY and GROUP BY. It takes in a list of one or more dxQ Column Nodes. This list could be a parameterized list and/or an array. Specifically for OrderBy, to specify a dxQ Node that you wish to order by in descending order, add a \"false\" after the dxQ Node. So for example, dxQ::OrderBy(dxQN::Task()->Description, false,dxQN::Task()->TaskName) will do the SQL equivalent of \"ORDER BY Description DESC, TaskName ASC\". Count, Minimum, Maximum , Average and Sum are aggregation-related clauses, and only work when GroupBy is specified. Having adds a SQL Having clause, which allows you to filter the results of your query based on the results of the aggregation-related functions. Having requires a Subquery, which is a SQL code snippet you create to specify the criteria to filter on. (See the Subquery section later in this tutorial for more information on those). LimitInfo will limit the result set. The first integer is the maximum number of rows you wish to limit the query to. The optional second integer is the offset (if any). Distinct will cause the query to be called with SELECT DISTINCT. The select clause will allow you to manually specify the specific columns you wish to return for the query. info All clauses must be wrapped around a single dxQ::Clause() call, which takes in any number of clause classes for your query. Further example using dxQ::Clause: Copy //Select all Tasks, Ordered by Description then TaskName $TaskArray= Task::QueryArray( dxQ::All(), dxQ::Clause( dxQ::OrderBy( dxQN::Task()->Description, dxQN::Task()->TaskName ) ) ); //Select all Tasks, Ordered by Description then TaskName, Limited to the first 4 results $TaskArray= Task::QueryArray( dxQ::All(), dxQ::Clause( dxQ::OrderBy( dxQN::Task()->Description, dxQN::Task()->Name ), dxQ::LimitInfo(4) ) ); "},{"title":"Useful Resources","type":0,"sectionRef":"#","url":"docs/useful-resources","content":"","keywords":""},{"title":"Code Snippets","type":1,"pageTitle":"Useful Resources","url":"docs/useful-resources#code-snippets","content":"This section is dedicated to giving the user an easy to use copy-paste template library of common Divblox functions. Javascript to perform an ajax request to the current component's php controller file Copy dxRequestInternal(getComponentControllerPath(this),{ // parameters fed to the backend f:\"backend_function_name\", additional_parameters:\"parameter1\" }, function(data_obj){ // Success function }.bind(this), function(data_obj){ // Failure function }.bind(this)); PHP function template for receiving an ajax call and presenting an output to the calling function Copy publicfunctionFunctionName(){ // Function operations $SomeReturnValue=[]; $this->setResult(true); $this->setReturnValue(\"ReturnData\",$SomeReturnValue); $this->presentOutput(); } Javascript example of how to add an event handler to an html element residing inside a component Copy getComponentElementById(this,\"ElementId\").on(\"click\",function(){ //Function to be implemented }); "},{"title":"Divblox Public Shared","type":1,"pageTitle":"Useful Resources","url":"docs/useful-resources#divblox-public-shared","content":"Divblox shares a public Google Drive folder with various resources to ease your development workflow. This shared folder can be found here "},{"title":"Troubleshooting","type":0,"sectionRef":"#","url":"docs/troubleshooting","content":"","keywords":""},{"title":"Most common issues","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#most-common-issues","content":""},{"title":"Browser cache","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#browser-cache","content":"All of your Divblox app's assets (html, javascript & css files) will, by default, be cached by your browser. It is therefore important to take this into account when building new functionality. If something does not work the way you expect it to, the most common cause is that the old functionality was cached. The easiest way around this is to have the browser's console open while developing and to disable caching through the options provided. It is also a good idea to always perform a cache-refresh when refreshing your page. "},{"title":"Browser console","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#browser-console","content":"Divblox makes use of the browser console to inform the developer of errors. Ensure that the browser console is open during installation and/or development to quickly diagnose the most common problems. "},{"title":"Installation checker","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#installation-checker","content":"The installation checker might fail if you do not already have a database connection configured. If your minimum database requirements are not met according to the installation checker, please firstconfigure your database settings before checking again. "},{"title":"Linux","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#linux","content":"Divblox makes use of code generation to create its components and ORM class files. This means that full write access is required in order for everything to work correctly. To avoid most of the common issues in this regard, ensure that the user that is used to run your web server has ownership and write permission for the Divblox folder. "},{"title":"Windows","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#windows","content":"Divblox was created on a UNIX based operating system and it is recommended that you use MacOS for the best experience when developing with the framework. However, we do try to support Windows as far as possible. The following are common issues users experience when setting up Divblox on Windows environments: Basecamp communication error (SSL)# The installation wizard complains that the connection to basecamp cannot be established because of an SSL Certificate problem. Specifically: unable to get local issuer certificate. To solve this problem, follow these steps: Download cURL's cacert.pem certificate from the official cURL website hereSave this cacert.pem file in your webserver's root folder, for example C:\\MAMP\\cacert.pemUpdate your php.ini file to tell cURL where to locate this certificate. Add the following to the bottom of your php.ini file: Copy curl.cainfo=\"C:\\MAMP\\cacert.pem\" openssl.cafile=\"C:\\MAMP\\cacert.pem\" Note, that your path may be different, depending on the webserver and path you chose to save the file. Next, you need to enable mod_ssl and php_openssl.dll. To enable mod_ssl, you can add the following to your Apache configuration file: Copy LoadModule ssl_module C:\\MAMP\\bin\\apache\\modules\\mod_ssl.so Note, that your path may be different, depending on the webserver. To enable php_openssl.dll, you will need to uncomment the following line in your php.ini file: Copy extension=php_openssl.dll Restart your webserver and refresh the Divblox installation checker to confirm that things are working now. "},{"title":"Lowercase table names","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#lowercase-table-names","content":"Divblox relies on code generation to create the ORM for you project. This generation is done, based on the tables in your database. If the tables in your database do not have case-sensitive names, the classes in the corresponding ORM might fail to work correctly. The installation checker will display the following error: Copy Database table names case configuration: Failed! Please ensure that the database variable 'lower_case_table_names' is set to 2 To solve this, open your mysql configuration file and add the following: Look for: # The MySQL server [mysqld] and add this right below it: Copy lower_case_table_names = 2 Save the file and restart MySQL service "},{"title":"ionCube","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#ioncube","content":"When you open your Divblox project in the browser for the first time, you might encounter strange behaviour if ionCube has not been installed. Please follow these steps to install ionCube for your OS. Sandboxes# "},{"title":"Re-initializing a sandbox","type":1,"pageTitle":"Troubleshooting","url":"docs/troubleshooting#re-initializing-a-sandbox","content":"You can re-initialize your sandbox at any time by simply removing it and initializing it again. note Important! This process removes ALL files and data from your sandbox Click the \"Modify Project\" icon to open your projectClick the tab \"Environments\"Click the red trashcan to remove on your sandboxFollow the steps in Getting Started to initialize a new sandbox for your project "},{"title":"Native Support","type":0,"sectionRef":"#","url":"docs/the-basics-native-support","content":"","keywords":""},{"title":"Push Notifications","type":1,"pageTitle":"Native Support","url":"docs/the-basics-native-support#push-notifications","content":"info divbloxPHP push notifications implement Firebase Cloud Messaging. API integration to register and send push notifications is already present in every Divblox project Support for push notifications on the native device is provided for by an API that allows for the registration of devices and their push registration tokens. The developer needs to implement the native library of their choice to handle push notifications on the device. "},{"title":"Server-side","type":1,"pageTitle":"Native Support","url":"docs/the-basics-native-support#server-side","content":"In order to be able to send push notifications we need to have devices registered with push registration tokens. This data is stored in the entity \"PushRegistration\". A PushRegistration is device-specific, but divbloxPHP will link the PushRegistration to the relevant Account if an authentication takes place. To register a device's push registration token we will use this api operation: /api/global_functions/updatePushRegistration.You can read more about how this operation works by visiting its docs page: /api/global_functions/updatePushRegistration/docs Push notifications can be sent from the server using the built-in divbloxPHP Project Functions. In this example we want to send a push notification to all users with the role \"Administrator\": Copy $PushRegistrationArray= PushRegistration::QueryArray( dxQ::Equal( dxQN::PushRegistration()->AccountObject->UserRoleObject->Role,\"Administrator\" ) ); $TokenArray=[]; foreach($PushRegistrationArrayas$PushRegistrationObj){ $TokenArray[]=$PushRegistrationObj->RegistrationId; } ProjectFunctions::deliverBatchedPushPayload( $TokenArray, \"Admin message title\", \"Here is a message only for admins\"); "},{"title":"Device-side","type":1,"pageTitle":"Native Support","url":"docs/the-basics-native-support#device-side","content":"Divblox does not implement the device-side functionality to deal with push notifications by default, because: There are many different flavours for frontend implementation (search npm registry for \"react native push notifications\"). The developer can choose the implementation of their choiceSetting up push notifications with Firebase Cloud Messaging (FCM) involves quite a few configuration steps for both Android and iOS. If the resulting configuration files are present in the React Native project, but not properly set up, the build process will fail However, please see below a working example of how to implement this functionality in a freshly exported Divblox project: Setting up the prerequisites# Open your browser and go to Google Firebase Console. Then log in using your Google account From that page, click the \"+\" add project button to create a Google Firebase project Follow the steps to create your project and then add both an Android and an iOS app. When creating your Android and iOS apps, ensure that the \"Android package name\" and \"iOS bundle ID\" matches the \"Widget Id\" as configured in your environment You do not need to do the steps involving adding Firebase SDK & verification of installation for now After all the setup is complete, you need to have downloaded the following 2 files for Android and iOS: google-services.json (Android)GoogleService-info.plist (iOS) On the Firebase Project Overview page, ensure that your newly created project is selected and click on either the iOS or Android app to go to the project settings page Click on Cloud Messaging. You want to now copy the Server Key into your Divblox project: In /divblox/config/framework/config.php update the value for \"FIREBASE_SERVER_KEY_STR\" to match your Server Key For iOS you will need to get an APNs Authentication Key. You can do this by following the steps provided by Apple at:https://developer.apple.com/account/resources/authkeys/list. Once you have the APNs key, upload it to your iOS app in Firebase Configuring React Native# Open /native/[Your Environment Name]/ios/[Your Environment Name].xcworkspace using Xcode. Ensure that the Bundle Identifier matches the \"Widget Id\" as configured in your environment.Edit /native/[Your Environment Name]/android/app/src/main/java/com/[Your Environment Name]/MainActivity.java, ensure the first line package [xxx] is package [Your Widget Id]Edit /native/[Your Environment Name]/android/app/src/main/java/com/[Your Environment Name]/MainApplication.java, ensure the first line package [xxx] is package [Your Widget Id]Edit the value for \"package\" to match your Widget Id in /native/[Your Environment Name]/android/app/src/main/AndroidManifest.xmland add the following after INTERNET permissions: Copy <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" /> <uses-permission android:name=\"android.permission.VIBRATE\" /> In the same file, add the Firebase MESSAGING_EVENT before the closing of the <application> tag: Copy <application ...> //... <service android:name=\"io.invertase.firebase.messaging.RNFirebaseMessagingService\"> <intent-filter> <action android:name=\"com.google.firebase.MESSAGING_EVENT\" /> </intent-filter> </service> </application> Edit the value for \"applicationId\" to match your Widget Id in /native/[Your Environment Name]/android/app/build.gradle: Copy android { //... defaultConfig { applicationId \"[Your Widget Id]\" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName \"1.0\" } //... } Edit the value for \"package\" to match your Widget Id in /native/[Your Environment Name]/android/app/BUCK Copy android_build_config( name = \"build_config\", package = \"[Your Widget Id]\", ) android_resource( name = \"res\", package = \"[Your Widget Id]\", res = \"src/main/res\", ) Run this command from the android folder to clean Gradle: Copy ./gradlew clean Install and configure React Native Firebase Copy npm install --save react-native-firebase Copy the previously downloaded google-services.json to the /native/android/app/ folderEdit /native/android/build.gradle and add this classpath dependency for Google Services: Copy dependencies { classpath(\"com.android.tools.build:gradle:3.4.1\") classpath 'com.google.gms:google-services:4.2.0' } Edit /native/android/app/build.gradle and add this line to the bottom of the file: Copy apply plugin: \"com.google.gms.google-services\" Also add these lines for the Firebase implementation to the dependencies in the same file: Copy dependencies { //.... implementation \"com.google.android.gms:play-services-base:16.1.0\" implementation \"com.google.firebase:firebase-core:17.0.1\" implementation \"com.google.firebase:firebase-messaging:19.0.1\" implementation 'me.leolin:ShortcutBadger:1.1.21@aar' //.... } Edit /native/[Your Environment Name]/android/app/src/main/java/com/[Your Environment Name]/MainApplication.java and add these imports for RNFirebaseMessagingPackage and RNFirebaseNotificationsPackage Copy import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage; In the same file, add those packages to the list of packages: Copy @Override protected List<ReactPackage> getPackages() { @SuppressWarnings(\"UnnecessaryLocalVariable\") List<ReactPackage> packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); packages.add(new RNFirebaseMessagingPackage()); packages.add(new RNFirebaseNotificationsPackage()); return packages; } At this point, the build for our Android app might fail. We solve this by Enabling Multidex. Open the /native//android/app/build.gradle file. Under dependencies we need to add the module, and then enable it within our defaultConfig: Copy android { defaultConfig { // ... multiDexEnabled true } // ... } dependencies { implementation 'com.android.support:multidex:1.0.3' } Open /native/[Your Environment Name]/ios/[Your Environment Name].xcworkspaceusing Xcode and add the previously downloaded GoogleService-Info.plist to the XCode project name by right clicking and selecting \"Add Files to [Your Environment Name]\"In Xcode, enable the remote notifications by clicking on the project name in the left pane then clicking the Capabilities tab. Add Push Notifications.In Xcode, edit the Pods/podfile and add these lines: Copy pod 'Firebase/Core' pod 'Firebase/Messaging' Also add the Pod path for RNFirebase to the app under \"# Pods for [Your Environment Name]\": Copy pod 'RNFirebase', :path => '../node_modules/react-native-firebase/ios' Run this command from the terminal inside the ios folder: Copy pod update Edit /native/[Your Environment Name]/ios/[Your Environment Name]/AppDelegate.m and add the imports for Firebase, React Native Firebase Notifications, and Messaging: Copy #import <Firebase.h> #import \"RNFirebaseNotifications.h\" #import \"RNFirebaseMessaging.h\" At the beginning of the didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method add these lines to initialize Firebase and RNFirebaseNotifications: Copy [FIRApp configure]; [RNFirebaseNotifications configure]; Add a new method to receive local RNFirebaseNotifications: Copy - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [[RNFirebaseNotifications instance] didReceiveLocalNotification:notification]; } Add a new method to receive remote RNFirebaseNotifications: Copy - (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{ [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } Add a new method to register with Firebase and receive the FCM token: Copy - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { [[RNFirebaseMessaging instance] didRegisterUserNotificationSettings:notificationSettings]; } Handling Push Notifications in App# You can add the following code to the file divblox_react_native.js in the root of your native project: Copy import firebase from 'react-native-firebase'; import {Alert} from \"react-native\"; Edit the function registerPushNotifications() to handle the registration correctly: Copy asyncregisterPushNotifications(success_callback,failed_callback){ // You can safely remove this line //console.log(\"TODO: Put your code that asks for push notification permissions here. Once a successful Push\" + // \" registration ID is received, send it to the server with\"); // Add these 2 lines: this.checkPermission(); this.messageListener(); } Then simply add the following functions to the same file inside the Divblox class: Copy checkPermission=async()=>{ const enabled =await firebase.messaging().hasPermission(); if(enabled){ this.getFcmToken(); }else{ this.requestPermission(); } }; getFcmToken=async()=>{ const fcmToken =await firebase.messaging().getToken(); if(fcmToken){ this.createPushRegistration( fcmToken, function(data){ //console.log(\"Registered with app: \"+data); }, function(data){ //console.log(\"NOT Registered with app: \"+data); } ); }else{ //this.showAlert('Failed', 'No token received'); } }; requestPermission=async()=>{ try{ await firebase.messaging().requestPermission(); this.getFcmToken(); // User has authorised }catch(error){ // User has rejected permissions } }; messageListener=async()=>{ this.notificationListener= firebase .notifications() .onNotification((notification)=>{ const{ title, body }= notification; this.showAlert(title, body); }); this.notificationOpenedListener= firebase .notifications() .onNotificationOpened((notificationOpen)=>{ const{ title, body }= notificationOpen.notification; //this.showAlert(title, body); }); const notificationOpen =await firebase .notifications() .getInitialNotification(); if(notificationOpen){ const{ title, body }= notificationOpen.notification; //this.showAlert(title, body); } this.messageListener= firebase.messaging().onMessage((message)=>{ //console.log(JSON.stringify(message)); }); }; showAlert=(title, message)=>{ Alert.alert( title, message, [{ text:\"OK\",onPress:()=>console.log(\"OK Pressed\")}], { cancelable:false} ); }; "},{"title":"Basic Training Exercise","type":0,"sectionRef":"#","url":"docs/basic-training-exercise","content":"","keywords":""},{"title":"Introduction","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#introduction","content":"In this training exercise, we will be creating a basic ticketing system that will allow users to create and manage \"tickets\". To allow users to interact with our tickets, we will generate CRUD (Create, Read, Update, Delete) components. Additionally, we will create the following components: A page where full CRUD of tickets and categories is doneA page where we reuse the CREATE component for a ticket to allow the user to create tickets in a simple way We will also be building some custom functionality to demonstrate how to communicate between the front-end and back-end of a divbloxPHP application. Finally, we will also learn how to secure our components and data model entities, as well as how to expose our functionality via the divbloxPHP API layer. "},{"title":"Step 1 - Data Model","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-1---data-model","content":"We will be creating a data model with the following entities and attributes: Ticket: TicketName, TicketDescription, TicketDueDate, TicketUniqueId, TicketStatusCategory: CategoryLabel This can be represented as follows: If you need a refresh on divbloxPHP data modelling, click here. Below is a walk-through of how to add the necessary entities using Divblox's Data Modeller. "},{"title":"Step 2 - CRUD Components","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-2---crud-components","content":"Now that our data model is created and synchronized with our database, let's generate some CRUD components (using the component builder) for Ticket and Category. Below is a walk-through of how to create full CRUD functionality for the Category entity. And now we will create the CRUD functionality for the Ticket entity, which although more complex, is just as easy with divbloxPHP. It may be interesting to note how divbloxPHP creates these components in the IDE. below is a screenshot of the component file structure after we created the CRUD components. You can see the 4 CRUD components for both the Ticket and the Category entities, with each component have independent PHP, JS, HTML, CSS and json files. When checking the Validate checkbox, divbloxPHP automatically notifies the user that input is required. Further validations can be added at a later stage. Notice that in both examples we did not tick the Constrain To checkbox. If you constrain by a certain attribute, you are filtering to see only results that satisfy that criteria. An example would be to constrain Tickets by the current user account. This will display only tickets created by the current user. These constraints can only be done with entities that have a singular relationship. Singular relationships mean that an entity instance is linked to one, and only one, instance of another entity. E.g. Each ticket can only be linked to one account at any given time. This logic also applies when using 'create' and 'update' functionality and using the Constrain By checkbox. An example here would be to automatically link a ticket to the current user upon creation. You may want to change the display of certain attributes, in all of the components they feature. In our example, let's say we have a set predetermined list of ticket statuses the user should be able to chose from. This can be manually done in the data_lists.json and entity_definitions.json files. A walk-through of this is shown in the below video. "},{"title":"Step 3 - Page Components","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-3---page-components","content":"In order for us to be able to use our newly generated CRUD components, or any other component for that matter, we need to put them inside pages. Pages are also just components, but they can be navigated to by the user in the browser, while individual components can not. info A component is considered a page component when it is located in the \"pages\" grouping (the folder /project/components/pages/[component_name]) The pages we will build for this exercise are: An admin page where our full CRUD components can liveA \"New Ticket\" page where users can create new tickets To do this we will use a pre-made page template with a side navbar. As you will see, the navigation bar is pre-populated with links we will later override or delete to suite our needs. Now we can create the 'Tickets' page where users can create tickets. Note that we are not creating any new functionality, just reusing the 'create' component previously generated and placing it on its own page. "},{"title":"Step 4 - Navigation bar","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-4---navigation-bar","content":"Ok, we now have components that allow us to create our data, as well as pages to view them on. We will now update the side navigation bar to function as we want it to. Notice how, in this video, we edit the component code in our IDE (any IDE/text editor of your choice). The preferred way is to use an IDE, but for quick fixes like changing the HTML layout of our page we can use divbloxPHP's built-in code editor. The process followed here is as follows: divbloxPHP incorporates an easy to use seperation between the actual navigation bars and the menu items in them. The idea of this is to allow for reusabililty of the same menus and styiling throughout a project. You can access the list of menus available in your project from the Navigation bar in the setup page. You will see the following: There are a few default menus, and we will create a basic-training-exercise-menu with the necessary items. Here we add the HTML that we want to be displayed as our item, in our case we want an icon and page name. We give our two pages the following: Copy <!-- ADMIN --> <iclass=\"fa fa-home\"aria-hidden=\"true\"></i> <br/> Admin <--!NewTicket--> <iclass=\"fa fa-link\"aria-hidden=\"true\"></i> <br/> New Ticket We also set the action we want to occur when clicking on the menu item to load up the corresponding page. The navigation bar component HTML looks like this: "},{"title":"Step 5 - Global Functions","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-5---global-functions","content":"For the purposes of this exercise, we want to assign a unique ID to every ticket. This will allow us later on to retrieve information about our ticket via an API. To generate this unique ID, we will make use of a global function call. info Global functions are defined for functionality that will be used multiple times, reducing code duplication We will create the unique ID in the backend, as we need to verify whether or not it is indeed unique by checking our database. "},{"title":"Step 5.1 - Adding button","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-51---adding-button","content":"Add the button in our ticket_crud_create component that will generate a unique ID and populate the input box. We can do this through the divbloxPHP Component Builder or in the source code. Below is a video running through step 1: "},{"title":"Step 5.2 - Create global function","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-52---create-global-function","content":"Create the global php function that will generate the unique ID in project_functions.php. Here is the code added into the class ProjectFunctions, in project/assets/php/project_functions.php. Copy publicstaticfunctiongetNewTicketUniqueId(){ $CandidateStr= self::generateRandomString(24); $DoneBool=false; while(!$DoneBool){ // divbloxPHP query language to load a ticket from the database, // based on the UniqueId field $ExistingTicketCount= Ticket::LoadByTicketUniqueId($CandidateStr); if($ExistingTicketCount==0){ $DoneBool=true; }else{ $CandidateStr= self::generateRandomString(24); } } return$CandidateStr; } "},{"title":"Step 5.3 - Call global function","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-53---call-global-function","content":"Call the global function from component.php, sending information to the front end. The code added into the ticket_crud_create component.php file is: Copy // The function on our component controller that will return a new unique ticket ID for us. // This function is executed when we pass \"getNewTicketUniqueId\" as // the value for \"f\" from our component JavaScript publicfunctiongetNewTicketUniqueId(){ // setReturnValue() sets the values in an array that will be returned as JSON //when the script completes. We always need to set the value for \"Result\" to either // \"Success\" or \"Failed\" in order for the component JavaScript to know // how to treat the response $this->setResult(true); // It is always a good idea to populate a \"Message\" for the front-end $this->setReturnValue(\"Message\",\"New unique ID created\"); // Here we set the value of any additional parameters to return $this->setReturnValue(\"TicketId\", ProjectFunctions::getNewTicketUniqueId()); // \"presentOutput()\" returns our array as JSON and stops any // further execution of the current php script $this->presentOutput(); } Below is a video running through step 2 and 3: "},{"title":"Step 5.4 - Add JavaScript","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-54---add-javascript","content":"Add the JavaScript functionality that auto-populates the input box with the newly generated unique ID in component.js. Below is a video of step 4: The code added into the initCustomFunctions function was: Copy // dxRequestInternal() is the global function used to communicate // from the component's JavaScript to its back-end php component dxRequestInternal( // The first parameter tells the function where to send the request // getComponentControllerPath(this) returns the path to current component's php script getComponentControllerPath(this), // Tell component.php which function to execute { f:\"getNewTicketUniqueId\"}, function(data_obj){ // Success function getComponentElementById(this,\"TicketUniqueId\").val(data_obj.TicketId); }.bind(this), function(data_obj){ // Fail function }.bind(this) ); "},{"title":"Step 6 - Security","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-6---security","content":"It is important to understand how divbloxPHP user roles are used to control access to the application. divbloxPHP has two forms of access: Component access allows the user to view the componentsData Model access gives the user permissions to perform CRUD operations on specific entities defined in the data model. By default, there are two user roles. Administrator - Has access to all components and full CRUD functionality.User - This is the user role allocated to anyone who registers on your app. The default access is only to your profile and account. Any user that is not authenticated is treated as \"Anonymous\" - No access, gets redirected to the anonymous landing page. info Additional user roles can be defined in the data modeller. It is important to define a user role hierarchy in the abstract class 'UserRoleHierarchy' (project/assets/php/user_role_hierarchy.class.php), so as to prevent duplicate writing of components and data model access for different user types. This allows for user roles to inherit access from lower user roles, so you only need to specify what access the user role has above and beyond previously defined access. The Component default settings are as follows: And the Data Model settings seen below. It is also important to note that by default users are able to create and read data, even if not explicitly stated in the $AccessArray. For our exercise we created 2 pages (The 'admin' and 'new ticket' pages). Let's assume that we only want administrators to access the admin page. You can access the register page by navigating to [your_project_root]/?view=register. New users are registered with the user role \"User\" by default. note It is also good practice to test user role access in incognito/private mode, as you are typically logged in as a divbloxPHP admin (dxAdmin) most of the time while in the component builder. dxAdmin is a superuser As you can see, our new user is unable to view any of the pages we built. This is because he does not have component access to the components on those pages. We will change that in the ComponentRoleBasedAccessArray::$AccessArray. In the below video we will firstly give our user full access to any Ticket and Category components. This will allow us to see how the Data Model access works (we will observe this on our admin page). Once the Data Model access is configured, we will then give our user access only to the create components of both Ticket and Category, allowing the user to view the New Ticket page, but not the admin page. It is worth noting that this is a basic example to demonstrate how divbloxPHP handles user access. As you may have seen above, there is no need to change the Data Model access of our user to be able to update and delete as he will never be able to get to the admin page to do this. "},{"title":"Step 7 - Exposing an API","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-7---exposing-an-api","content":"Now that we have all the groundwork completed, let's provide the world with an API endpoint that will allow us to do some custom functionality on our tickets. To do this, we will copy the provided api_example endpoint and modify it for our use case. The API functionality we want to achieve is as follows: Allow a user to provide us with an array of unique ticket IDs as inputSelect only the ticket descriptions from the ticketsMerge all of the ticket descriptions into the first ticket (initial unique ID)Delete the remaining ticketsReturn the new merged ticket as output We will be using a program called 'PostMan' to test our API functionality. It comes pre-installed on the divbloxPHP VM image. You can view their website here. info divbloxPHP automatically handles the routing for your API endpoint. API endpoints are available at [your_project_root]/api/[endpoint_name] Below we will briefly explore the api_example functionality, how to navigate the URL and what the expected output looks like. To proceed, we will create a file basic_training_exercise.php in project/api and mimic the basic structure of an API endpoint like in api_example.php. Firstly, we add an API operation with function namemergeTickets();. In the mergeTickets(); function we code the following logic: check if the input is valid JSONcheck if the master unique ID existscheck if the master unique ID is validcheck if there are more than two IDsif there is more than two IDs: loop through the valid IDsperform a merging of the ticket descriptionsdelete each ticket after its description is merged save the results into the databasepresent output to front end The code added into our 'basic_training_exercise.php' endpoint (/project/api/basic_training_exercise.php) is the following: Copy require(\"../../divblox/divblox.php\"); // Start by declaring your operations and then calling the initApi function. // This is important for your API documentation to be automatically generated at run-time PublicApi::addApiOperation(\"mergeTickets\", // Specify the various input parameters as an array [\"input_ids\"], // Specify the various expected output parameters as an associative array [\"merged_ticket\"=>\"[JSON object representing new merged ticket]\"], // Give your operation a name \"Merge Tickets\", // Give your operation a description \"This operation will merge an array of tickets into a combined ticket with the unique ID of the first ticket. input_ids should be a JSON encoded array of unique ticket IDs\"); // Describes the \"entire\" API endpoint PublicApi::initApi(\"API endpoint to demonstrate our basic training exercise functionality\", \"Basic Training Exercise\"); // Operation functionmergeTickets(){ // More information on functions available in the public API class // is provided in the API documentation section $InputIdArrayStr= PublicApi::getInputParameter(\"input_ids\"); if(!ProjectFunctions::isJson($InputIdArrayStr)){ PublicApi::setApiResult(false); PublicApi::addApiOutput(\"Message\",\"Invalid value for input_ids provided.\"); PublicApi::printApiResult(); } $InputIdArray=json_decode($InputIdArrayStr); if(!isset($InputIdArray[0])){ PublicApi::setApiResult(false); PublicApi::addApiOutput(\"Message\",\"Invalid value for input_ids provided.\"); PublicApi::printApiResult(); } // Notice the function LoadByTicketUniqueId we are calling. This is generated by divbloxPHP and will be discussed shortly $MasterTicketObj= Ticket::LoadByTicketUniqueId($InputIdArray[0]); if(is_null($MasterTicketObj)){ PublicApi::setApiResult(false); PublicApi::addApiOutput(\"Message\",\"Invalid input ID for master ticket\"); PublicApi::printApiResult(); } $InputIdArraySizeInt= ProjectFunctions::getDataSetSize($InputIdArray); if($InputIdArraySizeInt<2){ PublicApi::setApiResult(true); PublicApi::addApiOutput(\"merged_ticket\",json_decode($MasterTicketObj->getJson())); PublicApi::printApiResult(); } for($i=1;$i<$InputIdArraySizeInt;$i++){ $TicketObj= Ticket::LoadByTicketUniqueId($InputIdArray[$i]); if(is_null($TicketObj)){ continue; } $MasterTicketObj->TicketDescription.=$TicketObj->TicketDescription; $TicketObj->Delete(); } $MasterTicketObj->Save(); PublicApi::setApiResult(true); PublicApi::addApiOutput(\"merged_ticket\",json_decode($MasterTicketObj->getJson())); PublicApi::printApiResult(); } Referring to the Ticket::LoadByTicketUniqueId() function that was called above: every Entity has an EntityNameGen.class.php file where divbloxPHP generates ORM related functionality (located at /project/assets/php/data_model_orm/generated/EntityNameGen.class.php). It is a good idea to look through these classes once your data model is synchronised with a database, and familiarise yourself with what is available. We will briefly highlight some of this functionality for the TicketGen class specifically. Firstly we have class wide methods which are simply wrappers for the most common dxQuery statements a developer might use. Then there are index-based load methods. It is important to note how types of attributes and data model structure are translated into generated functions. The LoadById() function is available for all entities, as the ID is a unique field. Since we made the TicketUniqueId attribute unique as well, divbloxPHP allows you to load by that attribute as well. We are also able to LoadArrayByEntityName and CountByEntityName for any entities that are constraining the current entity. In our case, we defined a one to many relationship from Account and Category to Ticket, hence we can load/count an array of tickets constrained by each of these entities. We are also able to perform certain further actions onto any associated entities as well, shown below for the SubTask and Note entities. There are more functions in this class, as well as some background divbloxPHP-related query functions which you may look at in your own time. Back to the topic at hand, once we have defined our endpoint, we can test to see if everything works. Note that this specific API operation updates and deletes data in our database, so we need to update the Data Model permissions so that 'any' users can 'update' and 'delete' (Recall that default permissions are only to 'create' and 'read'). The file where we define the data model (database) user permission is located at /project/assets/php/data_model_role_based_access.php. We add full permission for the Ticket and Category entities. Once this is done, our API operation should be set up and permissions for operations granted. We use Postman this time, as it makes it easier to input parameters and has a great user interface. "},{"title":"Step 8 - Further Examples","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#step-8---further-examples","content":"In the following examples, you will gain a deeper understanding of the divbloxPHP query language that is used to communicate with the database. We will also be building on your existing knowledge on how to communicate between the frontend and backend of your Divblox application. info In this section we will focus on the dxQuery language explained here. You will build the following: A custom component that will serve as a container for some example functions. This component will manage the communication between frontend and backendFunction 1: An example of how to generate dummy dataFunction 2: Basic dxQuery example using QueryArray()Function 3: A slightly more advanced dxQuery example using query conditions like dxQ::AndCondition() and dxQ::Equal()Function 4: Even more advanced example that makes use of divbloxPHP's wrappers for PHP's DateTime classFunction 5: Example of building up a query result in a loopFunction 6: More advanced example of building a query result in a loopFunction 7: Optimization of function 6Lastly, we will optimize our code for extremely large datasets "},{"title":"Custom Component Setup","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#custom-component-setup","content":"Create a custom component with two equally sized columns. In the left column we will house three elements, namely a drop down list of functions to select, an additional input box and a button to execute the chosen functionality. In the right column we will just create and empty div with ID = \"ResultWrapper\" so we can instruct dxRequestInternal() where to display any output. Below is a video walk through of the process: It is important to create a div in the right column so you can tell dxInternalRequest() where to return any output. Next, we will set up our component JavaScript to send the selected function (and include the additional output if required) to the backend, and return whatever output the function provided to the front end. We will again be using the dxRequestInternal function for backend/frontend communication. The code that replaces the default 3 second loading function for our button is: Copy dxRequestInternal( getComponentControllerPath(this), { // We specifically named the values of the drop down the same as // the functions to execute, so we can just use the value of input box f:getComponentElementById(this,\"8LSBQ_FormControlSelect\").val(), additional_input:getComponentElementById( this, \"H7u7b_FormControlInput\" ).val(), }, function(data_obj){ // Success Function // Returns (in JSON format) backend function output // in div with id=\"ResultWrapper\" getComponentElementById(this,\"ResultWrapper\").html( JSON.stringify(data_obj.ReturnData) ); }.bind(this), function(data_obj){ // Failure Function // Nothing set here right now }.bind(this), false, // Set loading text of button while function executes getComponentElementById(this,\"dfVzo_btn\"), \"Executing \"+ getComponentElementById(this,\"8LSBQ_FormControlSelect\").val() ); We should now be set! Our custom component is ready, our input selection is set up and we have a div to display our output. What remains now is to define our 7 functions. "},{"title":"Function 1","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-1","content":"Generate data: This function will generate a bunch of categories & accounts, then it will generate a bunch of tickets, linked to a random category with a random status, account & due date Here we use the built in PHP function rand(), as well as a bit of divbloxPHP functionality, including the dxDateTime() class as well as the generateRandomString() and generateTimeBasedString() functions. If you are not comfortable with these, you can refer to the class/function definitions. Copy publicfunctionFunction1(){ // Set how many of each we want to generate $AccountDataSize=50; $CategoryDataSize=8; $TicketDataSize=500; $TicketStatusArray=[\"New\",\"In Progress\",\"Backlog\",\"Urgent\",\"Completed\"]; // Note that you need an initial account and category for this to work for($i=0;$i<$TicketDataSize;$i++){ // Fill the Ticket object with necessary values and save into database $TicketObj=newTicket(); $TicketObj->TicketName= ProjectFunctions::generateRandomString(8); $TicketObj->TicketDescription= ProjectFunctions::generateRandomString(100); $TicketObj->TicketStatus=$TicketStatusArray[rand(0,4)]; // dxDateTime has prebuilt functionality for working with dates, // all we are doing is making the DueDate a random date between // tomorrow and 20 days from now. $TicketObj->TicketDueDate= dxDateTime::Now()->AddDays(rand(1,20)); // Load a random value from existing Account and Category Entities $TicketObj->AccountObject= Account::Load(rand(0,Account::CountAll()-1)); $TicketObj->CategoryObject= Category::Load(rand(0,Category::CountAll()-1)); $TicketObj->Save(); if($i>=$AccountDataSize){ continue; } // Fill the Account object with necessary values and save into database $AccountObj=newAccount(); $AccountObj->FirstName= ProjectFunctions::generateRandomString(8); $AccountObj->LastName= ProjectFunctions::generateRandomString(8); $AccountObj->FullName=$AccountObj->FirstName.\" \".$AccountObj->LastName; $AccountObj->EmailAddress= ProjectFunctions::generateTimeBasedRandomString(); $AccountObj->Username=$AccountObj->EmailAddress; $AccountObj->Save(); if($i>=$CategoryDataSize){ continue; } //Fill the Category object with necessary values and save into database $CategoryObj=newCategory(); $CategoryObj->CategoryLabel= ProjectFunctions::generateTimeBasedRandomString(); $CategoryObj->Save(); } // Prepare the result we will send to the front end $this->setResult(true); $this->setReturnValue(\"ReturnData\",\"$TicketDataSize Tickets created\"); $this->presentOutput(); } Every time we run this function, we are generating 500 new tickets, 8 new categories and 50 new accounts. Note that the way we executed our loop, the tickets will not be generated with uniform distribution of categories or accounts, as the oldest generated accounts and categories will be sampled more often. But since we are just doing this to get some data, we are not worried about that. "},{"title":"Function 2","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-2","content":"Return all tickets in the category defined by the user in the additional input box. The default category should be 'Personal' if no input provided. Copy // Return all tickets in the category specified in the input box publicfunctionFunction2(){ // User input for which category to sort by. $CategoryInputStr=$this->getInputValue(\"additional_input\"); // Set default to \"Personal\" if(is_null($CategoryInputStr)||!strlen($CategoryInputStr)){ $CategoryInputStr=\"Personal\"; } // dxQuery to return all tickets whose category matches the input category. // Note: $TicketArray is an array of individual ticket objects. $TicketArray= Ticket::QueryArray( dxQ::Equal( dxQN::Ticket()->CategoryObject->CategoryLabel, $CategoryInputStr ) ); // Create an array of the ticket objects with wanted category. // Note: we convert each ticket object into a JSON object // getJSON() returns the values in question in JSON (removing all methods). // We then decode the JSON object and append it to our $ResultArray. This is // because dxRequestInternal() already handles the JSON type conversions. $ReturnArray=[]; foreach($TicketArrayas$TicketObj){ $ReturnArray[]=json_decode($TicketObj->getJson()); } // Set what we are returning to the front end $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Function 3","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-3","content":"Return all tickets where the Account's first name is specified in the additional input box (Default value is 'John') and the ticket status is \"In Progress\" Copy // Return all tickets where the account's first name is specified in the input box and // the ticket's status is <em>In Progress</em> publicfunctionFunction3(){ // User input for which Name to sort by. $FirstNameStr=$this->getInputValue(\"additional_input\"); // Set default name to 'John' if(is_null($FirstNameStr)||!strlen($FirstNameStr)){ $FirstNameStr=\"John\"; } // dxQuery to return all tickets whose name matches the input name AND // whose ticket status is 'In Progress' $TicketArray= Ticket::QueryArray( dxQ::AndCondition( dxq::Equal( dxqN::Ticket()->AccountObject->FirstName, $FirstNameStr ), dxq::Equal( dxqN::Ticket()->TicketStatus, \"In Progress\" ) ) ); $ReturnArray=[]; foreach($TicketArrayas$TicketObj){ $ReturnArray[]=json_decode($TicketObj->getJson()); } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Function 4","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-4","content":"Return all tickets that have a status of \"Completed\" for the current month This function is slightly trickier as we are now trying to sort by date. There are many ways to try do this, but we will use built-in divbloxPHP functions to make this easier. The 'trick' here would be to be familiar and comfortable with what dxDateTime can offer you. All we do below is: Set the StartDate to today's year and month but manually change the day to 1 and time to 00:00:00.Set the EndDate to StartDate, + 1 month, - 1 second (which is then just the last day of the current month) Copy // Return all tickets that have a status Completed where the due // date is in the current month, ordered by TicketDueDate ascending publicfunctionFunction4(){ // Define start and end of current month $StartDateObj= dxDateTime::Now()->setDate(dxDateTime::Now()->format(\"Y\"), dxDateTime::Now()->format(\"m\"),1)->setTime(0,0,0); $EndDateObj= dxDateTime::Now()->setDate(dxDateTime::Now()->format(\"Y\"), dxDateTime::Now()->format(\"m\"),1)->setTime(0,0,0); $EndDateObj->addMonths(1); $EndDateObj->addSeconds(-1); // Slightly more complex dxQuery with 3 parts to the AND clause, as well as // and OrderBy Clause to sort by TicketDueDate ascending $TicketArray= Ticket::QueryArray( dxQ::AndCondition( dxq::Equal( dxqN::Ticket()->TicketStatus, \"Completed\" ), dxQ::GreaterOrEqual( dxqN::Ticket()->TicketDueDate, $StartDateObj ), dxQ::LessOrEqual( dxqN::Ticket()->TicketDueDate, $EndDateObj ) ), dxQ::Clause( dxQ::OrderBy( dxqN::Ticket()->TicketDueDate, true ) ) ); $ReturnArray=[]; foreach($TicketArrayas$TicketObj){ $ReturnArray[]=json_decode($TicketObj->getJson()); } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Function 5","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-5","content":"Return a list of \"Account\" full names with a count of tickets that they each currently have \"In Progress\" In this function we first request an array of all Account objects. We then loop through each of those objects, and fill a key-value pair array as [FullName => NrTicketsInProgress] which is our expected result. Copy // Return a list of account full names with a count of tickets that // they each currently have <em>In Progress</em>. publicfunctionFunction5(){ // Returns an array of all Account objects $AccountArray= Account::QueryArray( dxQ::All() ); //Same results as: $AccountArray = Account::LoadAll(); $ReturnArray=[]; foreach($AccountArrayas$AccountObj){ $ReturnArray[$AccountObj->FullName]= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->AccountObject->Id, $AccountObj->Id ) ); } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Function 6","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-6","content":"Return a list of \"Account\" email addresses. For each account, show an array of categories. For each \"Category\" in the array, show the total count of all tickets for that category as well as the count for the specific account You will end up with a nested array of objects, structured something like this: For each account (defined by it's email address) we want to see an array of all the categories used in their tickets. For each of those account-used categories, we want to see both the category's total tickets, as well as the account's category total ticket count. Copy publicfunctionFunction6(){ // Get Array of Account objects and array of Category objects $AccountArray= Account::QueryArray( dxQ::All() ); $CategoryArray= Category::QueryArray( dxQ::All() ); $ReturnArray=[]; //Note Array indexing, refer to above diagram for visual aid of data structure foreach($AccountArrayas$AccountObj){ $ReturnArray[$AccountObj->EmailAddress]=[]; foreach($CategoryArrayas$CategoryObj){ // For each account and each category, we find the two totals $TotalCountInt= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->CategoryLabel, $CategoryObj->CategoryLabel ) ); $AccountCountInt= Ticket::QueryCount( dxQ::AndCondition( dxQ::Equal( dxQN::Ticket()->CategoryObject->CategoryLabel, $CategoryObj->CategoryLabel ), dxQ::Equal( dxQN::Ticket()->AccountObject->Id, $AccountObj->Id ) ) ); // For each account->category pair, we add the two total values $ReturnArray[$AccountObj->EmailAddress][$CategoryObj->CategoryLabel]= [\"GrandTotal\"=>$TotalCountInt,\"AccountTotal\"=>$AccountCountInt]; } } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Function 7","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#function-7","content":"Optimize the query in Function 6 using the Select Clause and by reducing the number of iterations in the loop. You can use your browser's dev tools to monitor the time taken to execute the query. Select only the necessary attribute from Account entity, i.e. EmailAddress. This means that we are only returning a single column from the database. NOTE: Even though you only select one column, the Account primary key is still included for indexed searching.We only calculate the GrandTotal once for every Category, instead of re-checking and re-saving it every time we loop over an account, which has a ticket with said category. The new code can be seen below: Copy // Optimizes function 6 to return its query result quicker publicfunctionFunction7(){ // Optimization 1 : Select only the necessary attribute from // Account entity, i.e. EmailAddress. $AccountArray= Account::QueryArray( dxQ::All(), dxQ::Clause( dxQ::Select( dxQN::Account()->EmailAddress ) ) ); $CategoryArray= Category::QueryArray( dxQ::All() ); $ReturnArray=[]; $TotalCountArray=[]; foreach($AccountArrayas$AccountObj){ $ReturnArray[$AccountObj->EmailAddress]=[]; foreach($CategoryArrayas$CategoryObj){ // Optimization 2: Only check TotalCount once for each category if(!isset($TotalCountArray[$CategoryObj->Id])){ $TotalCountArray[$CategoryObj->Id]= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); } $AccountCountInt= Ticket::QueryCount( dxQ::AndCondition( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ), dxQ::Equal( dxQN::Ticket()->AccountObject->Id, $AccountObj->Id ) ), dxQ::Clause( dxQ::Select( dxqN::Ticket()->Id ) ) ); $ReturnArray[$AccountObj->EmailAddress][$CategoryObj->CategoryLabel]= [\"GrandTotal\"=>$TotalCountArray[$CategoryObj->Id],\"AccountTotal\"=>$AccountCountInt]; } } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } Using the browser's network monitoring tool, the difference with just these minor changes can already be seen. Below is a screenshot of the times taken with the two methods. 5 observations were taken for a better average. There are much better and more in-depth ways to test your code efficiency, but for this example a rough visual difference is all we need. The time taken is dependent on the hardware of your machine, as well as how big your database is. So if your times are different, don't worry, as long as you can see a visible decrease in time taken. "},{"title":"Final Optimizations","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#final-optimizations","content":"In this last step, we will have to change our data model slightly. Let us include the TicketCount attribute into the Category entity, which will represent the total number of tickets in the category in question, and will be updated as tickets are saved and deleted. Using Divblox's data modeller, this process is simple and all the user needs to do is add the attribute and sync to the database, which updates the database and regenerates all the classes and necessary functionality code. This optimization is intended for when our dataset becomes extremely large, and the cost of making even an optimized query in a loop becomes too high. In this optimization we are focusing on preparing our data for reporting purposes rather than aggregating after the fact. We will need to write the functionality that calculates the TicketCount into the Save() and Delete() functions of the Ticket ORM class, meaning that any time a ticket is saved or deleted, the TicketCount will be updated. We can do this by overriding their default behaviour defined in TicketGen.class.php (/project/assets/php/data_model_orm/generated/TicketGen.class.php) in Ticket.class.php (/project/assets/php/data_model_orm/generated/Ticket.class.php). We do this in accordance with the Divblox code-generation philosophy: never touch auto-generated files, but rather add functionality in classes that extend base Divblox functionality to prevent loss of work. The code added to the class Ticket which extends the class TicketGen was: Copy publicfunctionSave($blnForceInsert=false,$blnForceUpdate=false){ // This step ensures that all of the original Save functionality is still carried out. $mixToReturn=parent::Save($blnForceInsert,$blnForceUpdate); $CategoryObj= Category::Load($this->intCategory); //If the category exists, calculate the total if(!is_null($CategoryObj)){ $TicketCount= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); // Write it into local storage, and save into database $CategoryObj->TicketCount=$TicketCount; $CategoryObj->Save(); } // Return return$mixToReturn; } Copy publicfunctionDelete(){ // For delete, we first load the category since after it is deleted // we won't be able to know which category the ticket was in. $CategoryObj= Category::Load($this->intCategory); parent::Delete(); // If category exists, calculate the total if(!is_null($CategoryObj)){ $TicketCount= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); // Write it into local storage, and save into database $CategoryObj->TicketCount=$TicketCount; $CategoryObj->Save(); } } This is great. Now every time we create or delete a ticket, our counter will automatically update. But what do we do about the tickets that already exist in our database? We will have to create a throwaway script to run once to account for all the already existing tickets in our database. Copy require(\"divblox/divblox.php\"); // Select All Categories $CategoryArray= Category::QueryArray( dxQ::All() ); // For each unique category, calculate the total number of tickets // which have that category foreach($CategoryArrayas$CategoryObj){ $TicketCount= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); // save $TicketCount into localstorage and into database. $CategoryObj->TicketCount=$TicketCount; $CategoryObj->Save(); } This script can be saved anywhere and run by going to localhost/[your_project]/[path_to_script]/throwaway.php in your browser. You can confirm that it performed what it was supposed to by checking the category table in phpMyAdmin. Now, the final step is to go back to our code and just reference the TicketCount attribute instead of calculating the TicketCount total every time. This shouldn't really affect our speed significantly until we have a very large data set, but was included for good practice. Copy publicfunctionFunction7(){ $AccountArray= Account::QueryArray( dxQ::All(), dxQ::Clause( dxQ::Select( dxQN::Account()->EmailAddress ) ) ); $CategoryArray= Category::QueryArray( dxQ::All() ); $ReturnArray=[]; foreach($AccountArrayas$AccountObj){ $ReturnArray[$AccountObj->EmailAddress]=[]; foreach($CategoryArrayas$CategoryObj){ $AccountCountInt= Ticket::QueryCount( dxQ::AndCondition( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ), dxQ::Equal( dxQN::Ticket()->AccountObject->Id, $AccountObj->Id ) ), dxQ::Clause( dxQ::Select( dxqN::Ticket()->Id ) ) ); $ReturnArray[$AccountObj->EmailAddress][$CategoryObj->CategoryLabel]= [\"GrandTotal\"=>$CategoryObj->TicketCount,\"AccountTotal\"=>$AccountCountInt]; } } $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArray); $this->presentOutput(); } "},{"title":"Summary","type":1,"pageTitle":"Basic Training Exercise","url":"docs/basic-training-exercise#summary","content":"In this exercise you learned about all the basic elements of a Divblox project. If you understand step 1 - 8 completely, you should have a fundamental understanding of the basics of any Divblox application. If you would like to receive further hands-on training from the Divblox team, please reach out to us at support@divblox.com and we will arrange a consultation. "},{"title":"Advanced Training Exercise","type":0,"sectionRef":"#","url":"docs/advanced-training-exercise","content":"","keywords":""},{"title":"Introduction","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#introduction","content":"In this exercise we will be continuing with the example established in the basic training exercise. Specifically, we will be extending the basic exercise functionality with the following: Categories will now have the ability to have sub-categories using a self referenceWe will add the ability to add notes and file attachments to tickets to understand how the default file uploader works and how it can be customizedWe will allow for tickets to have sub tasks. These sub tasks will enable the ability to track a ticket's progress.We will build a dashboard that will give us a nice overview of our tickets and their progress. The new data model we will be making is as follows: As you can see, the data model is starting to look more complicated. Let's break this down: Adding the Note entity: A note needs to be attached to a Ticket, and may have an attachment (linked to the FileDocument entity)The attributes in the Note entity are NoteDescription and NoteCreatedDate Adding the SubTask entity: Each ticket can have many subtasksThe attributes in the SubTask entity are Description, SubTaskStatus and SubTaskDueDate.SubTaskStatus will have the same drop down as TicketStatus Updating the Category entity: Add the attributes CategoryParentId and HierarchyPath.This is done in order to allow for self referencing, where each category can have many subcategories.The added attributes allow for relative and absolute identification of the category relationships. "},{"title":"Category Functionality","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#category-functionality","content":"Let us begin with the changes to the Category entity. As seen in the new data model, we have added new attributes. Our old CRUD components will not reflect the changes unless we add them manually (or create new CRUD components). In our case, this will not even be necassary as the attributes added need to be defined by the programmer. We want the CategoryParentId to be assigned automatically, depending on what category we were in when we clicked '+ Category'. Similarly with the HierarchyPath, we want this to be automatically generated for each category based on the trail of parent IDs. This HierarchyPath value will be used in the frontend to indicate the actual category, including its entire hierarchy, for the ticket. In the 'category_crud_create' component we can immediately add the code to auto-populate the two new attributes accordingly. In the javascript component.js file, we override the saveEntity() function to NOT set the global constrain ID to the current one. This is because we want to keep the constrainID we initially started with in that variable. The rest of the code is default divbloxPHP functionality. Copy saveEntity(){ let current_component_obj =this.updateValues(); this.resetValidation(); if(!this.validateEntity()){ return; } // The parameter object we will send to the backend via dxRequestInternal() let parameters_obj ={ f:\"saveObjectData\", ObjectData:JSON.stringify(current_component_obj), Id:this.getLoadArgument(\"entity_id\") }; // Checks if the component is constrained by an entity and subsequently honours the constraint if(this.constrain_by_array.length>0){ this.constrain_by_array.forEach(function(relationship){ parameters_obj['Constraining'+ relationship +'Id']=getGlobalConstrainById(relationship); }) } // Communication to the backend dxRequestInternal( getComponentControllerPath(this), parameters_obj, function(data_obj){ if(this.getLoadArgument(\"entity_id\")!=null){ // THIS LINE IS REMOVED // setGlobalConstrainById(this.entity_name, data_obj.Id); pageEventTriggered(this.lowercase_entity_name+\"_updated\",{\"id\": data_obj.Id}); }else{ // THIS LINE IS REMOVED // setGlobalConstrainById(this.entity_name, data_obj.Id); pageEventTriggered(this.lowercase_entity_name+\"_created\",{\"id\": data_obj.Id}); } this.loadEntity(); this.resetValidation(); }.bind(this), function(data_obj){ showAlert(\"Error saving \"+this.lowercase_entity_name+\": \"+ data_obj.Message, \"error\", \"OK\", false); }.bind(this)); } On the PHP side, we override the empty-by-default function doAfterSaveActions() to save both the CategoryParentId as well as the HierarchyPath. The constraining (i.e. parent) category object is loaded from the database, and if this is not null, it's ID is saved into the CategoryParentId attribute of the current category object. The doAfterSaveActions() is meant for exactly this type of functionality, which needs to query dadtabase values after the input is saved. We then use the getBreadCrumbsRecursive() function (defined shortly) to return an array of the parental hierarchy of categories. Here is the function we add to the component.php file: Copy publicfunctiondoAfterSaveActions($EntityToUpdateObj=null){ if(is_null($EntityToUpdateObj)){ return; } // Query the parent category object based on constraining ID $ParentCategoryObj= Category::Load( $this->getInputValue(\"ConstrainingCategoryId\",true) ); // If the category does have a parent, save it's ID in the CategoryParentId attribute if(!is_null($ParentCategoryObj)){ $EntityToUpdateObj->CategoryParentId=$ParentCategoryObj->Id; $EntityToUpdateObj->Save(); // Then call the getBreadCrumbsRecursive() function on the current category object. // This function will return an array of the hierarchical parent categories $ReturnArr= ProjectFunctions::getBreadCrumbsRecursive($EntityToUpdateObj); // We inverse the array as it is not in the order we need, and create a HierarchyPath // string from the array $ReturnArr=array_reverse($ReturnArr); $HierarchyPathStr=\"\"; foreach($ReturnArras$CategoryLabel=>$CategoryId){ if(strlen($HierarchyPathStr)>0){ $HierarchyPathStr.=' / '; } $HierarchyPathStr.=$CategoryLabel; } $EntityToUpdateObj->HierarchyPath=$HierarchyPathStr; }else{ $EntityToUpdateObj->HierarchyPath=$EntityToUpdateObj->CategoryLabel; } $EntityToUpdateObj->Save(); } The function getBreadCrumbsRecursive() is created in the ProjectFunctions class to reduce code duplication, as we will be using it again when displaying the breadcrumb trail on our 'category_update' page. The ProjectFunctions class (project/assets/php/project_functions.php) is created for this very reason, and is where you should house all your functions that will have multiple calls in your project. The function getBreadCrumbsRecursive() is just a recursive function that returns the parental hierarchy of the input category in a key-value pair array. Copy publicstaticfunctiongetBreadCrumbsRecursive(Category $CategoryObj=null,$BreadCrumbsArray=[]){ if(is_null($CategoryObj)){ return$BreadCrumbsArray; } // Append a key-value pair to the return array $BreadCrumbsArray[$CategoryObj->CategoryLabel]=$CategoryObj->Id; // This will only return if the current category does not have a parent if(is_null($CategoryObj->CategoryParentId)||($CategoryObj->CategoryParentId<1)){ return$BreadCrumbsArray; } // Set the parent category ID, and rerun function with that ID $ParentCategoryObj= Category::Load($CategoryObj->CategoryParentId); return self::getBreadCrumbsRecursive($ParentCategoryObj,$BreadCrumbsArray); } Now that our create component correctly saves all necessary information to the database, let us set up a 'category_update' page, a screenshot of which is presented below. It will house 3 components: Breadcrumb trail for subcategories (yellow)The update component (blue)SubCategory list, based on the currently clicked category (green) This is the page a user will be redirected to only by clicking on a category to edit on the main page, and will not be accessible via navigation bar. The reason we would want a seperate page to display and update our categories is because of their hierarchical nature, and it may be a lot easier for the user to visualize parent child relationships of categories in this way. "},{"title":"Breadcrumbs","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#breadcrumbs","content":"The breadcrumb basic component can be easily added via divbloxPHP's component builder. We will do this in the 'category_update' page component. The following default HTML code: Copy <navaria-label=\"breadcrumb\"> <olclass=\"breadcrumb\"> <liclass=\"breadcrumb-item\"><ahref=\"#\">Home</a></li> <liclass=\"breadcrumb-item\"><ahref=\"#\">Library</a></li> <liclass=\"breadcrumb-item active\"aria-current=\"page\">Data</li> </ol> </nav> will be replaced with our custom HTML frame, ID'ed accordingly so we can populate it dynamically from our database based on which category is selected. Copy <navaria-label=\"breadcrumb\"> <olid=\"CategoryBreadcrumbs\"class=\"breadcrumb\"> <liclass=\"breadcrumb-item\"> <aid=\"AllCategories\"href=\"#\">All Categories</a> </li> <!-- BREADCRUMBS TO FOLLOW--> </ol> </nav> The following code will be added to the 'category_update' page component's javascript component.js file. We add two event handlers, which are defined in the initCustomFunctions() function. This function is run only once upon initialization of the component. The first event handler is to navigate back to the admin page when 'All Categories' is clickedThe second is to reset the global constraining ID for the entity 'Category' to the clicked on category and then refresh the page to load it up accordingly note Note how we attach the event to a click on the document, after which we specify where on the document the click should be. This is because if we set the event handler to listen directly for a click on '.category-breadcrumb', we will get unexpected output because during page load-up, this sub component is not defined yet. Copy initCustomFunctions(){ super.initCustomFunctions(); // Event handler navigating back to admin page getComponentElementById(this,\"AllCategories\").on(\"click\",function(){ loadPageComponent(\"admin\"); }); // Event handler refreshing the page with a new category constraint $(document).on(\"click\",\".category-breadcrumb\",function(){ // Fetches the stored category ID of form \"CategoryId_categoryname\" // and stored only the name in a variable let category_id =$(this).attr(\"id\").replace(\"CategoryId_\",\"\"); // Sets new constraint ID and reloads page setGlobalConstrainById(\"Category\", category_id); loadPageComponent(\"category_update\"); }); } The updateBreadCrumbs() function handles the request to the server using divbloxPHP's dxRequestInternal() function. The parameters we send to the backend are the function name we want to call and the category ID. The success function deals with formatting the returned array into HTML and displaying it. Copy updateBreadCrumbs(){ // Communication to the backend dxRequestInternal(getComponentControllerPath(this),{ // Parameter object received by backend f:\"getBreadCrumbs\", category_id:getGlobalConstrainById(\"Category\") }, function(data_obj){ // Success function: adding relevant breadcrumbs let html =\"\"; let category_keys =Object.keys(data_obj.ReturnData); let count =1; category_keys.forEach(function(key){ if(count ===(category_keys.length)){ html =\"<li class=\\\"breadcrumb-item active\\\">\"+ key +\"</li>\"; }else{ html =\"<li class=\\\"breadcrumb-item\\\"><a id=\\\"CategoryId_\"+ data_obj.ReturnData[key]+\"\\\" class=\\\"category-breadcrumb\\\" href=\\\"#\\\">\"+ key +\"</a></li>\"; } getComponentElementById(this,\"CategoryBreadcrumbs\").append(html); count++; }.bind(this)); }.bind(this), function(data_obj){ // Failure function dxLog(\"dxRequestInternal() failure.\"); }); } The updateBreadCrumbs() function is the called in the page component's reset() function, meaning that every time the page or component refreshes, it will execute. We make use of this functionality in conjunction with the setGlobalConstrainById() and getGlobalConstrainById() functions to very easily and dynamically update components based on constraints. Copy reset(inputs, propagate){ setActivePage(\"category_update\",\"Category Update\"); super.reset(inputs, propagate); this.updateBreadCrumbs(); } Now in the component.php file, we define the getBreadCrumbs() function referenced in the javascript. It receives the category ID, and loads the category by ID. We then call the getBreadCrumbsRecursive() function we defined in the class ProjectFunctions previously and order the array. Copy publicfunctiongetBreadCrumbs(){ $InputCategoryInt=$this->getInputValue(\"category_id\",true); $CategoryObj= Category::Load($InputCategoryInt); $ReturnArr= ProjectFunctions::getBreadCrumbsRecursive($CategoryObj); $ReturnArr=array_reverse($ReturnArr); $this->setResult(true); $this->setReturnValue(\"ReturnData\",$ReturnArr); $this->presentOutput(); } "},{"title":"Sub Category List","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#sub-category-list","content":"On the 'category_update' page we want to also be able to display and manage the sub categories for the current category. In this section we will build the subcategory_list component displayed below. We will also to create a modal pop up that will allow us to create more subcategories. (Note that this will have to reference the constraining category to make sure the hierarchy is correct). For this, we will create a new list-only CRUD component for the 'Category' entity using the component builder, displaying only the category name and subsequent ticket count. The component created is not constrained in any way. We need to constrain it to only display child categories of the category currently defined on the page. (That we defined using the setGlobalConstrainById() function). We do this by overriding the buildQueryConditions() function in the component.php file to only query the constrained array of categories from the database. This function is quite long, and you can go through it on your own time. Below is the adaptation we made to the query. Copy // Change logic in the buildQueryConditions() function protectedfunctionbuildQueryConditions(){ $EntityNodeNameStr=$this->EntityNameStr; $QueryCondition= dxQ::All(); // Redefined constraining query $QueryCondition= dxQ::Equal( dxQN::Category()->CategoryParentId, $this->getInputValue(\"ConstrainingCategoryId\",true) ); // Code that remains the same } On the javascript side, we do a few things. Below you will see the whole javascript.js file. We will break the changes into 3 things: We change the behaviour of the on_item_clicked() function to reload the current page with the new category constraint ID.There is a lot of auto-generated boilerplate code relating to the modal, which we will not use. However, we do want to put a 'category_create' component in the modal. This is again as easy as a few clicks in the the component builder.Change the outcome of the event 'category_created'. divbloxPHP has built in boilerplate code for such events, which we fill in to both hide the modal and refresh the page. With this, we have updated all the functionality needed for the Category entity, created a page to edit the categories as well as have visual aid with regard to the hierarchical structure of the categories. Here is a quick replay of what we built: "},{"title":"Ticket Functionality","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#ticket-functionality","content":"Now we will focus on the changes to the Ticket entity and its pages, which will be split into 3 parts: Updating the 'ticket_crud_update' component with SubTask and Note CRUDCustomizing the sub tasksCustomizing the notes and attachments "},{"title":"Update Component Changes","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#update-component-changes","content":"We have already set up our 'create' and 'update' components to work in a streamline way: When creating a ticket, only having to input the ticket name and description,after which you are navigated to the ticket_update page where you can complete all other relevant fields. We now want to have a sub tasks list and a notes list in this component. We first create the CRUD components for each using the divbloxPHP Component Builder, after which we just insert them into our ticket_crud_update' component (in their own row, taking up equal 6 columns each in bootstrap terms). "},{"title":"Ticket progression functionality","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#ticket-progression-functionality","content":"The ticket progression idea is somewhat loosely defined as a concept, so we need to define what we actually mean by it. The definition we will implement is as follows: If a ticket has no subtasks, then if it's status is completed its TicketProgression will be 100% and if it is anything else it will be 0%.If a ticket has subtasks, then the ticket's status is ignored and the number of subtasks with status Complete divided by the number of subtasks that are not complete, will give us the percentage TicketProgression. This logic will be defined directly in the Ticket entity Save() function, which can be overridden in the Ticket.class.php file (project/assets/php/data_model_orm/Ticket.class.php). Below are the Save() and Delete() functions with the changes made. You will also see the changes made to store the categoryCount value as explained in the basic training exercise. Copy publicfunctionSave($blnForceInsert=false,$blnForceUpdate=false){ $ExistingObj= Ticket::Load($this->intId); // Calculating TicketProgression /////////////////////////////////////// // Total number of subtasks (int) $TotalInt= SubTask::QueryCount( dxQ::Equal( dxQN::SubTask()->TicketObject->Id, $this->intId ) ); // Completed number of subtasks (int) $CompletedInt= SubTask::QueryCount( dxQ::AndCondition( dxQ::Equal( dxQN::SubTask()->TicketObject->Id, $this->intId ), dxQ::Equal( dxQN::SubTask()->SubTaskStatus, \"Complete\" ) ) ); $TicketProgress=0; if($ExistingObj){ // If there are SubTasks if($TotalInt!==0){ $TicketProgress=round(($CompletedInt/$TotalInt)*100); // if there aren't subtasks, and TicketStatus is Complete }elseif($ExistingObj->TicketStatus==\"Complete\"){ $TicketProgress=100; } // If it is not Complete, the default value of 0 remains } //////////////////////////////////////////////////////////////////// $this->intTicketProgress=$TicketProgress; $mixToReturn=parent::Save($blnForceInsert,$blnForceUpdate); // Category Count logic ///////////////////////////////////// // Count the number of tickets in each category $CategoryObj= Category::Load($this->intCategory); if(!is_null($CategoryObj)){ $TicketCount= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); $CategoryObj->TicketCount=$TicketCount; $CategoryObj->Save(); } /////////////////////////////////////////////////////////////// return$mixToReturn; } publicfunctionDelete(){ // Update category count when tickets are deleted. $CategoryObj= Category::Load($this->intCategory); parent::Delete(); if(!is_null($CategoryObj)){ $TicketCount= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->Id, $CategoryObj->Id ) ); $CategoryObj->TicketCount=$TicketCount; $CategoryObj->Save(); } } "},{"title":"Customizing the SubTask CRUD","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#customizing-the-subtask-crud","content":"The sub tasks are already constrained by the parent Ticket ID, so all we need to do is make the HTML formatting a little bit more to our liking. You can make the input boxes full-width and arrange them in a bootstrap layout to your liking. We change the create layout to maximize the space we have: "},{"title":"Customizing the Note CRUD","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#customizing-the-note-crud","content":"The Note section is a bit more complicated, for a few reasons. Firstly, we want to be able to attach files here, which need to be constrained to the currently opened ticket. Let's start off by creating a 'note_attachment_uploader' component which we will tailor to our needs, based off of the default 'default_file_upload' component. This is done via the component builder, simply by creating a new component from the existing component \"default_file_upload\". As before, there is a lot of background code functionality we will not discuss here, but encourage you to sift through to understand. We will outline the changes made and why we made them. In the component.js file, we just had to add an additional input parameter (note_id) into the JQuery upload in the initFileUploader() function, as we need this ID so as to constrain the attachment to the current note. Copy initFileUploader(){ let uid =this.uid; let this_component =this; $('#'+uid+'_file_uploader').fileuploader({ changeInput:// Default input onSelect:function(item){ // Default functionality }, upload:{ url:getComponentControllerPath(this_component), data:{f:\"handleFilePost\", AuthenticationToken:getValueFromAppState('dxAuthenticationToken'), // NEW INPUT PARAMETER DEFINED note_id: this_component.getLoadArgument(\"note_id\")}, type:'POST', enctype:'multipart/form-data', start:false, synchron:true, beforeSend:function(item){ // Default functionality }, onSuccess:function(result, item){ // Default functionality }, onError:function(item){ // Default functionality }, onProgress:function(data, item){ // Default functionality }, onComplete:function(){ // Default functionality }, }, onRemove:function(item){ // Default functionality }, captions:{ // Default captions }, enableApi:true }); } On the PHP side, we override the default functionality of the handleFilePost() function, adding the code snippet indicated below. We query the Note table by the \"note_id\" passed, and proceed with a few checks. If the Note object is null, we delete the corresponding file upload (this is to prevent having orphaned data). If the Note object exists, we delete whatever file (if any) was uploaded before and save the new file. With this simple set up each note will only be able to have none or one attachment. Copy publicfunctionhandleFilePost(){ // initialize FileUploader $FileUploader=newFileUploader('files',array( 'uploadDir'=>$this->UploadPath, 'title'=>'auto' )); $this->setResult(true); // call to upload the files $data=$FileUploader->upload(); $this->setReturnValue(\"Message\",$data); foreach($data[\"files\"]as$file){ $FileDocumentObj=newFileDocument(); $FileDocumentObj->FileName=$file[\"name\"]; $FileDocumentObj->Path=$file[\"file\"]; $FileDocumentObj->UploadedFileName=$file[\"old_name\"]; $FileDocumentObj->FileType=$file[\"type\"]; $FileDocumentObj->SizeInKilobytes=round(doubleval(preg_replace('/[^0-9.]+/','',$file[\"size2\"])),2); $FileDocumentObj->Save(); // START NEW CODE $NoteObj= Note::Load($this->getInputValue(\"note_id\",true)); if(is_null($NoteObj)){ $FileDocumentObj->Delete(); }else{ if(!is_null($NoteObj->FileDocumentObject)){ $NoteObj->FileDocumentObject->Delete(); } $NoteObj->FileDocumentObject=$FileDocumentObj; $NoteObj->Save(); } // END NEW CODE } foreach($dataas$key=>$value){ $this->setReturnValue($key,$value); } $this->presentOutput(); } Now that we have prepared our file uploader to link to the current note, let's dig into the actual Note CRUD. Firstly, we want to follow a similar approach as with the Ticket and Category create CRUD components, whereby the initial create only requires limited fields, after which you are navigated to the update component to complete the process. We do this by shifting the 'note_created' case of the eventTriggered() function in the 'note_crud' component to above the 'note_clicked' case, as before. We can then add the two buttons we want via the component builder. These are: A modal popup housing the custom file uploaderA download link, appearing only when there is actually an attachment. First, we add a row with two columns in the update component. We then add the modal using the component builder, and change relevant text and button text as well as make the modal button have classes 'full-width' and 'btn-link'. Below is a walk through of the step we take in the component builder: We can also go ahead and remove the modal footer as those buttons are not needed. Now we will proceed to inspect the necessary code changes applied to create the functionality we need. Starting with the update component javascript: Firstly we want to make sure that the modal will always be closed until clicked, we do this in the component's reset() functionWe then add functionality to our modal boilerplate code. We wish to pass the current note's note_id to the note_attachment_uploader inside the modal. This is done in the initCustomFunctions() function. This note_id is how we are able to constrain attachments to the current note.Note that you generated modal ID will probably be different to the one below.We also decide to override the eventTriggered() 'FileUploaded' case to reset the component, from a UX point of view. If i have described my note and added an attachment, I am most likely done with that note.Finally, we also want to override the onAfterLoadEntity() function to populate our right column with a download link if and only if the attachment exists. Copy if(typeof component_classes[\"data_model_note_crud_update\"]===\"undefined\"){ classdata_model_note_crud_updateextendsDivbloxDomEntityInstanceComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[]; // Sub component config end this.included_attribute_array=[\"NoteDescription\"]; this.included_relationship_array=[]; this.constrain_by_array=[\"Ticket\"]; this.data_validation_array=[]; this.custom_validation_array=[]; this.required_validation_array=[\"NoteDescription\"]; this.initCrudVariables(\"Note\"); } reset(inputs, propagate){ if(typeof inputs !==\"undefined\"){ this.setEntityId(inputs); } super.reset(inputs, propagate); // Make sure modal is always initially hidden getComponentElementById(this,\"HPxt9_modal\").modal(\"hide\"); } initCustomFunctions(){ // HPxt9_modal Related functionality //////////////////////////////////////////////////////////////////////////////////////////////////////////////// getComponentElementById(this,\"HPxt9_btn-close\").on( \"click\", function(){ // Your custom code here }.bind(this) ); // Modal functions // Show the modal using javascript //getComponentElementById(this,\"HPxt9_modal\").modal(\"show\"); // Hide the modal using javascript //getComponentElementById(this,\"HPxt9_modal\").modal(\"hide\"); // Toggle the modal using javascript //getComponentElementById(this,\"HPxt9_modal\").modal(\"toggle\"); // Modal events getComponentElementById(this,\"HPxt9_modal\").on( \"show.bs.modal\", function(e){ // The loadComponent() function was added in the component builder, // We just want to add the note_id parameter loadComponent( \"system/note_attachment_uploader\", this.getUid(), \"XLGKu\", { note_id:this.getEntityId()}, true ); }.bind(this) ); getComponentElementById(this,\"HPxt9_modal\").on( \"shown.bs.modal\", function(e){ // Your custom code here }.bind(this) ); getComponentElementById(this,\"HPxt9_modal\").on( \"hide.bs.modal\", function(e){ // Your custom code here }.bind(this) ); getComponentElementById(this,\"HPxt9_modal\").on( \"hidden.bs.modal\", function(e){ // Your custom code here }.bind(this) ); //////////////////////////////////////////////////////////////////////////////////////////////////////////////// } eventTriggered(event_name, parameters_obj){ switch(event_name){ case\"FileUploaded\": this.reset(); break; default: dxLog( \"Event triggered: \"+ event_name + \": \"+ JSON.stringify(parameters_obj) ); } // Let's pass the event to all sub components this.propagateEventTriggered(event_name, parameters_obj); } onAfterLoadEntity(data_obj){ // Check to see if there is an attachment. Only display download button if it does exist getComponentElementById(this,\"DownloadWrapper\").html(\"\"); if(typeof data_obj.AttachmentPath!==\"undefined\"){ if(data_obj.AttachmentPath.length>0){ getComponentElementById(this,\"DownloadWrapper\").html( '<a href=\"'+ data_obj.AttachmentPath+ '\" target=\"_blank\" class=\"btn btn-link fullwidth\">Download Attachment</a>' ); } } } } component_classes[ \"data_model_note_crud_update\" ]= data_model_note_crud_update; } Now for the backend side. Below is the note update component.php file. Firstly, we override the default getObjectData() function. What we added here is backend validation for the existence and validity of the attachment by checking the relational entity FileDocument. The only return values the front end can receive is an empty string or a valid attachment path string that points to a file that exists in the database. Secondly, we need to make sure that if we delete any notes, we do not accidentally leave behind any orphaned files/images. This is done using divbloxPHP's doBeforeDeleteActions() function. Copy require(\"../../../../divblox/divblox.php\"); classNoteControllerextendsEntityInstanceComponentController{ protected$EntityNameStr=\"Note\"; protected$IncludedAttributeArray=[\"NoteDescription\",]; protected$IncludedRelationshipArray=[]; protected$ConstrainByArray=[\"Ticket\",]; protected$RequiredAttributeArray=[]; protected$NumberValidationAttributeArray=[]; publicfunction__construct($ComponentNameStr='Component'){ parent::__construct($ComponentNameStr); } publicfunctiongetObjectData(){ $EntityObj=$this->EntityNameStr::Load( $this->getInputValue(\"Id\",true) ); $EntityJsonDecoded=array(); // Set the attachment string to \"\" as default $AttachmentPathStr=\"\"; if(!is_null($EntityObj)){ $EntityJsonDecoded=json_decode($EntityObj->getJson()); // Check if the FIleDocumentObject actually exists and is valid, // only then set $AttachmentPathStr to the string if(!is_null($EntityObj->FileDocumentObject)){ if(file_exists(DOCUMENT_ROOT_STR.SUBDIRECTORY_STR.$EntityObj->FileDocumentObject->Path)){ $AttachmentPathStr= ProjectFunctions::getBaseUrl().$EntityObj->FileDocumentObject->Path; } } } $this->setReturnValue(\"Object\",$EntityJsonDecoded); foreach($this->IncludedRelationshipArrayas$Relationship=>$DisplayValue){ $RelationshipList=$this->getRelationshipList($EntityObj,$Relationship); $this->setReturnValue($Relationship.\"List\",$RelationshipList); } $this->setResult(true); $this->setReturnValue(\"Message\",\"\"); $this->setReturnValue(\"AttachmentPath\",$AttachmentPathStr); $this->presentOutput(); } publicfunctiondoBeforeDeleteActions($EntityToUpdateObj=null){ if(is_null($EntityToUpdateObj)){ return; } // Delete FileDocumentObject before Note is deleted if(!is_null($EntityToUpdateObj->FileDocumentObject)){ $EntityToUpdateObj->FileDocumentObject->Delete(); } } } $ComponentObj=newNoteController(\"note_crud_update\"); And that is it! We have now finished the Ticket functionality we require. "},{"title":"Dashboard","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#dashboard","content":"We will now build a dashboard which should give us a summary overview of the tickets we have and their distribution into categories and statuses. The first step is to create a page with a sidebar navigation component, and then proceed to add the necessary link into the menu we created. Once we have our page set up and navigation configured, we can proceed with our dashboard. The final product will look something like this: To create the dashboard we will create a few individual components, that can be reused anywhere: A \"Status Tile\" component for the 6 status tiles seen in the top rowThe account summary listThe overdue ticket listGraphs using ChartJS "},{"title":"Status Tile","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#status-tile","content":"Let's create the tiles that display a summary of the breakdown of statuses of all tickets. As you can see, we will create a single component, and reuse it on our Dashboard page with different input parameters. These input parameters are specified in the javascript of the parent element, i.e. our dashboard page itself. This is done in the subcomponent definitions of the page by adding a secondary load argument as seen below: Now, to build our component. Once created, we add a fullwidth container and two rows, the first split into a 9-width bootstrap column and a 3-width bootstrap column, and the second a full 12-width column. These 3 sections will be the sections for the: Status nameNumber of ticketsPercentage of total tickets that status represents We will look at the relevant component files individually. These are: component.htmlcomponent.jscomponent.phpcomponent.json (unedited)component.css (unedited) The full HTML file looks like this: Copy <div xmlns=\"http://www.w3.org/1999/xhtml\" id=\"ComponentWrapper\" class=\"component-wrapper\" > <div id=\"ComponentPlaceholder\" class=\"component_placeholder component_placeholder_general\" > <divid=\"ComponentFeedback\"></div> </div> <divid=\"ComponentContent\"class=\"component-content\"style=\"display:none;\"> <divxmlns=\"\"class=\"container-fluid\"> <divid=\"StatusWrapper\"class=\"dashboard-tile\"> <divclass=\"row\"> <divclass=\"col-9\"> <div id=\"StatusLabel\" class=\"StatusLabel float-left ml-1\" > {Status} </div> </div> <divclass=\"col-3\"> <div id=\"StatusCount\" class=\"StatusCount float-right mr-4\" > {X} </div> </div> </div> <divclass=\"row\"> <divclass=\"col-12\"> <divid=\"StatusPercentage\"class=\"StatusPercentage\"> {StatusPercentage} </div> </div> </div> </div> </div> </div> </div> Note the IDs and class names which will be used to input information from the backend and deal with CSS styling. We also use built-in bootstrap classes such as float-left and ml-1, mr-4, etc to position our text as needed. Further CSS classes can and will be added if needed. Now taking a look at the javascript file: Copy if( typeof component_classes[\"dashboard_ticket_status_indicator\"]=== \"undefined\" ){ classdashboard_ticket_status_indicatorextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[]; // Sub component config end } // The reset() function (which is called ever time the component is refreshed) is altered // to include the two additionally defined functions reset(inputs, propagate){ super.reset(inputs, propagate); this.applyStatusCssClass(); this.loadStatusTotals(); } // Function to add necessary CSS classes to our 3 display nodes applyStatusCssClass(){ let status =this.getLoadArgument(\"ticket_status\") .replace(\" \",\"-\") .toLowerCase(); getComponentElementById(this,\"StatusWrapper\").addClass( \"dashboard-tile-\"+ status ); getComponentElementById(this,\"StatusCount\").addClass( \"status-count-\"+ status ); getComponentElementById(this,\"StatusPercentage\").addClass( \"status-percentage-\"+ status ); } // Function to retrieve necessary information from the backend. Notice how nothing is // status-specific, as we will be using the same component for 6 Statuses loadStatusTotals(){ // Function dealing with backend communication dxRequestInternal( // Associated PHP script for this component getComponentControllerPath(this), // Parameter object { f:\"loadStatusTotals\", ticket_status:this.getLoadArgument(\"ticket_status\"), }, function(data_obj){ // Success function: Writes information onto relevant DOM nodes getComponentElementById(this,\"StatusLabel\").html( \"<p>\"+this.getLoadArgument(\"ticket_status\")+\":</p>\" ); let status =this.getLoadArgument(\"ticket_status\") .replace(\" \",\"-\") .toLowerCase(); // JQuery animate function: Creates a linear animation from 0 to // the relevant value $({Counter:0}).animate( { Counter: data_obj.Count, }, { duration:1500, easing:\"linear\", step:function(){ $(\".status-count-\"+ status).html( Math.ceil(this.Counter) ); }, } ); $({Counter:0}).animate( { Counter: data_obj.Percentage, }, { duration:2000, easing:\"linear\", step:function(){ $(\".status-percentage-\"+ status).html( (this.Counter*100).toFixed(2)+\"%\" ); }, } ); }.bind(this), function(data_obj){ // Failure function dxLog( \"dxRequestInternal Failure. Data Object returned: \"+ JSON.stringify(data_obj) ); } ); } } component_classes[ \"dashboard_ticket_status_indicator\" ]= dashboard_ticket_status_indicator; } Moving onto the backend side, looking at the ticket_status_indicator component.php file: Copy require(\"../../../../divblox/divblox.php\"); classTicketStatusIndicatorControllerextendsProjectComponentController{ publicfunction__construct($ComponentNameStr='Component'){ parent::__construct($ComponentNameStr); } // Function that is called by the frontend, and returns the necessary information publicfunctionloadStatusTotals(){ $StatusStr=$this->getInputValue(\"ticket_status\"); // Query the database using the input parameter (this is how each component displays // different information) $StatusTicketCountInt= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->TicketStatus, $StatusStr ) ); $TotalTicketCountInt= Ticket::QueryCount( dxQ::All() ); $StatusPercentage=$StatusTicketCountInt/$TotalTicketCountInt; // Set and present required data $this->setResult(true); $this->setReturnValue(\"Count\",$StatusTicketCountInt); $this->setReturnValue(\"Percentage\",$StatusPercentage); $this->presentOutput(); } } $ComponentObj=newTicketStatusIndicatorController(\"ticket_status_indicator\"); "},{"title":"Account Summary","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#account-summary","content":"For any custom data list, there are quite a few things to consider, but when you understand those, using a default entity data list to construct anything you need becomes very quick and easy. Firstly, we create a data list from the component builder for the Account entity, including the attributes: FirstNameLastNameProfilePicturePath Then we will edit the default addRow() function in the component.js file (to change the generated HTML rows) as well as the getPage() function in the component.php (to add the 'In Progress' and 'Overdue' ticket counts to the information passed to the front end) Again, we will look at each of the 5 components individually (omitting the unchanged .css and .json files). Looking at the HTML that is generated by the component builder, we see that there isn't really anything we want to change. The HTML file gives us a template for the structure or the component, including pagination and search functionality. The only thing we do add is a heading describing the component purpose. Copy <divid=\"ComponentWrapper\"class=\"component-wrapper\"> <div id=\"ComponentPlaceholder\" class=\"component_placeholder component_placeholder_data_list\" > <divid=\"ComponentFeedback\"></div> </div> <divid=\"ComponentContent\"class=\"component-content\"style=\"display:none\"> <divclass=\"container-fluid container-no-gutters\"> <divclass=\"row\"> <divclass=\"col-md-6\"> <!-- Added heading --> <pclass=\"heading\">Account Summary</p> </div> <divclass=\"col-md-6\"> <divclass=\"input-group mb-3\"> <input type=\"text\" id=\"DataListSearchInput\" class=\"form-control data_table_search_icon\" placeholder=\"Search...\" aria-label=\"Search\" aria-describedby=\"btnResetSearch\" /> <divclass=\"input-group-append\"> <button class=\"btn btn-outline-secondary\" type=\"button\" id=\"btnResetSearch\" > <iclass=\"fa fa-times\"aria-hidden=\"true\"></i> </button> </div> </div> </div> </div> <divclass=\"row\"> <divclass=\"col-12\"> <divid=\"DataList\"class=\"list-group\"></div> <divid=\"DataListLoading\">Loading...</div> </div> </div> <divclass=\"row\"> <divclass=\"col-md-4\"></div> <divclass=\"col-md-4\"> <button type=\"button\" id=\"DataListMoreButton\" class=\"btn btn-link fullwidth\" > <iclass=\"fa fa-repeat\"aria-hidden=\"true\"></i> Load More </button> </div> <divclass=\"col-md-4\"></div> </div> </div> </div> </div> You might think that you need to restructure how your list will be displayed here, but that is done in the javascript and just written into the necessary DOM node. So looking at our javascript file, we note that the only changes we make to the default functionality lies in the addRow() function, where we will create each row to display the information we need in the format we want it. Copy if( typeof component_classes[\"data_model_account_summary_list\"]===\"undefined\" ){ classdata_model_account_summary_listextendsDivbloxDomEntityDataListComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[]; // Sub component config end this.included_attributes_object={ FullName:\"Normal\", FirstName:\"Normal\", LastName:\"Normal\", ProfilePicturePath:\"Normal\", Title:\"Normal\", }; this.included_relationships_object={UserRole:\"Normal\"}; this.constrain_by_array=[]; this.initDataListVariables(\"Account\"); } // The addrow() function writes each row dynamically into the data list addRow(row_data_obj){ let current_item_keys =Object.keys(this.current_page_array); let must_add_row =true; current_item_keys.forEach( function(key){ if( this.current_page_array[key][\"Id\"]== row_data_obj[\"Id\"] ){ must_add_row =false; } }.bind(this) ); if(!must_add_row){ return; } this.current_page_array.push(row_data_obj); let row_id = row_data_obj[\"Id\"]; let included_keys =Object.keys(this.included_all_object); // Create a wrapper for all the information we will be displaying. These two strings create the anchor uniform look and feel of the list // using both Divblox classes as well as bootstrap classes. let wrapping_html = '<a href=\"#\" id=\"'+ this.getUid()+ \"_row_item_\"+ row_id + '\" class=\"list-group-item'+ \" list-group-item-action flex-column align-items-start data_list_item data_list_item_\"+ this.getUid()+ ' dx-data-list-row\">'; // Here we create our custom DOM node frame for the information we want to display let profile_picture_html ='<div class=\"col-2\">'; let account_info_html ='<div class=\"col-4\">'; let status_summary_html ='<div class=\"col-6\">'; // We append the necessary formatting and information // The col-2 section for the profile picture profile_picture_html += '<img data-action=\"zoom\" class=\"dashboard-tile-profile-picture\" src=\"'+ row_data_obj[\"ProfilePicturePath\"]+ '\" alt=\"Profile Picture\">'+ \"</div>\"; // The col-4 section for the account holders full name account_info_html += '<div class=\"row\"> <div class=\"col-12 dashboard-tile-list\">'+ row_data_obj[\"FirstName\"]+ \"<br>\"+ row_data_obj[\"LastName\"]+ \"</div>\"; account_info_html +=\"</div>\"+\"</div>\"; // The col-6 section displaying number of tickets that are overdue or // in progress status_summary_html +='<div class=\"row\">'; let status_array =[\"In Progress\",\"Overdue\"]; status_array.forEach(function(status){ status_summary_html += '<div class=\"col-6 dashboard-tile-list\">'+ status + \" <br> <strong>\"+ row_data_obj[\"StatusCounts\"][status]+ \"</strong></div>\"; }); status_summary_html +=\"</div>\"; // Appending all necessary strings to create the final HTML to be inserted into the DOM wrapping_html += '<div class=\"row\">'+ profile_picture_html + account_info_html + status_summary_html + \"</div>\"+ \"</div>\"; wrapping_html +=\"</a>\"; // Writing to the DOM node by ID getComponentElementById(this,\"DataList\").append(wrapping_html); } } component_classes[ \"data_model_account_summary_list\" ]= data_model_account_summary_list; } Adding this component to our dashboard is shown below. Note that all of the CSS will be shown at the end, but is not the purpose of this exercise, so we encourange you to style it as you see fit. Now looking at the component.php file. This is a long one. Feel free to browse through the entire code to familiarize yourself with the background process, but the changes we have made will be defined with prepended comments. Copy classAccountControllerextendsEntityDataSeriesComponentController{ protected$EntityNameStr=\"Account\"; protected$IncludedAttributeArray=[\"FullName\",\"FirstName\",\"LastName\",\"ProfilePicturePath\",\"Title\",]; protected$IncludedRelationshipArray=[]; protected$ConstrainByArray=[]; protected$RequiredAttributeArray=[]; protected$NumberValidationAttributeArray=[]; publicfunction__construct($ComponentNameStr='Component') { parent::__construct($ComponentNameStr); } publicfunctiongetPage(){ error_log(\"Constrain by values: \".json_encode($this->ConstrainByArray)); $EntityNodeNameStr=$this->EntityNameStr; $DefaultSortAttribute=$this->IncludedAttributeArray[0]; if(is_null($this->getInputValue(\"ItemsPerPage\"))){ $this->setResult(false); $this->setReturnValue(\"Message\",\"No items per page provided\"); $this->presentOutput(); } $AccessArray= ProjectAccessManager::getObjectAccess(ProjectFunctions::getCurrentAccountId(),$this->EntityNameStr); if(!in_array(AccessOperation::READ_STR,$AccessArray)){ $this->setResult(false); $this->setReturnValue(\"Message\",\"Read access denied\"); $this->presentOutput(); } $Offset=$this->getInputValue(\"CurrentOffset\",true); if($Offset<0){ $Offset=($this->getInputValue(\"CurrentPage\",true)-1)*$this->getInputValue(\"ItemsPerPage\",true); } if($Offset<0){ $Offset=0; } $QueryCondition= dxQ::All(); foreach($this->ConstrainByArrayas$Relationship){ $RelationshipNodeStr=$Relationship.'Object'; $QueryCondition= dxQ::AndCondition( $QueryCondition, dxQ::Equal( dxQN::$EntityNodeNameStr()->$RelationshipNodeStr->Id,$this->getInputValue('Constraining'.$Relationship.'Id',true) ) ); } $this->setReturnValue(\"This1\",$this->getInputValue(\"SearchText\")); if(!is_null($this->getInputValue(\"SearchText\"))){ if(strlen($this->getInputValue(\"SearchText\"))>0){ $SearchInputStr=\"%\".$this->getInputValue(\"SearchText\").\"%\"; $this->setReturnValue(\"This\",$SearchInputStr); $QueryOrConditions=null; foreach($this->IncludedAttributeArrayas$Attribute){ if(is_null($QueryOrConditions)){ $QueryOrConditions= dxQ::Like(dxQueryN::$EntityNodeNameStr()->$Attribute,$SearchInputStr); }else{ $QueryOrConditions= dxQ::OrCondition($QueryOrConditions, dxQ::Like(dxQueryN::$EntityNodeNameStr()->$Attribute,$SearchInputStr)); } }; foreach($this->IncludedRelationshipArrayas$Relationship=>$DisplayAttribute){ $RelationshipNodeStr=$Relationship.'Object'; if(is_null($QueryOrConditions)){ $QueryOrConditions= dxQ::Like(dxQueryN::$EntityNodeNameStr()->$RelationshipNodeStr->$DisplayAttribute,$SearchInputStr); }else{ $QueryOrConditions= dxQ::OrCondition($QueryOrConditions, dxQ::Like(dxQueryN::$EntityNodeNameStr()->$RelationshipNodeStr->$DisplayAttribute,$SearchInputStr)); } }; } } $OrderByClause= dxQ::OrderBy(dxQueryN::$EntityNodeNameStr()->$DefaultSortAttribute); if(!is_null($this->getInputValue(\"SortOptions\"))){ if(ProjectFunctions::isJson($this->getInputValue(\"SortOptions\"))){ $SortOptionsArray=json_decode($this->getInputValue(\"SortOptions\")); if(is_array($SortOptionsArray)){ if(ProjectFunctions::getDataSetSize($SortOptionsArray)==2){ $AttributeStr=$SortOptionsArray[0]; $OrderByClause= dxQ::OrderBy(dxQueryN::$EntityNodeNameStr()->$AttributeStr,$SortOptionsArray[1]); } } } } $EntityArray=$EntityNodeNameStr::QueryArray( $QueryCondition, dxQ::Clause( $OrderByClause, dxQ::LimitInfo($this->getInputValue(\"ItemsPerPage\",true),$Offset) ) ); $EntityReturnArray=[]; foreach($EntityArrayas$EntityObj){ $CompleteReturnArray=[\"Id\"=>$EntityObj->Id]; foreach($this->IncludedAttributeArrayas$Attribute){ if(in_array($this->DataModelObj->getEntityAttributeType($this->EntityNameStr,$Attribute),[\"DATE\",\"DATETIME\"])){ $CompleteReturnArray[$Attribute]=is_null($EntityObj->$Attribute)?'N/A':$EntityObj->$Attribute->format(DATE_TIME_FORMAT_PHP_STR.\" H:i:s\"); }elseif($Attribute=='ProfilePicturePath'){ $AttachmentPathStr= ProjectFunctions::getBaseUrl().\"/project/assets/images/divblox_profile_picture_placeholder.svg\"; if(!is_null($EntityObj->ProfilePicturePath)){ if(file_exists(DOCUMENT_ROOT_STR.SUBDIRECTORY_STR.$EntityObj->ProfilePicturePath)){ $AttachmentPathStr= ProjectFunctions::getBaseUrl().$EntityObj->ProfilePicturePath; } } $CompleteReturnArray[$Attribute]=$AttachmentPathStr; }else{ $CompleteReturnArray[$Attribute]=is_null($EntityObj->$Attribute)?'N/A':$EntityObj->$Attribute; } } foreach($this->IncludedRelationshipArrayas$Relationship=>$DisplayAttribute){ $RelationshipReturnStr=\"N/A\"; $RelationshipNodeStr=$this->DataModelObj->getEntityRelationshipPathAsNode($EntityObj,$this->EntityNameStr,$Relationship,[]); if(!is_null($RelationshipNodeStr)){ if(!is_null($RelationshipNodeStr->$DisplayAttribute)){ if(in_array($this->DataModelObj->getEntityAttributeType($Relationship,$DisplayAttribute),[\"DATE\",\"DATETIME\"])){ $RelationshipReturnStr=$RelationshipNodeStr->$DisplayAttribute->format(DATE_TIME_FORMAT_PHP_STR.\" H:i:s\"); }else{ $RelationshipReturnStr=is_null($RelationshipNodeStr->$DisplayAttribute)?'N/A':$RelationshipNodeStr->$DisplayAttribute; } } } $CompleteReturnArray[$Relationship]=$RelationshipReturnStr; } // All we want to do is add additional information to the return array // Fill up an array of number of tickets in each status $StatusCountArray=[]; $StatusKeys=[\"New\",\"In Progress\",\"Due Soon\",\"Urgent\",\"Complete\",\"Overdue\"]; foreach($StatusKeysas$Key){ $StatusCountInt= Ticket::QueryCount( dxQ::AndCondition( dxQ::Equal( dxQN::Ticket()->AccountObject->Id, $EntityObj->Id ), dxQ::Equal( dxQN::Ticket()->TicketStatus, $Key ) ) ); $StatusCountArray[$Key]=$StatusCountInt; } // Append our array of status counts to the final return array (nested array) $CompleteReturnArray[\"StatusCounts\"]=$StatusCountArray; array_push($EntityReturnArray,$CompleteReturnArray); } $this->setResult(true); $this->setReturnValue(\"Message\",\"\"); $this->setReturnValue(\"Page\",$EntityReturnArray); $this->setReturnValue(\"TotalCount\",$EntityNodeNameStr::QueryCount($QueryCondition)); $this->presentOutput(); } } $ComponentObj=newAccountController(\"account_summary_list\"); Done! It is normal for your components to look unpolished until you add the CSS necessary to scale the profile pictures, center the text, etc. You can spend some time to make it look as you please. You can do this in the one of three places: In the current component's component.css fileIn the parent (dashboard page) component.css file (These classes will be accessible to all child components in the page)In the file themes.css (Accessible across your project) Where you define your classes is obviously dependant on the scope in which you will be using them. "},{"title":"Overdue Tickets","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#overdue-tickets","content":"This component is constructed exactly the same way we constructed the account summary list. We create a list component using the component builder (including all the attributes we need). Below is the component.js file, in which we again altered the addRow() function to display the information we want neatly. Since all the information we want to display is already directly stored in the database, we do not need to edit the component.php file. Copy if( typeof component_classes[\"data_model_ticket_summary_list\"]===\"undefined\" ){ classdata_model_ticket_summary_listextendsDivbloxDomEntityDataListComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions=[]; // Sub component config end this.included_attributes_object={ TicketName:\"Normal\", TicketDueDate:\"Normal\", TicketStatus:\"Normal\", TicketProgress:\"Normal\", }; this.included_relationships_object={ Account:\"Normal\", Category:\"Normal\", }; this.constrain_by_array=[]; this.initDataListVariables(\"Ticket\"); } addRow(row_data_obj){ let current_item_keys =Object.keys(this.current_page_array); let must_add_row =true; current_item_keys.forEach( function(key){ if( this.current_page_array[key][\"Id\"]== row_data_obj[\"Id\"] ){ must_add_row =false; } }.bind(this) ); if(!must_add_row){ return; } this.current_page_array.push(row_data_obj); let row_id = row_data_obj[\"Id\"]; let included_keys =Object.keys(this.included_all_object); let wrapping_html = '<a href=\"#\" id=\"'+ this.getUid()+ \"_row_item_\"+ row_id + '\" class=\"list-group-item'+ \" list-group-item-action flex-column align-items-start data_list_item data_list_item_\"+ this.getUid()+ ' dx-data-list-row\">'; let header_wrapping_html = '<div class=\"d-flex w-100 justify-content-between\">'; let ticket_name_html ='<div class=\"col-3\">'; let account_name_html ='<div class=\"col-4\">'; let ticket_date_html ='<div class=\"col-4\">'; let ticket_progress_html = '<div class=\"col-1 float-right\" style=\"color: red;\">'; let account_names = row_data_obj[\"Account\"].split(\" \"); let return_account_name = account_names[0].slice(0,1)+\". \"+ account_names[1]; ticket_name_html += row_data_obj[\"TicketName\"]+\"</div>\"; account_name_html += return_account_name +\"</div>\"; ticket_date_html += row_data_obj[\"TicketDueDate\"]+\"</div>\"; ticket_progress_html += row_data_obj[\"TicketProgress\"]+\"%</div>\"; wrapping_html += '<div class=\"row\">'+ ticket_name_html + account_name_html + ticket_date_html + ticket_progress_html + \"</div>\"+ \"</div>\"; wrapping_html +=\"</a>\"; getComponentElementById(this,\"DataList\").append(wrapping_html); } } component_classes[ \"data_model_ticket_summary_list\" ]= data_model_ticket_summary_list; } "},{"title":"Graph components","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#graph-components","content":"We will now look at the two graph components we want to create for our dashboard. Divblox uses the ChartJS library, and further documentation can be found here. Divblox has a default chart component which is designed to be used as boilerplate code to be tweaked to create the necessary graph instead of having to create new ones. Below is a video of how to create new graph components via the component builder. Now that we have created (identical to the default) graph components, let's go through the changes we make to display the correct data and graph types. Ticket Status Graph# This component is a bar chart showing the number of tickets in each of the status's. We will not discuss the chartJS-specific code, as their documentation is thorough and well-defined. Below we show the ticket_status_graph component's component.js and component.php files. Copy if( typeof component_classes[\"data_visualization_status_bar_chart\"]=== \"undefined\" ){ classdata_visualization_status_bar_chartextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions={}; // Sub component config end this.chart_obj=null; this.prerequisite_array=[ \"project/assets/js/chartjs/Chart.min.js\", ]; } reset(inputs, propagate){ super.reset(inputs, propagate); this.initChart(); } updateChart(){ dxRequestInternal( getComponentControllerPath(this), { f:\"getData\"}, function(data_obj){ this.chart_obj.data= data_obj.Data; this.chart_obj.update(); }.bind(this), function(data_obj){ thrownewError(data_obj.Message); } ); } initChart(){ let ctx =this.uid+\"_ComponentChart\"; this.chart_obj=newChart(ctx,{ type:\"bar\", data:{ /* server data */ }, options:{ scales:{ yAxes:[ { ticks:{ beginAtZero:true, }, }, ], }, legend:{ display:false, }, }, }); this.updateChart(); } } component_classes[ \"data_visualization_status_bar_chart\" ]= data_visualization_status_bar_chart; } Copy require(\"../../../../divblox/divblox.php\"); classStatusBarChartControllerextendsProjectComponentController{ publicfunction__construct($ComponentNameStr='Component'){ parent::__construct($ComponentNameStr); } // Query relevant data publicfunctiongetData(){ // This is the actual graph data we query from database and set into an array $TicketStatusArray=[\"New\",\"In Progress\",\"Due Soon\",\"Urgent\",\"Complete\",\"Overdue\"]; foreach($TicketStatusArrayas$TicketStatus){ $TicketStatusCountArray[]= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->TicketStatus, $TicketStatus ) ); } // ReturnData is the array of data + other parameters sent to the front-end $ReturnData=array( \"labels\"=>$TicketStatusArray, \"datasets\"=> array([\"label\"=>\"Dataset 1 label\", \"data\"=>$TicketStatusCountArray, \"backgroundColor\"=>[ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)'], \"borderColor\"=>[ 'rgba(255,99,132,1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'], \"borderWidth\"=>1],)); $this->setResult(true); $this->setReturnValue(\"Data\",$ReturnData); $this->presentOutput(); } } $ComponentObj=newStatusBarChartController(\"status_bar_chart\"); Main Category Graph# This pie chart is supposed to show us the proportion of tickets in each of the main categories, i.e. Work, Sport and Leisure. Below we show the category_pie_chart's component's component.js and component.php files: Copy if( typeof component_classes[\"data_visualization_category_pie_chart\"]=== \"undefined\" ){ classdata_visualization_category_pie_chartextendsDivbloxDomBaseComponent{ constructor(inputs, supports_native, requires_native){ super(inputs, supports_native, requires_native); // Sub component config start this.sub_component_definitions={}; // Sub component config end this.chart_obj=null; this.prerequisite_array=[ \"project/assets/js/chartjs/Chart.min.js\", ]; } reset(inputs, propagate){ super.reset(inputs, propagate); this.initChart(); } updateChart(){ // Call to the backend dxRequestInternal( getComponentControllerPath(this), { f:\"getData\"}, function(data_obj){ this.chart_obj.data= data_obj.Data; this.chart_obj.update(); dxLog(\"AA: \"+ data_obj.DataArray); }.bind(this), function(data_obj){ thrownewError(data_obj.Message); } ); } // Changed the graph type to \"\"pie\" initChart(){ let ctx =this.uid+\"_ComponentChart\"; this.chart_obj=newChart(ctx,{ type:\"pie\", data:{ /* Server Data */ }, options:{ scales:{ yAxes:[ { ticks:{ beginAtZero:true, }, gridLines:{ display:false, }, }, ], xAxes:[ { gridLines:{ display:false, }, }, ], }, }, }); this.updateChart(); } } component_classes[ \"data_visualization_category_pie_chart\" ]= data_visualization_category_pie_chart; } Copy require(\"../../../../divblox/divblox.php\"); classCategoryPieChartControllerextendsProjectComponentController{ publicfunction__construct($ComponentNameStr='Component'){ parent::__construct($ComponentNameStr); } // Query the relevant data publicfunctiongetData(){ // Graph data we query from database and set into an array $CategoryLabelCountArray=[]; $CategoryLabelArray=[\"Sport\",\"Leisure\",\"Work\"]; foreach($CategoryLabelArrayas$Category){ $CategoryLabelCountArray[]= Ticket::QueryCount( dxQ::Equal( dxQN::Ticket()->CategoryObject->CategoryLabel, $Category ) ); } // Removed the default 2 datasets and replaced it with our actual data $ReturnData=array( \"labels\"=>$CategoryLabelArray, \"datasets\"=> array([\"label\"=>\"Categories\", \"data\"=>$CategoryLabelCountArray, \"backgroundColor\"=>[ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)'], \"borderColor\"=>[ 'rgba(255,99,132,1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'], \"borderWidth\"=>1],)); $this->setResult(true); $this->setReturnValue(\"Data\",$ReturnData); $this->setReturnValue(\"DataArray\",$CategoryLabelCountArray); $this->presentOutput(); } } $ComponentObj=newCategoryPieChartController(\"category_pie_chart\"); "},{"title":"Summary","type":1,"pageTitle":"Advanced Training Exercise","url":"docs/advanced-training-exercise#summary","content":"In this exercise you learned about some of the more advanced concepts of a Divblox project. If you understand all the functionality completely, you should now be able to build complex web applications, from start to finish using Divblox. If you would like to receive further hands-on training from the Divblox team, please reach out to us at support@divblox.com and we will arrange a consultation. "}]