Add basic "Buefy" support for grids (master index view)
still pretty experimental at this point, but making progress
This commit is contained in:
		
							parent
							
								
									3cef591719
								
							
						
					
					
						commit
						8d6ecc3ec7
					
				
					 9 changed files with 296 additions and 15 deletions
				
			
		|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2018 Lance Edgar | #  Copyright © 2010-2019 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -176,6 +176,12 @@ class Grid(object): | ||||||
|         if key in self.filters: |         if key in self.filters: | ||||||
|             self.filters[key].label = label |             self.filters[key].label = label | ||||||
| 
 | 
 | ||||||
|  |     def get_label(self, key): | ||||||
|  |         """ | ||||||
|  |         Returns the label text for given field key. | ||||||
|  |         """ | ||||||
|  |         return self.labels.get(key, prettify(key)) | ||||||
|  | 
 | ||||||
|     def set_link(self, key, link=True): |     def set_link(self, key, link=True): | ||||||
|         if link: |         if link: | ||||||
|             if key not in self.linked_columns: |             if key not in self.linked_columns: | ||||||
|  | @ -900,9 +906,16 @@ class Grid(object): | ||||||
|         """ |         """ | ||||||
|         context = kwargs |         context = kwargs | ||||||
|         context['grid'] = self |         context['grid'] = self | ||||||
|  |         context['request'] = self.request | ||||||
|         context.setdefault('allow_save_defaults', True) |         context.setdefault('allow_save_defaults', True) | ||||||
|         return render(template, context) |         return render(template, context) | ||||||
| 
 | 
 | ||||||
|  |     def render_buefy(self, template='/grids/buefy.mako', **kwargs): | ||||||
|  |         """ | ||||||
|  |         Render the Buefy grid, including filters. | ||||||
|  |         """ | ||||||
|  |         return self.render_complete(template=template, **kwargs) | ||||||
|  | 
 | ||||||
|     def render_filters(self, template='/grids/filters.mako', **kwargs): |     def render_filters(self, template='/grids/filters.mako', **kwargs): | ||||||
|         """ |         """ | ||||||
|         Render the filters to a Unicode string, using the specified template. |         Render the filters to a Unicode string, using the specified template. | ||||||
|  | @ -982,6 +995,96 @@ class Grid(object): | ||||||
|         # TODO: Make configurable or something... |         # TODO: Make configurable or something... | ||||||
|         return [5, 10, 20, 50, 100, 200] |         return [5, 10, 20, 50, 100, 200] | ||||||
| 
 | 
 | ||||||
|  |     def has_static_data(self): | ||||||
|  |         """ | ||||||
|  |         Should return ``True`` if the grid data can be considered "static" | ||||||
|  |         (i.e. a list of values).  Will return ``False`` otherwise, e.g. if the | ||||||
|  |         data is represented as a SQLAlchemy query. | ||||||
|  |         """ | ||||||
|  |         # TODO: should make this smarter? | ||||||
|  |         if isinstance(self.data, list): | ||||||
|  |             return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def get_buefy_columns(self): | ||||||
|  |         """ | ||||||
|  |         Return a list of dicts representing all grid columns.  Meant for use | ||||||
|  |         with Buefy table. | ||||||
|  |         """ | ||||||
|  |         columns = [] | ||||||
|  |         for name in self.columns: | ||||||
|  |             columns.append({ | ||||||
|  |                 'field': name, | ||||||
|  |                 'label': self.get_label(name), | ||||||
|  |                 'sortable': self.sortable and name in self.sorters, | ||||||
|  |             }) | ||||||
|  |         return columns | ||||||
|  | 
 | ||||||
|  |     def get_buefy_data(self): | ||||||
|  |         """ | ||||||
|  |         Returns a list of data rows for the grid, for use with Buefy table. | ||||||
|  |         """ | ||||||
|  |         # filter / sort / paginate to get "visible" data | ||||||
|  |         raw_data = self.make_visible_data() | ||||||
|  |         data = [] | ||||||
|  | 
 | ||||||
|  |         # iterate over data rows | ||||||
|  |         for i in range(len(raw_data)): | ||||||
|  |             rowobj = raw_data[i] | ||||||
|  |             row = {} | ||||||
|  | 
 | ||||||
|  |             # iterate over data fields | ||||||
|  |             for name in self.columns: | ||||||
|  | 
 | ||||||
|  |                 # leverage configured rendering logic where applicable; | ||||||
|  |                 # otherwise use "raw" data value as string | ||||||
|  |                 if self.renderers and name in self.renderers: | ||||||
|  |                     row[name] = self.renderers[name](rowobj, name) | ||||||
|  |                 else: | ||||||
|  |                     value = self.obtain_value(rowobj, name) | ||||||
|  |                     if value is None: | ||||||
|  |                         value = "" | ||||||
|  |                     row[name] = six.text_type(value) | ||||||
|  | 
 | ||||||
|  |             # set action URL(s) for row, as needed | ||||||
|  |             self.set_action_urls(row, rowobj, i) | ||||||
|  | 
 | ||||||
|  |             data.append(row) | ||||||
|  | 
 | ||||||
|  |         results = { | ||||||
|  |             'data': data, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if self.pageable and self.pager is not None: | ||||||
|  |             results['total_items'] = self.pager.item_count | ||||||
|  |             results['per_page'] = self.pager.items_per_page | ||||||
|  |             results['page'] = self.pager.page | ||||||
|  |             results['pages'] = self.pager.page_count | ||||||
|  |             results['first_item'] = self.pager.first_item | ||||||
|  |             results['last_item'] = self.pager.last_item | ||||||
|  | 
 | ||||||
|  |         return results | ||||||
|  | 
 | ||||||
|  |     def set_action_urls(self, row, rowobj, i): | ||||||
|  |         """ | ||||||
|  |         Pre-generate all action URLs for the given data row.  Meant for use | ||||||
|  |         with Buefy table, since we can't generate URLs from JS. | ||||||
|  |         """ | ||||||
|  |         for action in (self.main_actions + self.more_actions): | ||||||
|  |             url = action.get_url(rowobj, i) | ||||||
|  |             row['_action_url_{}'.format(action.key)] = url | ||||||
|  | 
 | ||||||
|  |     def is_linked(self, name): | ||||||
|  |         """ | ||||||
|  |         Should return ``True`` if the given column name is configured to be | ||||||
|  |         "linked" (i.e. table cell should contain a link to "view object"), | ||||||
|  |         otherwise ``False``. | ||||||
|  |         """ | ||||||
|  |         if self.linked_columns: | ||||||
|  |             if name in self.linked_columns: | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class CustomWebhelpersGrid(webhelpers2_grid.Grid): | class CustomWebhelpersGrid(webhelpers2_grid.Grid): | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
							
								
								
									
										142
									
								
								tailbone/templates/grids/buefy.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								tailbone/templates/grids/buefy.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,142 @@ | ||||||
|  | ## -*- coding: utf-8; -*- | ||||||
|  | 
 | ||||||
|  | <div id="buefy-table-app"> | ||||||
|  |   <b-table | ||||||
|  |      :data="data" | ||||||
|  |      :columns="columns" | ||||||
|  |      :loading="loading" | ||||||
|  | 
 | ||||||
|  |      :default-sort="[sortField, sortOrder]" | ||||||
|  |      backend-sorting | ||||||
|  |      @sort="onSort" | ||||||
|  | 
 | ||||||
|  |      % if grid.pageable: | ||||||
|  |      paginated | ||||||
|  |      :per-page="perPage" | ||||||
|  |      :current-page="page" | ||||||
|  |      backend-pagination | ||||||
|  |      :total="total" | ||||||
|  |      @page-change="onPageChange" | ||||||
|  |      % endif | ||||||
|  | 
 | ||||||
|  |      ## TODO: should let grid (or master view) decide how to set these? | ||||||
|  |      icon-pack="fas" | ||||||
|  |      :striped="true" | ||||||
|  |      :hoverable="true" | ||||||
|  |      :narrowed="true"> | ||||||
|  | 
 | ||||||
|  |     <template slot-scope="props"> | ||||||
|  |       % for column in grid_columns: | ||||||
|  |           <b-table-column field="${column['field']}" label="${column['label']}" ${'sortable' if column['sortable'] else ''}> | ||||||
|  |             % if grid.is_linked(column['field']): | ||||||
|  |                 <a :href="props.row._action_url_view" v-html="props.row.${column['field']}"></a> | ||||||
|  |             % else: | ||||||
|  |                 {{ props.row.${column['field']} }} | ||||||
|  |             % endif | ||||||
|  |           </b-table-column> | ||||||
|  |       % endfor | ||||||
|  | 
 | ||||||
|  |       % if grid.main_actions or grid.more_actions: | ||||||
|  |           <b-table-column field="actions" label="Actions"> | ||||||
|  |             % for action in grid.main_actions: | ||||||
|  |                 <a :href="props.row._action_url_${action.key}"><i class="fas fa-${action.icon}"></i> | ||||||
|  |                   ${action.label} | ||||||
|  |                 </a> | ||||||
|  |             % endfor | ||||||
|  |           </b-table-column> | ||||||
|  |       % endif | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <template slot="empty"> | ||||||
|  |       <section class="section"> | ||||||
|  |         <div class="content has-text-grey has-text-centered"> | ||||||
|  |           <p> | ||||||
|  |             <b-icon | ||||||
|  |                pack="fas" | ||||||
|  |                icon="fas fa-sad-tear" | ||||||
|  |                size="is-large"> | ||||||
|  |             </b-icon> | ||||||
|  |           </p> | ||||||
|  |           <p>Nothing here.</p> | ||||||
|  |         </div> | ||||||
|  |       </section> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     % if grid.pageable and grid.pager: | ||||||
|  |     <template slot="footer"> | ||||||
|  |       <div class="has-text-right">showing {{ firstItem }} - {{ lastItem }} of {{ total }} results</div> | ||||||
|  |     </template> | ||||||
|  |     % endif | ||||||
|  | 
 | ||||||
|  |   </b-table> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <script type="text/javascript"> | ||||||
|  | 
 | ||||||
|  |   new Vue({ | ||||||
|  |       el: '#buefy-table-app', | ||||||
|  |       data() { | ||||||
|  |           return { | ||||||
|  |               data: ${json.dumps(grid_data['data'])|n}, | ||||||
|  |               loading: false, | ||||||
|  |               sortField: '${grid.sortkey}', | ||||||
|  |               sortOrder: '${grid.sortdir}', | ||||||
|  |               % if grid.pageable: | ||||||
|  |               % if static_data: | ||||||
|  |               total: ${len(grid_data['data'])}, | ||||||
|  |               % else: | ||||||
|  |               total: ${grid_data['total_items']}, | ||||||
|  |               % endif | ||||||
|  |               perPage: ${grid.pagesize}, | ||||||
|  |               page: ${grid.page}, | ||||||
|  |               firstItem: ${grid_data['first_item']}, | ||||||
|  |               lastItem: ${grid_data['last_item']}, | ||||||
|  |               % endif | ||||||
|  |           } | ||||||
|  |       }, | ||||||
|  |       methods: { | ||||||
|  | 
 | ||||||
|  |           loadAsyncData() { | ||||||
|  | 
 | ||||||
|  |               const params = [ | ||||||
|  |                   'partial=true', | ||||||
|  |                   `sortkey=${'$'}{this.sortField}`, | ||||||
|  |                   `sortdir=${'$'}{this.sortOrder}`, | ||||||
|  |                   `pagesize=${'$'}{this.perPage}`, | ||||||
|  |                   `page=${'$'}{this.page}` | ||||||
|  |               ].join('&') | ||||||
|  | 
 | ||||||
|  |               this.loading = true | ||||||
|  |               this.$http.get(`${request.current_route_url(_query=None)}?${'$'}{params}`).then(({ data }) => { | ||||||
|  |                   this.data = data.data | ||||||
|  |                   this.total = data.total_items | ||||||
|  |                   this.firstItem = data.first_item | ||||||
|  |                   this.lastItem = data.last_item | ||||||
|  |                   this.loading = false | ||||||
|  |               }) | ||||||
|  |               .catch((error) => { | ||||||
|  |                   this.data = [] | ||||||
|  |                   this.total = 0 | ||||||
|  |                   this.loading = false | ||||||
|  |                   throw error | ||||||
|  |               }) | ||||||
|  |           }, | ||||||
|  | 
 | ||||||
|  |           onPageChange(page) { | ||||||
|  |               this.page = page | ||||||
|  |               this.loadAsyncData() | ||||||
|  |           }, | ||||||
|  | 
 | ||||||
|  |           onSort(field, order) { | ||||||
|  |               this.sortField = field | ||||||
|  |               this.sortOrder = order | ||||||
|  |               // always reset to first page when changing sort options | ||||||
|  |               // TODO: i mean..right? would we ever not want that? | ||||||
|  |               this.page = 1 | ||||||
|  |               this.loadAsyncData() | ||||||
|  |           } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | @ -17,7 +17,9 @@ | ||||||
|   <script type="text/javascript"> |   <script type="text/javascript"> | ||||||
|     $(function() { |     $(function() { | ||||||
| 
 | 
 | ||||||
|  |         % if not use_buefy: | ||||||
|         $('.grid-wrapper').gridwrapper(); |         $('.grid-wrapper').gridwrapper(); | ||||||
|  |         % endif | ||||||
| 
 | 
 | ||||||
|         % if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)): |         % if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)): | ||||||
| 
 | 
 | ||||||
|  | @ -170,4 +172,12 @@ | ||||||
| 
 | 
 | ||||||
| </%def> | </%def> | ||||||
| 
 | 
 | ||||||
| ${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n} | 
 | ||||||
|  | % if use_buefy: | ||||||
|  |     ${grid.render_buefy(grid_columns=grid_columns, grid_data=grid_data, static_data=static_data)|n} | ||||||
|  | 
 | ||||||
|  | % else: | ||||||
|  |     ## no buefy, so do the traditional thing | ||||||
|  |     ${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n} | ||||||
|  | 
 | ||||||
|  | % endif | ||||||
|  |  | ||||||
|  | @ -256,6 +256,10 @@ | ||||||
|   ## Vue.js |   ## Vue.js | ||||||
|   ${h.javascript_link('https://unpkg.com/vue')} |   ${h.javascript_link('https://unpkg.com/vue')} | ||||||
| 
 | 
 | ||||||
|  |   ## vue-resource | ||||||
|  |   ## (needed for e.g. this.$http.get() calls, used by grid at least) | ||||||
|  |   ${h.javascript_link('https://cdn.jsdelivr.net/npm/vue-resource@1.5.1')} | ||||||
|  | 
 | ||||||
|   ## Buefy 0.7.3 |   ## Buefy 0.7.3 | ||||||
|   ${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')} |   ${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2018 Lance Edgar | #  Copyright © 2010-2019 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -44,10 +44,10 @@ class DataSyncChangesView(MasterView): | ||||||
|     model_class = model.DataSyncChange |     model_class = model.DataSyncChange | ||||||
|     url_prefix = '/datasync/changes' |     url_prefix = '/datasync/changes' | ||||||
|     permission_prefix = 'datasync' |     permission_prefix = 'datasync' | ||||||
| 
 |  | ||||||
|     creatable = False |     creatable = False | ||||||
|     editable = False |     editable = False | ||||||
|     bulk_deletable = True |     bulk_deletable = True | ||||||
|  |     use_buefy = True | ||||||
| 
 | 
 | ||||||
|     grid_columns = [ |     grid_columns = [ | ||||||
|         'source', |         'source', | ||||||
|  |  | ||||||
|  | @ -52,6 +52,7 @@ class ProfilesView(MasterView): | ||||||
|     pageable = False |     pageable = False | ||||||
|     creatable = False |     creatable = False | ||||||
|     deletable = False |     deletable = False | ||||||
|  |     use_buefy = True | ||||||
| 
 | 
 | ||||||
|     grid_columns = [ |     grid_columns = [ | ||||||
|         'key', |         'key', | ||||||
|  |  | ||||||
|  | @ -122,6 +122,8 @@ class MasterView(View): | ||||||
| 
 | 
 | ||||||
|     grid_index = None |     grid_index = None | ||||||
|     use_index_links = False |     use_index_links = False | ||||||
|  |     # this should be turned on per-view as progress is made | ||||||
|  |     use_buefy = False | ||||||
| 
 | 
 | ||||||
|     has_versions = False |     has_versions = False | ||||||
|     help_url = None |     help_url = None | ||||||
|  | @ -265,6 +267,7 @@ class MasterView(View): | ||||||
|         """ |         """ | ||||||
|         self.listing = True |         self.listing = True | ||||||
|         grid = self.make_grid() |         grid = self.make_grid() | ||||||
|  |         use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') | ||||||
| 
 | 
 | ||||||
|         # If user just refreshed the page with a reset instruction, issue a |         # If user just refreshed the page with a reset instruction, issue a | ||||||
|         # redirect in order to clear out the query string. |         # redirect in order to clear out the query string. | ||||||
|  | @ -275,16 +278,28 @@ class MasterView(View): | ||||||
|         if grid.pageable and hasattr(grid, 'pager'): |         if grid.pageable and hasattr(grid, 'pager'): | ||||||
|             self.first_visible_grid_index = grid.pager.first_item |             self.first_visible_grid_index = grid.pager.first_item | ||||||
| 
 | 
 | ||||||
|         # Return grid only, if partial page was requested. |         # return grid only, if partial page was requested | ||||||
|         if self.request.params.get('partial'): |         if self.request.params.get('partial'): | ||||||
|             if six.PY3: |             if use_buefy: | ||||||
|                 self.request.response.content_type = 'text/html' |                 # render grid data only, as JSON | ||||||
|             else: |                 return render_to_response('json', grid.get_buefy_data(), | ||||||
|                 self.request.response.content_type = b'text/html' |                                           request=self.request) | ||||||
|             self.request.response.text = grid.render_grid() |             else: # just do traditional thing, render grid HTML | ||||||
|             return self.request.response |                 self.request.response.content_type = str('text/html') | ||||||
|  |                 self.request.response.text = grid.render_grid() | ||||||
|  |                 return self.request.response | ||||||
| 
 | 
 | ||||||
|         return self.render_to_response('index', {'grid': grid}) |         context = { | ||||||
|  |             'grid': grid, | ||||||
|  |             'use_buefy': use_buefy, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if use_buefy: | ||||||
|  |             context['grid_columns'] = grid.get_buefy_columns() | ||||||
|  |             context['grid_data'] = grid.get_buefy_data() | ||||||
|  |             context['static_data'] = grid.has_static_data() | ||||||
|  | 
 | ||||||
|  |         return self.render_to_response('index', context) | ||||||
| 
 | 
 | ||||||
|     def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): |     def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): | ||||||
|         """ |         """ | ||||||
|  | @ -2246,9 +2261,11 @@ class MasterView(View): | ||||||
|         """ |         """ | ||||||
|         actions = [] |         actions = [] | ||||||
|         prefix = self.get_permission_prefix() |         prefix = self.get_permission_prefix() | ||||||
|  |         use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') | ||||||
|         if self.viewable and self.request.has_perm('{}.view'.format(prefix)): |         if self.viewable and self.request.has_perm('{}.view'.format(prefix)): | ||||||
|             url = self.get_view_index_url if self.use_index_links else None |             url = self.get_view_index_url if self.use_index_links else None | ||||||
|             actions.append(self.make_action('view', icon='zoomin', url=url)) |             icon = 'eye' if use_buefy else 'zoomin' | ||||||
|  |             actions.append(self.make_action('view', icon=icon, url=url)) | ||||||
|         return actions |         return actions | ||||||
| 
 | 
 | ||||||
|     def get_view_index_url(self, row, i): |     def get_view_index_url(self, row, i): | ||||||
|  | @ -2261,8 +2278,10 @@ class MasterView(View): | ||||||
|         """ |         """ | ||||||
|         actions = [] |         actions = [] | ||||||
|         prefix = self.get_permission_prefix() |         prefix = self.get_permission_prefix() | ||||||
|  |         use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') | ||||||
|         if self.editable and self.request.has_perm('{}.edit'.format(prefix)): |         if self.editable and self.request.has_perm('{}.edit'.format(prefix)): | ||||||
|             actions.append(self.make_action('edit', icon='pencil', url=self.default_edit_url)) |             icon = 'edit' if use_buefy else 'pencil' | ||||||
|  |             actions.append(self.make_action('edit', icon=icon, url=self.default_edit_url)) | ||||||
|         if self.deletable and self.request.has_perm('{}.delete'.format(prefix)): |         if self.deletable and self.request.has_perm('{}.delete'.format(prefix)): | ||||||
|             actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url)) |             actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url)) | ||||||
|         return actions |         return actions | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2018 Lance Edgar | #  Copyright © 2010-2019 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -42,6 +42,7 @@ class TablesView(MasterView): | ||||||
|     viewable = False |     viewable = False | ||||||
|     filterable = False |     filterable = False | ||||||
|     pageable = False |     pageable = False | ||||||
|  |     use_buefy = True | ||||||
| 
 | 
 | ||||||
|     grid_columns = [ |     grid_columns = [ | ||||||
|         'name', |         'name', | ||||||
|  |  | ||||||
|  | @ -68,6 +68,7 @@ class UpgradeView(MasterView): | ||||||
|     executable = True |     executable = True | ||||||
|     execute_progress_template = '/upgrade.mako' |     execute_progress_template = '/upgrade.mako' | ||||||
|     execute_progress_initial_msg = "Upgrading" |     execute_progress_initial_msg = "Upgrading" | ||||||
|  |     use_buefy = True | ||||||
| 
 | 
 | ||||||
|     labels = { |     labels = { | ||||||
|         'executed_by': "Executed by", |         'executed_by': "Executed by", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar