From ad4f52d9128ddf8e1b17b28382456aa72991e82c Mon Sep 17 00:00:00 2001 From: ajugo Date: Wed, 27 Jul 2022 10:10:32 +0200 Subject: [PATCH] Add Label template list screen - view mode [SCI-7018] (#4292) * Initial label template datatable [SCI-7018] * Add new columns to LabelTemplate and update datatable view [SCI-7018] * Fix after rebase [SCI-7018] * Fix migration, disable checkboxes in view mode and fix label template to team level [SCI-7018] --- .../images/label_template_icons/zpl.svg | 4 + .../label_templates_datatable.js | 107 ++++++++++++++++++ .../stylesheets/label_template_index.scss | 54 +++++++++ app/controllers/label_templates_controller.rb | 23 ++++ app/datatables/label_template_datatable.rb | 76 +++++++++++++ app/models/label_template.rb | 5 +- app/permissions/team.rb | 4 + app/views/label_templates/index.html.erb | 62 +++++++++- config/initializers/assets.rb | 1 + config/locales/en.yml | 13 +++ config/routes.rb | 5 +- ...26133419_add_columns_to_label_templates.rb | 42 +++++++ db/seeds.rb | 8 ++ 13 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/label_template_icons/zpl.svg create mode 100644 app/assets/javascripts/label_templates/label_templates_datatable.js create mode 100644 app/assets/stylesheets/label_template_index.scss create mode 100644 app/datatables/label_template_datatable.rb create mode 100644 db/migrate/20220726133419_add_columns_to_label_templates.rb diff --git a/app/assets/images/label_template_icons/zpl.svg b/app/assets/images/label_template_icons/zpl.svg new file mode 100644 index 000000000..36ab34a26 --- /dev/null +++ b/app/assets/images/label_template_icons/zpl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/javascripts/label_templates/label_templates_datatable.js b/app/assets/javascripts/label_templates/label_templates_datatable.js new file mode 100644 index 000000000..88bf48010 --- /dev/null +++ b/app/assets/javascripts/label_templates/label_templates_datatable.js @@ -0,0 +1,107 @@ +/* global I18n DataTableHelpers */ + +(function() { + 'use strict'; + + var LABEL_TEMPLATE_TABLE; + + function renderCheckboxHTML(data, type, row) { + return `
+ + +
`; + } + + function renderDefaultTemplateHTML(data) { + return data ? '' : ''; + } + + function renderNameHTML(data, type, row) { + return `${data.icon_url}${data.name}`; + } + + function addAttributesToRow(row, data) { + $(row).addClass('label-template-row') + .attr('data-id', data['0']); + } + + function checkboxToggleCallback() { + $("[data-action='toggle']").change(function() { + if ($(this).is(':checked')) { + $(this).closest('.label-template-row').addClass('selected'); + } else { + $(this).closest('.label-template-row').removeClass('selected'); + } + }); + } + + function initToggleAllCheckboxes() { + $('input[name="select_all"]').change(function() { + if ($(this).is(':checked')) { + $("[data-action='toggle']").prop('checked', true); + $('.label-template-row').addClass('selected'); + } else { + $("[data-action='toggle']").prop('checked', false); + $('.label-template-row').removeClass('selected'); + } + }); + } + + function tableDrowCallback() { + checkboxToggleCallback(); + initToggleAllCheckboxes(); + } + + // INIT + + function initDatatable() { + var $table = $('#label-templates-table'); + LABEL_TEMPLATE_TABLE = $table.DataTable({ + dom: "Rt<'pagination-row hidden'<'pagination-info'li><'pagination-actions'p>>", + order: [[2, 'desc']], + sScrollX: '100%', + sScrollXInner: '100%', + processing: true, + serverSide: true, + ajax: $table.data('source'), + pagingType: 'simple_numbers', + colReorder: { + fixedColumnsLeft: 1000000 // Disable reordering + }, + columnDefs: [{ + targets: 0, + searchable: false, + orderable: false, + className: 'dt-body-center', + sWidth: '1%', + render: renderCheckboxHTML + }, { + targets: 1, + searchable: false, + orderable: false, + sWidth: '1%', + render: renderDefaultTemplateHTML + }, { + targets: 2, + className: 'label-template-name', + render: renderNameHTML + }], + oLanguage: { + sSearch: I18n.t('general.filter') + }, + fnDrawCallback: tableDrowCallback, + createdRow: addAttributesToRow, + fnInitComplete: function() { + DataTableHelpers.initLengthAppearance($table.closest('.dataTables_wrapper')); + $('.pagination-row').removeClass('hidden'); + } + }); + } + + $('.label-templates-index').on('keyup', '.label-templates-search', function() { + LABEL_TEMPLATE_TABLE.search($(this).val()).draw(); + }); + + initDatatable(); +}()); diff --git a/app/assets/stylesheets/label_template_index.scss b/app/assets/stylesheets/label_template_index.scss new file mode 100644 index 000000000..fbcb492a3 --- /dev/null +++ b/app/assets/stylesheets/label_template_index.scss @@ -0,0 +1,54 @@ +// scss-lint:disable SelectorDepth NestingDepth IdSelector SelectorFormat + +.label-templates-index { + .search-label-templates-container { + float: right; + margin-right: 2em; + padding-bottom: 16px; + width: 200px; + + .fa-search { + padding-bottom: 16px; + } + } + + .toolbar-row.label-templates-toolbar { + border-bottom: 0; + } +} + +.label-templates-datatable { + height: calc(100vh - var(--navbar-height) - var(--content-header-size)); + + #label-templates-table_wrapper { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + + .dataTables_scroll { + display: flex; + flex-direction: column; + flex-grow: 1; + height: calc(100% - var(--datatable-pagination-row)); + + .dataTables_scrollHead { + flex-shrink: 0; + } + } + + .pagination-row { + flex-shrink: 0; + } + } +} + +.label-template-name { + align-items: center; + display: flex; +} + +.label-template-icon { + padding-bottom: 2px; + padding-right: 4px; +} diff --git a/app/controllers/label_templates_controller.rb b/app/controllers/label_templates_controller.rb index 1439528bf..f36f2fb56 100644 --- a/app/controllers/label_templates_controller.rb +++ b/app/controllers/label_templates_controller.rb @@ -1,15 +1,38 @@ # frozen_string_literal: true class LabelTemplatesController < ApplicationController + include InputSanitizeHelper + before_action :check_view_permissions + before_action :load_label_templates, only: %i(index datatable) layout 'fluid' def index; end + def datatable + respond_to do |format| + format.json do + render json: ::LabelTemplateDatatable.new( + view_context, + can_manage_label_templates?(current_team), + @label_templates + ) + end + end + end + + def new + render_404 + end + private def check_view_permissions render_403 unless can_view_label_templates?(current_team) end + + def load_label_templates + @label_templates = LabelTemplate.where(team_id: current_team.id) + end end diff --git a/app/datatables/label_template_datatable.rb b/app/datatables/label_template_datatable.rb new file mode 100644 index 000000000..5beb9ecf8 --- /dev/null +++ b/app/datatables/label_template_datatable.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class LabelTemplateDatatable < CustomDatatable + include InputSanitizeHelper + include Rails.application.routes.url_helpers + + TABLE_COLUMNS = %w( + label_templates.name + label_templates.format + label_templates.description + label_templates.modified_by + label_templates.updated_at + label_templates.created_by_user + label_templates.created_at + ).freeze + + def initialize(view, can_manage_templates, label_templates) + super(view) + @manage_template = can_manage_templates + @label_templates = label_templates + end + + def sortable_columns + @sortable_columns ||= TABLE_COLUMNS + end + + def searchable_columns + @searchable_columns ||= TABLE_COLUMNS + end + + private + + def data + records.map do |record| + { + '0' => record.id, + '1' => record.default, + '2' => append_format_icon(sanitize_input(record.name)), + '3' => sanitize_input(record.format), + '4' => sanitize_input(record.description), + '5' => sanitize_input(record.modified_by), + '6' => I18n.l(record.updated_at, format: :full), + '7' => sanitize_input(record.created_by_user), + '8' => I18n.l(record.created_at, format: :full), + 'recordInfoUrl' => '', + 'manage_permission' => @manage_template + } + end + end + + def append_format_icon(data) + { icon_url: ActionController::Base.helpers.image_tag('label_template_icons/zpl.svg', class: 'label-template-icon'), + name: data } + end + + def get_raw_records + res = @label_templates.joins( + 'LEFT OUTER JOIN users AS creators ' \ + 'ON label_templates.created_by_id = creators.id' + ).joins( + 'LEFT OUTER JOIN users AS modifiers '\ + 'ON label_templates.last_modified_by_id = modifiers.id' + ).select('label_templates.* AS label_templates') + .select('creators.full_name AS created_by_user') + .select('modifiers.full_name AS modified_by') + LabelTemplate.from(res, :label_templates) + end + + def filter_records(records) + records.where_attributes_like( + ['label_templates.name', 'label_templates.format', 'label_templates.description', + 'label_templates.modified_by', 'label_templates.created_by_user'], + dt_params.dig(:search, :value) + ) + end +end diff --git a/app/models/label_template.rb b/app/models/label_template.rb index 068ee4d97..597720882 100644 --- a/app/models/label_template.rb +++ b/app/models/label_template.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true class LabelTemplate < ApplicationRecord + include SearchableModel + enum language_type: { zpl: 0 } - validates :name, presence: true + validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, + maximum: Constants::NAME_MAX_LENGTH } validates :size, presence: true validates :content, presence: true diff --git a/app/permissions/team.rb b/app/permissions/team.rb index 88b7117f5..657f4e8d1 100644 --- a/app/permissions/team.rb +++ b/app/permissions/team.rb @@ -60,6 +60,10 @@ Canaid::Permissions.register_for(Team) do can :view_label_templates do |user, team| user.is_normal_user_or_admin_of_team?(team) end + + can :manage_label_templates do |user, team| + user.is_admin_of_team?(team) + end end Canaid::Permissions.register_for(ProjectFolder) do diff --git a/app/views/label_templates/index.html.erb b/app/views/label_templates/index.html.erb index daab4ba32..33bbdb601 100644 --- a/app/views/label_templates/index.html.erb +++ b/app/views/label_templates/index.html.erb @@ -1,8 +1,68 @@ <% if current_team %> <% provide(:sidebar_title, t('sidebar.templates.sidebar_title')) %> + <% provide(:container_class, "no-second-nav-container") %> <%= content_for :sidebar do %> <%= render partial: "/shared/sidebar/templates_sidebar", locals: {active: :label} %> <% end %> -
+ + <% content_for :head do %> + + <% end %> + + <%= stylesheet_link_tag 'datatables' %> + + +
+