├── ticket_viewer ├── models │ ├── __init__.py │ └── ticket.py ├── controllers │ ├── __init__.py │ └── ticket.py ├── __init__.py ├── static │ ├── description │ │ └── icon.png │ ├── src │ │ ├── less │ │ │ └── ticket_viewer.less │ │ ├── xml │ │ │ └── ticket_views.xml │ │ └── js │ │ │ ├── models.js │ │ │ └── controllers.js │ └── lib │ │ └── js │ │ └── router.js ├── data │ ├── ir.model.access.csv │ └── ticket_security.xml ├── demo │ └── ticket_demo.xml ├── __manifest__.py └── views │ ├── ticket_views.xml │ └── ticket_templates.xml ├── .gitignore ├── README.md └── LICENSE /ticket_viewer/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from . import ticket 3 | -------------------------------------------------------------------------------- /ticket_viewer/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from . import ticket 3 | -------------------------------------------------------------------------------- /ticket_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from . import models 3 | from . import controllers 4 | -------------------------------------------------------------------------------- /ticket_viewer/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bouvyd/odoo-js-demo/HEAD/ticket_viewer/static/description/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dotfiles 2 | .* 3 | !.gitignore 4 | # compiled python files 5 | *.py[co] 6 | 7 | # various virtualenv 8 | /bin/ 9 | /build/ 10 | /dist/ 11 | /include/ 12 | /lib/ 13 | /man/ 14 | /share/ 15 | /src/ 16 | 17 | -------------------------------------------------------------------------------- /ticket_viewer/data/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_demo_ticket_user,Demo Ticket - User,model_demo_ticket,base.group_user,1,1,1,1 3 | access_demo_ticket_portal,Demo Ticket - Portal,model_demo_ticket,base.group_portal,1,1,1,0 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odoo Experience 2017 - JS Demo 2 | 3 | This repository contains a small demo shown at Odoo Experience 2017. 4 | 5 | This very small app shows how one can use the Odoo JS Framework to have a minimal frontend application up and running in a few minutes. 6 | 7 | To follow the tutorial yourself, you can read the commit messages. 8 | -------------------------------------------------------------------------------- /ticket_viewer/data/ticket_security.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo Ticket - Own Ticket on Portal 5 | 6 | [('partner_id', '=', user.partner_id.id)] 7 | 8 | 9 | -------------------------------------------------------------------------------- /ticket_viewer/demo/ticket_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unable to login 5 | 6 | I keep entering every password I know without success :( 7 | 8 | 9 | 10 | 11 | 12 | Unable to work when internet is down 13 | 14 | This is unacceptable. 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ticket_viewer/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | { 3 | 'name': 'Ticket Viewer', 4 | 'version': '1.0', 5 | 'author': 'Damien Bouvy', 6 | 'website': 'https://www.damienbouvy.be', 7 | 'summary': 'Demo a WebApp to view tickets online', 8 | 'depends': ['web', 'base_setup', 'bus'], 9 | 'description': """ 10 | Ticket Viewer Demo 11 | ================== 12 | View & submit support tickets online. 13 | Odoo Experience 2017 demo of the Odoo Javascript Framework. 14 | """, 15 | "data": [ 16 | "views/ticket_views.xml", 17 | "views/ticket_templates.xml", 18 | "data/ir.model.access.csv", 19 | "data/ticket_security.xml", 20 | ], 21 | "demo": [ 22 | "demo/ticket_demo.xml", 23 | ], 24 | 'installable': True, 25 | 'application': True, 26 | 'license': 'LGPL-3', 27 | } 28 | -------------------------------------------------------------------------------- /ticket_viewer/controllers/ticket.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from odoo.addons.bus.controllers.main import BusController 3 | from odoo.http import request, route 4 | 5 | 6 | class TicketController(BusController): 7 | def _poll(self, dbname, channels, last, options): 8 | """Add the relevant channels to the BusController polling.""" 9 | if options.get('demo.ticket'): 10 | channels = list(channels) 11 | ticket_channel = ( 12 | request.db, 13 | 'demo.ticket', 14 | options.get('demo.ticket') 15 | ) 16 | channels.append(ticket_channel) 17 | return super(TicketController, self)._poll(dbname, channels, last, options) 18 | 19 | @route(['/tickets', '/tickets/'], auth='user') 20 | def view_tickets(self, **kwargs): 21 | tickets = request.env['demo.ticket'].search([]) 22 | return request.render('ticket_viewer.ticket_list', {'tickets': tickets}) 23 | -------------------------------------------------------------------------------- /ticket_viewer/models/ticket.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from odoo import api, fields, models 3 | 4 | 5 | class DemoTicket(models.Model): 6 | _name = 'demo.ticket' 7 | _description = 'Demo Ticket' 8 | 9 | name = fields.Char(required=True) 10 | description = fields.Text() 11 | partner_id = fields.Many2one('res.partner', string='Customer', required=True, default=lambda s: s.env.user.partner_id) 12 | 13 | @api.model 14 | def create(self, vals): 15 | ticket = super(DemoTicket, self).create(vals) 16 | (channel, message) = ((self._cr.dbname, 'demo.ticket', ticket.partner_id.id), ('new_ticket', ticket.id)) 17 | self.env['bus.bus'].sendone(channel, message) 18 | return ticket 19 | 20 | def unlink(self): 21 | notifications = [] 22 | for ticket in self: 23 | notifications.append(((self._cr.dbname, 'demo.ticket', ticket.partner_id.id), ('unlink_ticket', ticket.id))) 24 | self.env['bus.bus'].sendmany(notifications) 25 | return super(DemoTicket, self).unlink() 26 | -------------------------------------------------------------------------------- /ticket_viewer/views/ticket_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tickets 5 | ir.actions.act_window 6 | demo.ticket 7 | tree 8 | 9 | 10 | 11 | demo.ticket.tree 12 | demo.ticket 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Damien Bouvy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ticket_viewer/static/src/less/ticket_viewer.less: -------------------------------------------------------------------------------- 1 | .table-ticket { 2 | th { 3 | font-weight: 600; 4 | } 5 | } 6 | 7 | img.o_user_avatar { 8 | border-radius: 50%; 9 | max-width: 20px !important; 10 | } 11 | 12 | /* With any luck, this should no longer be needed after the v11 freeze 13 | as I am currently trying to have the notification center entirely 14 | untangled from the web client. */ 15 | .o_notification_manager { 16 | width: 300px; 17 | max-width: 100%; 18 | top: @navbar-height; 19 | right: 0; 20 | position: fixed; 21 | 22 | 23 | z-index: 1100; // Bootstrap modal z-index is 1050 24 | 25 | .o_notification { 26 | padding: 0; 27 | margin: 5px 0 0 0; 28 | 29 | opacity: 0; 30 | 31 | background-color: #FCFBEA; 32 | position: relative; 33 | .o_close { 34 | .o-position-absolute(5px, 5px); 35 | color: rgba(0, 0, 0, 0.3); 36 | text-decoration: none; 37 | } 38 | 39 | .o_notification_title { 40 | .o-flex-display(); 41 | .o-align-items(center); 42 | 43 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 44 | padding: 10px 10px 10px 20px; 45 | 46 | font-weight: bold; 47 | 48 | .o_icon { 49 | display: inline-block; 50 | margin-right: 20px; 51 | color: rgba(0, 0, 0, 0.3); 52 | } 53 | } 54 | 55 | .o_notification_content { 56 | padding: 10px; 57 | } 58 | 59 | &.o_error { 60 | color: white; 61 | background-color: #F16567; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /ticket_viewer/views/ticket_templates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ticket_viewer/static/lib/js/router.js: -------------------------------------------------------------------------------- 1 | /* Shamelessy taken form KRASIMIR TSONEV for the purpose of this demo 2 | http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url 3 | */ 4 | odoo.define('demo.router', function (require) { 5 | 'use strict'; 6 | 7 | var Router = { 8 | routes: [], 9 | mode: null, 10 | root: '/', 11 | config: function(options) { 12 | this.mode = options && options.mode && options.mode == 'history' 13 | && !!(history.pushState) ? 'history' : 'hash'; 14 | this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/'; 15 | return this; 16 | }, 17 | getFragment: function() { 18 | var fragment = ''; 19 | if(this.mode === 'history') { 20 | fragment = this.clearSlashes(decodeURI(location.pathname + location.search)); 21 | fragment = fragment.replace(/\?(.*)$/, ''); 22 | fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment; 23 | } else { 24 | var match = window.location.href.match(/#(.*)$/); 25 | fragment = match ? match[1] : ''; 26 | } 27 | return this.clearSlashes(fragment); 28 | }, 29 | clearSlashes: function(path) { 30 | return path.toString().replace(/\/$/, '').replace(/^\//, ''); 31 | }, 32 | add: function(re, handler) { 33 | if(typeof re == 'function') { 34 | handler = re; 35 | re = ''; 36 | } 37 | this.routes.push({ re: re, handler: handler}); 38 | return this; 39 | }, 40 | remove: function(param) { 41 | for(var i=0, r; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Ticket Viewer 3000 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Logout 19 | 20 | 21 | 22 | 23 | 24 | Submit Ticket 25 | 26 | 27 | 28 | 29 | 30 | 31 | Current Tickets 32 | 33 | 34 | 35 | Title 36 | Description 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | No tickets to see 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Title 61 | 62 | 63 | 64 | Description 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Hi 74 | Thank you for viewing this demo. 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | $(this).addClass('o_error'); 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /ticket_viewer/static/src/js/models.js: -------------------------------------------------------------------------------- 1 | odoo.define('demo.classes', function (require) { 2 | 'use strict'; 3 | 4 | var Class = require('web.Class'); 5 | var rpc = require('web.rpc'); 6 | 7 | 8 | /** 9 | * Ticket 10 | * Represent a demo.ticket object from the Odoo Backend 11 | * @type {OdooClass} 12 | */ 13 | var Ticket = Class.extend({ 14 | init: function (values) { 15 | Object.assign(this, values); 16 | }, 17 | /** 18 | * Fetch the latest fields for this particular ticket 19 | * on the backend server 20 | * @return {jQuery.Deferred} Resolves to the updated 21 | * Ticket if successful. 22 | */ 23 | update: function () { 24 | var self = this; 25 | return rpc.query({ 26 | model: 'demo.ticket', 27 | method: 'read', 28 | args: [[this.id]], 29 | kwargs: {fields: ['id', 'name', 'description']} 30 | }).then(function (ticket_values) { 31 | Object.assign(self, ticket_values[0]); 32 | return self; 33 | }); 34 | }, 35 | }); 36 | 37 | 38 | /** 39 | * User 40 | * Represent a res.users from the Odoo Backend, with only 41 | * the fields [id, login, name, image_small] accessible by 42 | * default. 43 | * The User class also represent a Ticket collection. 44 | * @type {OdooClass} 45 | */ 46 | var User = Class.extend({ 47 | init: function (values) { 48 | Object.assign(this, values); 49 | this.tickets = []; 50 | }, 51 | /** 52 | * Create a ticket on the server via rpc call and create it 53 | * client-side in the User tickets' collection on success. 54 | * @param {Object} values Object containing the 'name' 55 | * and 'description' content for 56 | * the new ticket 57 | * @return {jQuery.Deferred} The newly created ticket. 58 | */ 59 | createTicket: function (values) { 60 | var self = this; 61 | var ticket_values = { 62 | name: values.name, 63 | description: values.description 64 | }; 65 | return rpc.query({ 66 | model: 'demo.ticket', 67 | method: 'create', 68 | args: [ticket_values] 69 | }).then(function (ticket_id) { 70 | var ticket = new Ticket({id: ticket_id}); 71 | self.tickets.push(ticket); 72 | return ticket.update(); 73 | }); 74 | }, 75 | /** 76 | * Fetch the default fields for the user on the server. 77 | * @return {jQuery.Deferred} Resolves to the udpate User. 78 | */ 79 | fetchUserInfo: function () { 80 | var self = this; 81 | return rpc.query({ 82 | model: 'res.users', 83 | method: 'read', 84 | args: [[this.id]], 85 | kwargs: {fields: ['id', 'login', 'name', 'image_small', 'partner_id']} 86 | }).then(function (user_values) { 87 | var values = user_values[0]; 88 | values.partner_id = values.partner_id[0]; 89 | Object.assign(self, values); 90 | return self; 91 | }); 92 | }, 93 | /** 94 | * Fetch all available tickets for the current user. 95 | * Note that the actual search is done server side 96 | * using the model's ACLs and Access Rules. 97 | * @return {jQuery.Deferred} Resolves to the udpated User 98 | * (with its Tickets collection 99 | * populated). 100 | */ 101 | fetchAllTickets: function () { 102 | var self = this; 103 | return rpc.query({ 104 | model: 'demo.ticket', 105 | method: 'search_read', 106 | args: [[]], 107 | kwargs: {fields: ['id', 'name', 'description']} 108 | }).then(function (ticket_values) { 109 | for (var vals of ticket_values) { 110 | self.tickets.push(new Ticket(vals)); 111 | } 112 | return self; 113 | }); 114 | }, 115 | /** 116 | * Fetch a specified ticket id for the current user. 117 | * @param {Integer} id ID of the ticket to fetch. 118 | * @return {jQuery.Deferred} Resolves to the new Ticket 119 | */ 120 | fetchTicket: function (id) { 121 | var self = this; 122 | return rpc.query({ 123 | model: 'demo.ticket', 124 | method: 'search_read', 125 | args: [[['id', '=', id]]], 126 | kwargs: {fields: ['id', 'name', 'description']} 127 | }).then(function (ticket_values) { 128 | if (ticket_values.length) { 129 | var ticket = new Ticket(ticket_values[0]); 130 | self.tickets.push(ticket); 131 | } 132 | return ticket; 133 | }); 134 | }, 135 | /** 136 | * Remove a specified ticket id from the collections. 137 | * @param {Integer} id ID of the ticket to remove 138 | */ 139 | removeTicket: function (id) { 140 | var t_idx = this.tickets.findIndex(t => t.id === id); 141 | if (t_idx !== -1) { 142 | this.tickets.splice(t_idx, 1); 143 | } 144 | }, 145 | }); 146 | 147 | return { 148 | Ticket: Ticket, 149 | User: User, 150 | }; 151 | }); -------------------------------------------------------------------------------- /ticket_viewer/static/src/js/controllers.js: -------------------------------------------------------------------------------- 1 | odoo.define('demo.views', function (require) { 2 | 'use strict'; 3 | 4 | var bus = require('bus.bus').bus; 5 | var core = require('web.core'); 6 | var Dialog = require('web.Dialog'); 7 | var notification = require('web.notification'); 8 | var User = require('demo.classes').User; 9 | var Widget = require('web.Widget'); 10 | var Router = require('demo.router'); 11 | 12 | var qweb = core.qweb; 13 | var _t = core._t; 14 | 15 | require('web.dom_ready'); 16 | 17 | var TicketApp = Widget.extend({ 18 | template: 'ticket_viewer.app', 19 | events: { 20 | 'click .ticket_about': function (ev) {ev.preventDefault(); Router.navigate('/about');}, 21 | 'click button.o_new_ticket': function () {Router.navigate('/new');}, 22 | }, 23 | custom_events: { 24 | 'ticket-submit': '_onTicketSubmit', 25 | 'warning': function (ev) {this.notification_manager.warn(ev.data.msg);}, 26 | 'notify': function (ev) {this.notification_manager.notify(ev.data.msg);}, 27 | }, 28 | xmlDependencies: ['/ticket_viewer/static/src/xml/ticket_views.xml'], 29 | /* Lifecycle */ 30 | init: function (parent, options) { 31 | this._super.apply(this, arguments); 32 | this.user = new User({id: odoo.session_info.user_id}); 33 | var self = this; 34 | Router.config({ mode: 'history', root:'/tickets'}); 35 | 36 | // adding routes 37 | Router 38 | .add(/new/, function () { 39 | self._onNewTicket(); 40 | }).add(/about/, function () { 41 | self._about(); 42 | }) 43 | .listen(); 44 | }, 45 | willStart: function () { 46 | return $.when(this._super.apply(this, arguments), 47 | this.user.fetchUserInfo(), 48 | this.user.fetchAllTickets() 49 | ).then(function (dummy, user) { 50 | bus.update_option('demo.ticket', user.partner_id); 51 | }); 52 | }, 53 | start: function () { 54 | var self = this; 55 | return this._super.apply(this, arguments).then(function () { 56 | self.list = new TicketList(self, self.user.tickets); 57 | self.list.appendTo($('.o_ticket_list')); 58 | self.notification_manager = new notification.NotificationManager(self); 59 | self.notification_manager.appendTo(self.$el); 60 | bus.on('notification', self, self._onNotification); 61 | Router.check(); 62 | }); 63 | }, 64 | _about: function () { 65 | new Dialog(this, { 66 | title: _t('About'), 67 | $content: qweb.render('ticket_viewer.about'), 68 | buttons: [{ 69 | text: _t('Awesome!'), 70 | click: function () { 71 | Router.navigate(); 72 | }, 73 | close: true, 74 | }], 75 | }).open(); 76 | }, 77 | /** 78 | * Open a new modal to encode a new ticket. 79 | * @param {jQuery.Event} ev 80 | */ 81 | _onNewTicket: function (ev) { 82 | new TicketDialog(this, { 83 | title: _t('New Ticket'), 84 | $content: qweb.render('ticket_viewer.ticket_form'), 85 | buttons: [{ 86 | text: _t('Submit Ticket'), 87 | click: function () { 88 | this._onFormSubmit(); 89 | }, 90 | }], 91 | }).open(); 92 | }, 93 | /** 94 | * Send the submitted ticket data to the model for saving. 95 | * @param {OdooEvent} ev Odoo Event containing the form data 96 | */ 97 | _onTicketSubmit: function (ev) { 98 | var self = this; 99 | var data = ev.data; 100 | this.user.createTicket(data).then(function (new_ticket) { 101 | self.list.insertTicket(new_ticket); 102 | Router.navigate(''); 103 | }); 104 | }, 105 | /** 106 | * Handle bus notification. 107 | * 108 | * Currently, 2 notification types are handled in this page: 109 | * - new_ticket: a ticket has been added for the current user 110 | * - unlink_ticket: a ticket has been deleted for the current user 111 | * 112 | * @param {Array} notifications Array of notification arriving through the bus. 113 | */ 114 | _onNotification: function (notifications) { 115 | var self = this; 116 | for (var notif of notifications) { 117 | var channel = notif[0], message = notif[1]; 118 | if (channel[1] !== 'demo.ticket' || channel[2] !== this.user.partner_id) { 119 | return; 120 | } 121 | if (message[0] === 'new_ticket') { 122 | var ticket_id = message[1]; 123 | if (!this.user.tickets.find(t => t.id === ticket_id)) { 124 | this.user.fetchTicket(ticket_id).then(function (new_ticket) { 125 | self.list.insertTicket(new_ticket); 126 | self.trigger_up('notify', {msg: (_t('New Ticket ') + new_ticket.name)}); 127 | }); 128 | } 129 | } else if (message[0] === 'unlink_ticket') { 130 | this.user.removeTicket(message[1]); 131 | this.list.removeTicket(message[1]); 132 | } 133 | } 134 | }, 135 | }); 136 | 137 | var TicketList = Widget.extend({ 138 | template: 'ticket_viewer.ticket_list', 139 | /* Lifecycle */ 140 | init: function (parent, tickets) { 141 | this._super.apply(this, arguments); 142 | this.tickets = tickets; 143 | }, 144 | /** 145 | * Insert a new ticket instance in the list. If the list is hidden 146 | * (because there was no ticket prior to the insertion), call for 147 | * a complete rerendering instead. 148 | * @param {OdooClass.Ticket} ticket Ticket to insert in the list 149 | */ 150 | insertTicket: function (ticket) { 151 | if (!this.$('tbody').length) { 152 | this._rerender(); 153 | return; 154 | } 155 | var ticket_node = qweb.render('ticket_viewer.ticket_list.ticket', {ticket: ticket}); 156 | this.$('tbody').prepend(ticket_node); 157 | }, 158 | /** 159 | * Remove a ticket from the list. If this is the last ticket to be 160 | * removed, rerender the widget completely to reflect the 'empty list' 161 | * state. 162 | * @param {Integer} id ID of the ticket to remove. 163 | */ 164 | removeTicket: function (id) { 165 | this.$('tr[data-id=' + id + ']').remove(); 166 | if (!this.$('tr[data-id]').length) { 167 | this._rerender(); 168 | } 169 | }, 170 | 171 | /** 172 | * Rerender the whole widget; will be useful when we switch from 173 | * an empty list of tickets to one or more ticket (or vice-versa) 174 | * by using the bus. 175 | */ 176 | _rerender: function () { 177 | this.replaceElement(qweb.render('ticket_viewer.ticket_list', {widget: this})); 178 | }, 179 | }); 180 | 181 | var TicketDialog = Dialog.extend({ 182 | events: { 183 | 'submit form': '_onFormSubmit', 184 | 'click .btn-primary': '_onFormSubmit', 185 | }, 186 | _onFormSubmit: function (ev) { 187 | if (ev) { 188 | ev.preventDefault(); 189 | } 190 | var form = this.$('form')[0]; 191 | var formdata = new FormData(form); 192 | var data = {}; 193 | for (var field of formdata) { 194 | data[field[0]] = field[1]; 195 | } 196 | if (!data.name || !data.description) { 197 | this.trigger_up('warning', {msg: _t('All fields are mandatory.')}); 198 | return; 199 | } 200 | this.trigger_up('ticket-submit', data); 201 | form.reset(); 202 | this.close(); 203 | }, 204 | }); 205 | 206 | 207 | var $elem = $('.o_ticket_app'); 208 | var app = new TicketApp(null); 209 | app.appendTo($elem).then(function () { 210 | bus.start_polling(); 211 | }); 212 | }); 213 | --------------------------------------------------------------------------------
No tickets to see
Thank you for viewing this demo.