mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-12-09 13:46:21 +08:00
Initial label templates refactor [SCI-9303]
This commit is contained in:
parent
da6d294573
commit
bb4349f346
36 changed files with 977 additions and 446 deletions
4
app/assets/images/checkbox/checked.svg
Normal file
4
app/assets/images/checkbox/checked.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1C0 0.447715 0.447715 0 1 0H15C15.5523 0 16 0.447715 16 1V15C16 15.5523 15.5523 16 15 16H1C0.447715 16 0 15.5523 0 15V1Z" fill="#3B99FD"/>
|
||||
<path d="M6.56358 11.8292L3 8.32244L3.70142 7.60969L6.55139 10.4143L12.8641 4L13.5775 4.70204L6.56358 11.8292Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 381 B |
4
app/assets/images/checkbox/default.svg
Normal file
4
app/assets/images/checkbox/default.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H1V15H15V1ZM1 0C0.447715 0 0 0.447715 0 1V15C0 15.5523 0.447715 16 1 16H15C15.5523 16 16 15.5523 16 15V1C16 0.447715 15.5523 0 15 0H1Z" fill="#1D2939"/>
|
||||
<path d="M1 1H15V15H1V1Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
4
app/assets/images/checkbox/disabled.svg
Normal file
4
app/assets/images/checkbox/disabled.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H1V15H15V1ZM1 0C0.447715 0 0 0.447715 0 1V15C0 15.5523 0.447715 16 1 16H15C15.5523 16 16 15.5523 16 15V1C16 0.447715 15.5523 0 15 0H1Z" fill="#EAECF0"/>
|
||||
<path d="M1 1H15V15H1V1Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
4
app/assets/images/checkbox/indeterminate.svg
Normal file
4
app/assets/images/checkbox/indeterminate.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1C0 0.447715 0.447716 0 1 0H15C15.5523 0 16 0.447715 16 1V15C16 15.5523 15.5523 16 15 16H1C0.447716 16 0 15.5523 0 15V1Z" fill="#3B99FD"/>
|
||||
<path d="M13 8H3V9H13V8Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 295 B |
|
|
@ -1,306 +0,0 @@
|
|||
/* global I18n DataTableHelpers HelperModule */
|
||||
/* eslint-disable no-use-before-define no-param-reassign */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var LABEL_TEMPLATE_TABLE;
|
||||
var rowsSelected = [];
|
||||
|
||||
function rowsSelectedIDs() {
|
||||
return rowsSelected.map(i => i.id);
|
||||
}
|
||||
|
||||
function renderCheckboxHTML(data) {
|
||||
return `<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox label-row-checkbox" data-action='toggle'
|
||||
data-label-template-id="${data}">
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderDefaultTemplateHTML(data) {
|
||||
return data ? '<i class="fas fa-thumbtack"></i>' : '';
|
||||
}
|
||||
|
||||
function renderNameHTML(data, type, row) {
|
||||
return `<div class="flex gap-2">${data.icon_image_tag}<a
|
||||
href='${row.DT_RowAttr['data-edit-url']}'
|
||||
class='label-info-link'
|
||||
>${data.name}</a></div>`;
|
||||
}
|
||||
|
||||
function addAttributesToRow(row, data) {
|
||||
$(row).addClass('label-template-row')
|
||||
.attr('data-id', data['0']);
|
||||
}
|
||||
|
||||
function initNameClick() {
|
||||
$('.label-info-link', '.dataTables_scrollBody').on('click', function() {
|
||||
window.location.href = this.href;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function initToggleAllCheckboxes() {
|
||||
$('input[name="select_all"]').change(function() {
|
||||
if ($(this).is(':checked')) {
|
||||
$("[data-action='toggle']").prop('checked', true);
|
||||
$('.label-template-row').addClass('selected');
|
||||
$('.label-template-row [data-action="toggle"]').change();
|
||||
} else {
|
||||
$("[data-action='toggle']").prop('checked', false);
|
||||
$('.label-template-row').removeClass('selected');
|
||||
$('.label-template-row [data-action="toggle"]').change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initCreateButton() {
|
||||
$('#newLabelTemplate').on('click', function() {
|
||||
$.post(this.dataset.url);
|
||||
});
|
||||
}
|
||||
|
||||
function initSetDefaultButton() {
|
||||
$(document).on('click', '#setZplDefaultLabelTemplate, #setFluicsDefaultLabelTemplate', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (rowsSelected.length === 1) {
|
||||
$.post(rowsSelected[0].setDefaultUrl, function(response) {
|
||||
reloadTable();
|
||||
HelperModule.flashAlertMsg(response.message, 'success');
|
||||
}).fail((response) => {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.error, 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initEditButton() {
|
||||
$('#editTemplate').on('click', function() {
|
||||
if (rowsSelected.length === 1) {
|
||||
window.location.href = rowsSelected[0].editUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initDuplicateButton() {
|
||||
$(document).on('click', '#duplicateLabelTemplate', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (rowsSelected.length > 0) {
|
||||
$.post(this.dataset.url, { selected_ids: rowsSelectedIDs() }, function(response) {
|
||||
reloadTable();
|
||||
HelperModule.flashAlertMsg(response.message, 'success');
|
||||
}).fail((response) => {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.error, 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initDeleteModal() {
|
||||
$(document).on('click', '#deleteLabelTemplate', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$('#deleteLabelTemplatesModal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
function initDeleteButton() {
|
||||
$('#confirmLabeleDeletion').on('click', function() {
|
||||
if (rowsSelected.length > 0) {
|
||||
$.post(this.dataset.url, { selected_ids: rowsSelectedIDs() }, function(response) {
|
||||
reloadTable();
|
||||
HelperModule.flashAlertMsg(response.message, 'success');
|
||||
$('#deleteLabelTemplatesModal').modal('hide');
|
||||
}).fail((response) => {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.error, 'danger');
|
||||
$('#deleteLabelTemplatesModal').modal('hide');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initRefreshFluicsButton() {
|
||||
$('#syncFluicsTemplates').on('click', function() {
|
||||
$.post(this.dataset.url, function(response) {
|
||||
reloadTable();
|
||||
HelperModule.flashAlertMsg(response.message, 'success');
|
||||
}).fail((response) => {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.error, 'danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tableDrawCallback() {
|
||||
initToggleAllCheckboxes();
|
||||
initRowSelection();
|
||||
initNameClick();
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
if (window.actionToolbarComponent) {
|
||||
window.actionToolbarComponent.fetchActions({ label_template_ids: rowsSelectedIDs() });
|
||||
$('.dataTables_scrollBody').css('padding-bottom', `${rowsSelectedIDs().length > 0 ? 68 : 0}px`);
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
LABEL_TEMPLATE_TABLE.ajax.reload(null, false);
|
||||
rowsSelected = [];
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function updateDataTableSelectAllCtrl() {
|
||||
var $table = LABEL_TEMPLATE_TABLE.table().node();
|
||||
var $header = LABEL_TEMPLATE_TABLE.table().header();
|
||||
var $chkboxAll = $('.label-row-checkbox', $table);
|
||||
var $chkboxChecked = $('.label-row-checkbox:checked', $table);
|
||||
var chkboxSelectAll = $('input[name="select_all"]', $header).get(0);
|
||||
|
||||
// If none of the checkboxes are checked
|
||||
if ($chkboxChecked.length === 0) {
|
||||
chkboxSelectAll.checked = false;
|
||||
if ('indeterminate' in chkboxSelectAll) {
|
||||
chkboxSelectAll.indeterminate = false;
|
||||
}
|
||||
|
||||
// If all of the checkboxes are checked
|
||||
} else if ($chkboxChecked.length === $chkboxAll.length) {
|
||||
chkboxSelectAll.checked = true;
|
||||
if ('indeterminate' in chkboxSelectAll) {
|
||||
chkboxSelectAll.indeterminate = false;
|
||||
}
|
||||
|
||||
// If some of the checkboxes are checked
|
||||
} else {
|
||||
chkboxSelectAll.checked = true;
|
||||
if ('indeterminate' in chkboxSelectAll) {
|
||||
chkboxSelectAll.indeterminate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initRowSelection() {
|
||||
// Handle clicks on checkbox
|
||||
$('#label-templates-table').on('change', '.label-row-checkbox', function(ev) {
|
||||
var rowId;
|
||||
var index;
|
||||
var row;
|
||||
|
||||
rowId = this.dataset.labelTemplateId;
|
||||
row = $(this).closest('tr')[0];
|
||||
|
||||
// Determine whether row ID is in the list of selected row IDs
|
||||
index = rowsSelected.findIndex(v => v.id === rowId);
|
||||
|
||||
// If checkbox is checked and row ID is not in list of selected row IDs
|
||||
if (this.checked && index === -1) {
|
||||
rowsSelected.push({
|
||||
id: rowId,
|
||||
default: row.dataset.default,
|
||||
editUrl: row.dataset.editUrl,
|
||||
setDefaultUrl: row.dataset.setDefaultUrl,
|
||||
format: row.dataset.format
|
||||
});
|
||||
// Otherwise, if checkbox is not checked and row ID is in list of selected row IDs
|
||||
} else if (!this.checked && index !== -1) {
|
||||
rowsSelected.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.checked) {
|
||||
$(this).closest('.label-template-row').addClass('selected');
|
||||
} else {
|
||||
$(this).closest('.label-template-row').removeClass('selected');
|
||||
}
|
||||
|
||||
updateDataTableSelectAllCtrl();
|
||||
|
||||
ev.stopPropagation();
|
||||
updateButtons();
|
||||
});
|
||||
}
|
||||
// INIT
|
||||
|
||||
function initDatatable() {
|
||||
var $table = $('#label-templates-table');
|
||||
LABEL_TEMPLATE_TABLE = $table.DataTable({
|
||||
dom: "R<'label-toolbar'<'label-buttons-container'><'label-search-container'f>>t<'pagination-row hidden'<'pagination-info'li><'pagination-actions'p>>",
|
||||
order: [[2, 'desc']],
|
||||
stateSave: true,
|
||||
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: true,
|
||||
width: '1.5rem',
|
||||
render: renderDefaultTemplateHTML
|
||||
}, {
|
||||
targets: 2,
|
||||
className: 'label-template-name',
|
||||
render: renderNameHTML
|
||||
}, {
|
||||
targets: 4,
|
||||
className: 'whitespace-break-spaces',
|
||||
render: data => `<span class='whitespace-break-spaces'>${data}</span>`
|
||||
}],
|
||||
oLanguage: {
|
||||
sSearch: I18n.t('general.filter')
|
||||
},
|
||||
fnDrawCallback: tableDrawCallback,
|
||||
createdRow: addAttributesToRow,
|
||||
fnInitComplete: function() {
|
||||
DataTableHelpers.initLengthAppearance($table.closest('.dataTables_wrapper'));
|
||||
DataTableHelpers.initSearchField(
|
||||
$table.closest('.dataTables_wrapper'),
|
||||
I18n.t('label_templates.index.search_templates')
|
||||
);
|
||||
$('.pagination-row').removeClass('hidden');
|
||||
|
||||
let toolBar = $($('#labelTemplatesToolbar').html());
|
||||
$('.label-buttons-container').html(toolBar);
|
||||
|
||||
initCreateButton();
|
||||
initEditButton();
|
||||
initSetDefaultButton();
|
||||
initDuplicateButton();
|
||||
initDeleteModal();
|
||||
initRefreshFluicsButton();
|
||||
window.initActionToolbar();
|
||||
window.actionToolbarComponent.setBottomOffset(68);
|
||||
},
|
||||
stateLoadParams: function(_, state) {
|
||||
state.search.search = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#wrapper').on('sideBar::shown sideBar::hidden', function() {
|
||||
if (LABEL_TEMPLATE_TABLE) {
|
||||
LABEL_TEMPLATE_TABLE.columns.adjust();
|
||||
}
|
||||
});
|
||||
|
||||
initDatatable();
|
||||
initDeleteButton();
|
||||
}());
|
||||
|
|
@ -81,6 +81,7 @@
|
|||
@import "shared/action_toolbar";
|
||||
@import "shared/assets";
|
||||
@import "shared/avatar";
|
||||
@import "shared/ag_table";
|
||||
@import "shared/cards";
|
||||
@import "shared/comments_sidebar";
|
||||
@import "shared/comments";
|
||||
|
|
@ -111,6 +112,7 @@
|
|||
@import "themes/repositories";
|
||||
@import "themes/scinote";
|
||||
|
||||
|
||||
@import "navigation/general";
|
||||
@import "navigation/breadcrumbs";
|
||||
@import "navigation/left_menu";
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
}
|
||||
|
||||
.projects-show {
|
||||
--content-header-size: 9em;
|
||||
.content-header {
|
||||
height: var(--content-header-size);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,6 +568,7 @@ li.module-hover {
|
|||
// New projects page
|
||||
|
||||
.projects-index {
|
||||
--content-header-size: 9em;
|
||||
.content-header {
|
||||
height: var(--content-header-size);
|
||||
}
|
||||
|
|
|
|||
33
app/assets/stylesheets/shared/ag_table.scss
Normal file
33
app/assets/stylesheets/shared/ag_table.scss
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.ag-root-wrapper {
|
||||
--agg-row-border-color: var(--sn-light-grey);
|
||||
--ag-odd-row-background-color: var(--sn-super-light-grey);
|
||||
--ag-header-background-color: var(--sn-light-grey);
|
||||
--ag-selected-row-background-color: var(--sn-super-light-blue);
|
||||
--ag-range-selection-border: var(--sn-science-blue);
|
||||
--ag-grid-size: .5rem;
|
||||
--ag-cell-horizontal-padding: calc(var(--ag-grid-size) * 2);
|
||||
--ag-row-hover-color: var(--sn-super-light-grey);
|
||||
--ag-header-column-resize-handle-height: 100%;
|
||||
--ag-header-column-resize-handle-color: var(--sn-sleepy-grey);
|
||||
--ag-header-column-resize-handle-width: 1px;
|
||||
--ag-row-border-width: 0px;
|
||||
--ag-icon-font-code-checkbox-unchecked: asset-url("checkbox/default.svg");
|
||||
--ag-icon-font-code-checkbox-checked: asset-url("checkbox/checked.svg");
|
||||
--ag-icon-font-code-checkbox-indeterminate: asset-url("checkbox/indeterminate.svg");
|
||||
--ag-input-focus-box-shadow: none;
|
||||
|
||||
border: 0;
|
||||
|
||||
.ag-header {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ag-input-field-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ag-input-field-input:focus {
|
||||
outline: none !important;
|
||||
outline-offset: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
// scss-lint:disable SelectorFormat
|
||||
|
||||
.cards-wrapper {
|
||||
--content-header-size: 9em;
|
||||
--card-min-width: 200px;
|
||||
--list-columns-number: 5;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// scss-lint:disable NestingDepth QualifyingElement
|
||||
|
||||
.content-pane {
|
||||
--content-header-size: 9.5em;
|
||||
--content-header-size: 4em;
|
||||
background-color: var(--sn-white);
|
||||
margin: 20px 0;
|
||||
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.fixed-content-body {
|
||||
height: calc(100vh - var(--content-header-size) - var(--navbar-height));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
&.sticky-header {
|
||||
background-color: inherit;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@
|
|||
left: 0;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
top: 2.5rem;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
}
|
||||
|
||||
.sci-input-container-v2 {
|
||||
@apply relative h-[2.75rem] flex items-center;
|
||||
@apply relative h-[2.75rem] flex items-center transition-all;
|
||||
}
|
||||
|
||||
.sci-input-container-v2.input-sm {
|
||||
|
|
|
|||
|
|
@ -50,13 +50,11 @@ class LabelTemplatesController < ApplicationController
|
|||
label_template.last_modified_by = current_user
|
||||
label_template.save!
|
||||
log_activity(:label_template_created, label_template)
|
||||
redirect_to label_template_path(label_template, new_label: true)
|
||||
render json: { redirect_url: label_template_path(label_template, new_label: true) }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error(e.message)
|
||||
Rails.logger.error(e.backtrace.join("\n"))
|
||||
flash[:error] = I18n.t('errors.general')
|
||||
redirect_to label_templates_path
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
@ -154,7 +152,7 @@ class LabelTemplatesController < ApplicationController
|
|||
actions:
|
||||
Toolbars::LabelTemplatesService.new(
|
||||
current_user,
|
||||
label_template_ids: params[:label_template_ids].split(',')
|
||||
label_template_ids: params[:item_ids].split(',')
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
|
|
|||
71
app/datatables/custom_datatable_v2.rb
Normal file
71
app/datatables/custom_datatable_v2.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomDatatableV2
|
||||
attr_reader :params, :options
|
||||
|
||||
def initialize(view, raw_data, options = {})
|
||||
@raw_data = raw_data
|
||||
@params = view.params
|
||||
@options = options
|
||||
end
|
||||
|
||||
def as_json(_options = {})
|
||||
{
|
||||
data: data,
|
||||
pageTotal: records.total_pages
|
||||
}
|
||||
end
|
||||
|
||||
def records
|
||||
@records ||= fetch_records
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def order_params
|
||||
@order_params ||=
|
||||
params.require(:order).permit(:column, :dir).to_h
|
||||
end
|
||||
|
||||
def fetch_records
|
||||
records = get_raw_records
|
||||
records = sort_records(records) if params[:order].present?
|
||||
records = paginate_records(records) if params[:page].present?
|
||||
records = filter_records(records) if params[:search].present?
|
||||
records
|
||||
end
|
||||
|
||||
def paginate_records(records)
|
||||
records.page(params[:page]).per(params[:per_page])
|
||||
end
|
||||
|
||||
def sort_direction(order_params)
|
||||
order_params[:dir] == 'asc' ? 'ASC' : 'DESC'
|
||||
end
|
||||
|
||||
def sort_records(records)
|
||||
sort_by = "#{sortable_columns[order_params[:column].to_sym]} #{sort_direction(order_params)}"
|
||||
records.order(sort_by)
|
||||
end
|
||||
|
||||
def generate_sortable_displayed_columns
|
||||
@sortable_displayed_columns = []
|
||||
columns_params.each_value do |col|
|
||||
@sortable_displayed_columns << col[:data] if col[:orderable] == 'true'
|
||||
end
|
||||
@sortable_displayed_columns
|
||||
end
|
||||
|
||||
def formated_date
|
||||
f_date = I18n.backend.date_format.dup
|
||||
f_date.gsub!(/%-d/, 'FMDD')
|
||||
f_date.gsub!(/%d/, 'DD')
|
||||
f_date.gsub!(/%-m/, 'FMMM')
|
||||
f_date.gsub!(/%m/, 'MM')
|
||||
f_date.gsub!(/%b/, 'Mon')
|
||||
f_date.gsub!(/%B/, 'Month')
|
||||
f_date.gsub!('%Y', 'YYYY')
|
||||
f_date += ' HH24:MI'
|
||||
f_date
|
||||
end
|
||||
end
|
||||
|
|
@ -1,24 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LabelTemplateDatatable < CustomDatatable
|
||||
class LabelTemplateDatatable < CustomDatatableV2
|
||||
include InputSanitizeHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
TABLE_COLUMNS = %w(
|
||||
label_templates.default
|
||||
label_templates.name
|
||||
label_templates.type
|
||||
label_templates.description
|
||||
label_templates.modified_by
|
||||
label_templates.updated_at
|
||||
label_templates.created_by_user
|
||||
label_templates.created_at
|
||||
).freeze
|
||||
|
||||
def initialize(view, label_templates)
|
||||
super(view)
|
||||
@label_templates = label_templates
|
||||
end
|
||||
TABLE_COLUMNS = {
|
||||
default: 'label_templates.default',
|
||||
name: 'label_templates.name',
|
||||
format: 'label_templates.type',
|
||||
description: 'label_templates.description',
|
||||
modified_by: 'label_templates.modified_by',
|
||||
updated_at: 'label_templates.updated_at',
|
||||
created_by: 'label_templates.created_by_user',
|
||||
created_at: 'label_templates.created_at'
|
||||
}.freeze
|
||||
|
||||
def sortable_columns
|
||||
@sortable_columns ||= TABLE_COLUMNS
|
||||
|
|
@ -30,24 +25,25 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
|
||||
private
|
||||
|
||||
def order_params
|
||||
@order_params ||=
|
||||
params.require(:order).permit(:column, :dir).to_h
|
||||
end
|
||||
|
||||
def data
|
||||
records.map do |record|
|
||||
{
|
||||
'0' => record.id,
|
||||
'1' => record.default,
|
||||
'2' => append_format_icon(record),
|
||||
'3' => escape_input(record.label_format),
|
||||
'4' => escape_input(record.description),
|
||||
'5' => escape_input(record.modified_by),
|
||||
'6' => I18n.l(record.updated_at, format: :full),
|
||||
'7' => escape_input(record.created_by_user),
|
||||
'8' => I18n.l(record.created_at, format: :full),
|
||||
'recordInfoUrl' => '',
|
||||
'DT_RowAttr': {
|
||||
'data-edit-url': label_template_path(record),
|
||||
'data-set-default-url': set_default_label_template_path(record),
|
||||
'data-default': record.default,
|
||||
'data-format': record.label_format
|
||||
id: record.id,
|
||||
default: record.default,
|
||||
name: append_format_icon(record),
|
||||
format: escape_input(record.label_format),
|
||||
description: escape_input(record.description),
|
||||
modified_by: escape_input(record.modified_by),
|
||||
updated_at: I18n.l(record.updated_at, format: :full),
|
||||
created_by: escape_input(record.created_by_user),
|
||||
created_at: I18n.l(record.created_at, format: :full),
|
||||
attributes: {
|
||||
edit_url: label_template_path(record)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -65,7 +61,7 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
end
|
||||
|
||||
def get_raw_records
|
||||
res = @label_templates.joins(
|
||||
res = @raw_data.joins(
|
||||
'LEFT OUTER JOIN users AS creators ' \
|
||||
'ON label_templates.created_by_id = creators.id'
|
||||
).joins(
|
||||
|
|
@ -85,7 +81,7 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
records.where_attributes_like(
|
||||
['label_templates.name', 'label_templates.label_format', 'label_templates.description',
|
||||
'label_templates.modified_by', 'label_templates.created_by_user'],
|
||||
dt_params.dig(:search, :value)
|
||||
params[:search]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
@import "bootstrap-select/sass/bootstrap-select"
|
||||
@import "bootstrap-select/sass/bootstrap-select";
|
||||
@import "ag-grid-community/styles/ag-grid.css";
|
||||
@import "ag-grid-community/styles/ag-theme-alpine.css";
|
||||
|
|
|
|||
19
app/javascript/packs/vue/label_templates_table.js
Normal file
19
app/javascript/packs/vue/label_templates_table.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import TurbolinksAdapter from 'vue-turbolinks';
|
||||
import Vue from 'vue/dist/vue.esm';
|
||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||
import LabelTemplatesTable from '../../vue/label_template/table.vue';
|
||||
|
||||
Vue.use(TurbolinksAdapter);
|
||||
Vue.use(PerfectScrollbar);
|
||||
Vue.prototype.i18n = window.I18n;
|
||||
|
||||
window.initLabelTemplatesTableComponent = () => {
|
||||
new Vue({
|
||||
el: '#labelTemplatesTable',
|
||||
components: {
|
||||
'label-templates-table': LabelTemplatesTable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
initLabelTemplatesTableComponent();
|
||||
154
app/javascript/vue/label_template/table.vue
Normal file
154
app/javascript/vue/label_template/table.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<div class="h-full">
|
||||
<DataTable :columnDefs="columnDefs"
|
||||
:tableId="'labelTemplates'"
|
||||
:dataUrl="dataSource"
|
||||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
@set_as_default="setDefault"
|
||||
@duplicate="duplicate"
|
||||
@create="createTemplate"
|
||||
@sync_fluics="syncFluicsLabels"
|
||||
@delete="deleteTemplates"
|
||||
/>
|
||||
<DeleteModal
|
||||
:title="i18n.t('label_templates.index.delete_modal.title')"
|
||||
:description="i18n.t('label_templates.index.delete_modal.description')"
|
||||
:confirmClass="'btn btn-danger'"
|
||||
:confirmText="i18n.t('general.delete')"
|
||||
ref="deleteModal"
|
||||
></DeleteModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
import DataTable from '../shared/datatable/table.vue'
|
||||
import DeleteModal from '../shared/confirmation_modal.vue'
|
||||
|
||||
export default {
|
||||
name: 'LabelTemplatesTable',
|
||||
components: {
|
||||
DataTable,
|
||||
DeleteModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
type: String,
|
||||
},
|
||||
syncFluicsUrl: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reloadingTable: false,
|
||||
columnDefs: [ { field: "default", headerName: '', width: 80, minWidth: 80,
|
||||
cellRenderer: this.defaultRenderer, sortable: true, headerComponentParams: { html: '<i class="fas fa-thumbtack"></i>' } },
|
||||
{ field: "name", headerName: this.i18n.t('label_templates.index.thead_name'), cellRenderer: this.labelNameRenderer, sortable: true},
|
||||
{ field: "format", headerName: this.i18n.t('label_templates.index.format'), sortable: true },
|
||||
{ field: "description", headerName: this.i18n.t('label_templates.index.description'), sortable: true },
|
||||
{ field: "modified_by", headerName: this.i18n.t('label_templates.index.updated_by'), sortable: true },
|
||||
{ field: "updated_at", headerName: this.i18n.t('label_templates.index.updated_at'), sortable: true },
|
||||
{ field: "created_by", headerName: this.i18n.t('label_templates.index.created_by'), sortable: true },
|
||||
{ field: "created_at", headerName: this.i18n.t('label_templates.index.created_at'), sortable: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
toolbarActions() {
|
||||
let left = []
|
||||
if (this.createUrl) {
|
||||
left.push({
|
||||
name: 'create',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('label_templates.index.toolbar.new'),
|
||||
type: 'emit',
|
||||
path: this.createUrl,
|
||||
buttonStyle: 'btn btn-primary'
|
||||
})
|
||||
}
|
||||
if (this.syncFluicsUrl) {
|
||||
left.push({
|
||||
name: 'sync_fluics',
|
||||
icon: 'fas fa-sync',
|
||||
label: this.i18n.t('label_templates.index.toolbar.update_fluics_labels'),
|
||||
type: 'emit',
|
||||
path: this.syncFluicsUrl,
|
||||
buttonStyle: 'btn btn-light'
|
||||
})
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
right: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
labelNameRenderer(params) {
|
||||
let name = params.data.name;
|
||||
let editUrl = params.data.attributes.edit_url;
|
||||
return `<a href="${editUrl}">
|
||||
${name.icon_image_tag}
|
||||
${name.name}
|
||||
</a>`
|
||||
},
|
||||
defaultRenderer(params) {
|
||||
let defaultSelected = params.data.default;
|
||||
return defaultSelected ? '<i class="fas fa-thumbtack"></i>' : '';
|
||||
},
|
||||
setDefault(action) {
|
||||
axios.post(action.path).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
duplicate(action, rows) {
|
||||
axios.post(action.path, { selected_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
createTemplate(action) {
|
||||
axios.post(action.path).then((response) => {
|
||||
window.location.href = response.data.redirect_url;
|
||||
})
|
||||
},
|
||||
syncFluicsLabels(action) {
|
||||
axios.post(action.path).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
async deleteTemplates(action, rows) {
|
||||
const ok = await this.$refs.deleteModal.show()
|
||||
if (ok) {
|
||||
axios.delete(action.path, { data: { selected_ids: rows.map((row) => row.id) } }).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
76
app/javascript/vue/shared/confirmation_modal.vue
Normal file
76
app/javascript/vue/shared/confirmation_modal.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title">
|
||||
{{ title }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" v-html="description"></div>
|
||||
<div class="modal-footer">
|
||||
<button :class="cancelClass" @click="cancel">{{ cancelText || i18n.t('general.cancel') }}</button>
|
||||
<button :class="confirmClass" @click="confirm">{{ confirmText || i18n.t('general.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'deleteStepModal',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
cancelText: {
|
||||
type: String
|
||||
},
|
||||
cancelClass: {
|
||||
type: String,
|
||||
default: 'btn btn-secondary'
|
||||
},
|
||||
confirmText: {
|
||||
type: String
|
||||
},
|
||||
confirmClass: {
|
||||
type: String,
|
||||
default: 'btn btn-primary'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.resolvePromise(false)
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resolvePromise: null,
|
||||
rejectPromise: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show: function() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvePromise = resolve
|
||||
this.rejectPromise = reject
|
||||
})
|
||||
},
|
||||
confirm() {
|
||||
this.resolvePromise(true)
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
cancel() {
|
||||
this.resolvePromise(false)
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
72
app/javascript/vue/shared/datatable/action_toolbar.vue
Normal file
72
app/javascript/vue/shared/datatable/action_toolbar.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="p-4 w-full rounded bg-sn-light-grey min-h-[68px]">
|
||||
<div class="flex gap-4 items-center h-full">
|
||||
<div v-if="loading && !actions.length" class="sn-action-toolbar__action">
|
||||
<a class="rounded flex items-center py-1.5 px-2.5 bg-transparent text-transparent no-underline"></a>
|
||||
</div>
|
||||
<div v-if="!loading && actions.length === 0" class="text-sn-grey-grey">
|
||||
{{ i18n.t('action_toolbar.no_actions') }}
|
||||
</div>
|
||||
<div v-for="action in actions" :key="action.name" class="sn-action-toolbar__action shrink-0">
|
||||
<a :class="`rounded flex gap-2 items-center py-1.5 px-2.5 bg-sn-white color-sn-blue hover:no-underline focus:no-underline ${action.button_class}`"
|
||||
:href="(['link', 'remote-modal']).includes(action.type) ? action.path : '#'"
|
||||
:id="action.button_id"
|
||||
:title="action.label"
|
||||
@click="doAction(action, $event)">
|
||||
<i :class="action.icon"></i>
|
||||
<span class="sn-action-toolbar__button-text">{{ action.label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ActionToolbar',
|
||||
props: {
|
||||
actionsUrl: { type: String, required: true },
|
||||
params: { type: Object }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
multiple: false,
|
||||
reloadCallback: null,
|
||||
loaded: false,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params() {
|
||||
this.loadActions()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadActions();
|
||||
},
|
||||
methods: {
|
||||
loadActions() {
|
||||
this.loading = true;
|
||||
this.loaded = false;
|
||||
$.get(`${this.actionsUrl}?${new URLSearchParams(this.params).toString()}`, (data) => {
|
||||
this.actions = data.actions;
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
});
|
||||
},
|
||||
doAction(action, event) {
|
||||
switch(action.type) {
|
||||
case 'emit':
|
||||
event.preventDefault();
|
||||
this.$emit('toolbar:action', action);
|
||||
// do nothing, this is handled by legacy code based on the button class
|
||||
break;
|
||||
case 'link':
|
||||
// do nothing, already handled by href
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
app/javascript/vue/shared/datatable/pagination.vue
Normal file
57
app/javascript/vue/shared/datatable/pagination.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div v-if="pages.length > 1" class="flex gap-3 select-none">
|
||||
<div class="w-9 h-9">
|
||||
<div class="w-9 h-9 cursor-pointer flex items-center justify-center"
|
||||
@click="$emit('setPage', currentPage - 1)"
|
||||
v-if="totalPage > 5 && currentPage > 1">
|
||||
<i class="sn-icon sn-icon-left cursor-pointer"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-9 h-9 cursor-pointer flex items-center justify-center"
|
||||
v-for="page in pages"
|
||||
:class="{ 'border-solid rounded border-sn-science-blue': page === currentPage }"
|
||||
:key="page"
|
||||
@click="$emit('setPage', page)">
|
||||
<span >{{ page }}</span>
|
||||
</div>
|
||||
<div class="w-9 h-9">
|
||||
<div class="w-9 h-9 cursor-pointer flex items-center justify-center"
|
||||
@click="$emit('setPage', currentPage + 1)"
|
||||
v-if="totalPage - currentPage > 2 && totalPage > 5">
|
||||
<i class="sn-icon sn-icon-right cursor-pointer"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
totalPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
let pages = [];
|
||||
for (let i = 1; i <= this.totalPage; i++) {
|
||||
if (i >= this.currentPage - 2 || this.totalPage <= 5) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (pages.length === 5) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
237
app/javascript/vue/shared/datatable/table.vue
Normal file
237
app/javascript/vue/shared/datatable/table.vue
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="relative flex flex-col flex-grow">
|
||||
<Toolbar :toolbarActions="toolbarActions" @toolbar:action="emitAction" :searchValue="searchValue" @search:change="setSearchValue" />
|
||||
<ag-grid-vue
|
||||
class="ag-theme-alpine w-full flex-grow h-full"
|
||||
:class="{'opacity-0': initializing}"
|
||||
:columnDefs="columnDefs"
|
||||
:rowData="rowData"
|
||||
:defaultColDef="defaultColDef"
|
||||
:rowSelection="'multiple'"
|
||||
:gridOptions="gridOptions"
|
||||
@grid-ready="onGridReady"
|
||||
@first-data-rendered="onFirstDataRendered"
|
||||
@sortChanged="setOrder"
|
||||
@columnResized="saveColumnsState"
|
||||
@columnMoved="saveColumnsState"
|
||||
@rowSelected="setSelectedRows"
|
||||
:CheckboxSelectionCallback="withCheckboxes"
|
||||
>
|
||||
</ag-grid-vue>
|
||||
<ActionToolbar v-if="selectedRows.length > 0 && actionsUrl" :actionsUrl="actionsUrl" :params="actionsParams" @toolbar:action="emitAction" />
|
||||
</div>
|
||||
<div class="flex items-center py-4">
|
||||
<div class="mr-auto">
|
||||
<Pagination
|
||||
:totalPage="totalPage"
|
||||
:currentPage="page"
|
||||
@setPage="setPage"
|
||||
></Pagination>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
{{ i18n.t('datatable.show') }}
|
||||
<div class="w-36">
|
||||
<Select
|
||||
:value="perPage"
|
||||
:options="perPageOptions"
|
||||
@change="setPerPage"
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AgGridVue } from "ag-grid-vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import Select from '../select.vue';
|
||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||
import Pagination from './pagination.vue';
|
||||
import CustomHeader from './tableHeader';
|
||||
import ActionToolbar from './action_toolbar.vue';
|
||||
import Toolbar from './toolbar.vue';
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
props: {
|
||||
withCheckboxes: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tableId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
columnDefs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dataUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
},
|
||||
toolbarActions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
reloadingTable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rowData: [],
|
||||
gridApi: null,
|
||||
columnApi: null,
|
||||
defaultColDef: {
|
||||
resizable: true
|
||||
},
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
order: null,
|
||||
totalPage: 0,
|
||||
selectedRows: [],
|
||||
searchValue: '',
|
||||
initializing: true
|
||||
};
|
||||
},
|
||||
components: {
|
||||
AgGridVue,
|
||||
Select,
|
||||
PerfectScrollbar,
|
||||
Pagination,
|
||||
agColumnHeader: CustomHeader,
|
||||
ActionToolbar,
|
||||
Toolbar
|
||||
},
|
||||
computed: {
|
||||
perPageOptions() {
|
||||
return [10, 20, 50, 100].map(value => [ value, `${value} ${this.i18n.t('datatable.rows')}` ]);
|
||||
},
|
||||
tableState() {
|
||||
if (!localStorage.getItem(`datatable:${this.tableId}_columns_state`)) return null;
|
||||
|
||||
return JSON.parse(localStorage.getItem(`datatable:${this.tableId}_columns_state`));
|
||||
},
|
||||
actionsParams() {
|
||||
return {
|
||||
item_ids: this.selectedRows.map(row => row.id).join(',')
|
||||
}
|
||||
},
|
||||
gridOptions() {
|
||||
return {
|
||||
suppressCellFocus: true
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.withCheckboxes) {
|
||||
this.columnDefs.unshift({
|
||||
field: "checkbox",
|
||||
headerCheckboxSelection: true,
|
||||
headerCheckboxSelectionFilteredOnly: true,
|
||||
checkboxSelection: true,
|
||||
width: 48,
|
||||
minWidth: 48,
|
||||
resizable: false
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
reloadingTable() {
|
||||
if (this.reloadingTable) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData();
|
||||
window.addEventListener('resize', this.resize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
if (this.tableState) return;
|
||||
|
||||
this.gridApi.sizeColumnsToFit();
|
||||
},
|
||||
loadData() {
|
||||
axios
|
||||
.get(this.dataUrl, {
|
||||
params: {
|
||||
per_page: this.perPage,
|
||||
page: this.page,
|
||||
order: this.order,
|
||||
search: this.searchValue
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
this.selectedRows = [];
|
||||
this.gridApi.setRowData(response.data.data);
|
||||
this.totalPage = response.data.pageTotal;
|
||||
this.$emit('tableReloaded');
|
||||
})
|
||||
},
|
||||
onGridReady(params) {
|
||||
this.gridApi = params.api;
|
||||
this.columnApi = params.columnApi;
|
||||
|
||||
if (this.tableState) {
|
||||
this.columnApi.applyColumnState({
|
||||
state: this.tableState,
|
||||
applyOrder: true
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.initializing = false;
|
||||
}, 200);
|
||||
},
|
||||
onFirstDataRendered(params) {
|
||||
this.resize();
|
||||
},
|
||||
setPerPage(value) {
|
||||
this.perPage = value;
|
||||
this.loadData();
|
||||
},
|
||||
setPage(page) {
|
||||
this.page = page;
|
||||
this.loadData();
|
||||
},
|
||||
setOrder() {
|
||||
const orderState = this.columnApi.getColumnState().filter(column => column.sort).map(column => {
|
||||
return {
|
||||
column: column.colId,
|
||||
dir: column.sort
|
||||
}
|
||||
});
|
||||
this.order = orderState[0];
|
||||
this.saveColumnsState();
|
||||
this.loadData();
|
||||
},
|
||||
saveColumnsState() {
|
||||
if (!this.columnApi) return;
|
||||
|
||||
const columnsState = this.columnApi.getColumnState();
|
||||
localStorage.setItem(`datatable:${this.tableId}_columns_state`, JSON.stringify(columnsState));
|
||||
},
|
||||
setSelectedRows() {
|
||||
this.selectedRows = this.gridApi.getSelectedRows();
|
||||
},
|
||||
emitAction(action) {
|
||||
this.$emit(action.name, action, this.selectedRows);
|
||||
},
|
||||
setSearchValue(value) {
|
||||
this.searchValue = value;
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
40
app/javascript/vue/shared/datatable/tableHeader.js
Normal file
40
app/javascript/vue/shared/datatable/tableHeader.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export default {
|
||||
template: `
|
||||
<div class="w-full grid items-center gap-2 grid-cols-[auto_1.5rem] cursor-pointer" @click="onSortRequested((activeSort == 'asc' ? 'desc' : 'asc'), $event)">
|
||||
<div v-if="params.html" class="customHeaderLabel truncate" v-html="params.html"></div>
|
||||
<div v-else class="customHeaderLabel truncate">{{ params.displayName }}</div>
|
||||
<div v-if="activeSort == 'asc'" class="customSortDownLabel text-sn-sleepy-grey">
|
||||
<i class="sn-icon sn-icon-sort-up"></i>
|
||||
</div>
|
||||
<div v-if="activeSort == 'desc'" class="customSortUpLabel text-sn-sleepy-grey">
|
||||
<i class="sn-icon sn-icon-sort-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeSort: null,
|
||||
};
|
||||
},
|
||||
beforeMount() {},
|
||||
mounted() {
|
||||
this.params.column.addEventListener('sortChanged', this.onSortChanged);
|
||||
this.onSortChanged();
|
||||
},
|
||||
methods: {
|
||||
onSortChanged() {
|
||||
this.activeSort = null;
|
||||
if (this.params.column.isSortAscending()) {
|
||||
this.activeSort = 'asc';
|
||||
} else if (this.params.column.isSortDescending()) {
|
||||
this.activeSort = 'desc';
|
||||
}
|
||||
},
|
||||
|
||||
onSortRequested(order, event) {
|
||||
if (!this.params.enableSorting) return;
|
||||
|
||||
this.params.setSort(order, event.shiftKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
82
app/javascript/vue/shared/datatable/toolbar.vue
Normal file
82
app/javascript/vue/shared/datatable/toolbar.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="flex py-4 items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<a v-for="action in toolbarActions.left" :key="action.label"
|
||||
:class="action.buttonStyle"
|
||||
:href="action.path"
|
||||
@click="doAction(action, $event)">
|
||||
<i :class="action.icon"></i>
|
||||
{{ action.label }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<a v-for="action in toolbarActions.right" :key="action.label"
|
||||
:class="action.buttonStyle"
|
||||
:href="action.path"
|
||||
@click="doAction(action, $event)">
|
||||
<i :class="action.icon"></i>
|
||||
{{ action.label }}
|
||||
</a>
|
||||
<div class="sci-input-container-v2" :class="{'w-48': showSearch, 'w-11': !showSearch}">
|
||||
<input
|
||||
ref="searchInput"
|
||||
class="sci-input-field !pr-8"
|
||||
type="text"
|
||||
@focus="openSearch"
|
||||
@blur="hideSearch"
|
||||
:value="searchValue"
|
||||
:placeholder="'Search...'"
|
||||
@change="$emit('search:change', $event.target.value)"
|
||||
/>
|
||||
<i class="sn-icon sn-icon-search !m-2.5 !ml-auto right-0"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Toolbar',
|
||||
props: {
|
||||
toolbarActions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSearch: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchValue() {
|
||||
if (this.searchValue.length > 0) {
|
||||
this.openSearch();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSearch() {
|
||||
this.showSearch = true;
|
||||
},
|
||||
hideSearch() {
|
||||
if (this.searchValue.length === 0) {
|
||||
this.showSearch = false;
|
||||
}
|
||||
},
|
||||
doAction(action, event) {
|
||||
switch(action.type) {
|
||||
case 'emit':
|
||||
event.preventDefault();
|
||||
this.$emit('toolbar:action', action);
|
||||
break;
|
||||
case 'link':
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -116,8 +116,10 @@
|
|||
updateOptionPosition() {
|
||||
const container = $(this.$refs.container);
|
||||
const rect = container.get(0).getBoundingClientRect();
|
||||
const screenHeight = window.innerHeight;
|
||||
let width = rect.width;
|
||||
let top = rect.top + rect.height;
|
||||
let bottom = screenHeight - rect.bottom + rect.height;
|
||||
let left = rect.left;
|
||||
|
||||
const modal = container.parents('.modal-content');
|
||||
|
|
@ -125,10 +127,23 @@
|
|||
if (modal.length > 0) {
|
||||
const modalRect = modal.get(0).getBoundingClientRect();
|
||||
top -= modalRect.top;
|
||||
bottom -= modalRect.bottom;
|
||||
left -= modalRect.left;
|
||||
}
|
||||
|
||||
this.optionPositionStyle = `position: fixed; top: ${top}px; left: ${left}px; width: ${width}px`
|
||||
if (rect.bottom > screenHeight / 2) {
|
||||
this.optionPositionStyle = `
|
||||
position: fixed;
|
||||
bottom: ${bottom}px;
|
||||
left: ${left}px;
|
||||
width: ${width}px`
|
||||
} else {
|
||||
this.optionPositionStyle = `
|
||||
position: fixed;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
width: ${width}px`
|
||||
}
|
||||
},
|
||||
setUpBlurHandlers() {
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ module Toolbars
|
|||
name: 'set_as_default',
|
||||
label: I18n.t("label_templates.index.toolbar.set_#{@label_templates.first.type}_default"),
|
||||
icon: 'fas fa-thumbtack',
|
||||
button_id: 'setZplDefaultLabelTemplate',
|
||||
type: :legacy
|
||||
path: set_default_label_template_path(@label_templates.first),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -70,9 +70,8 @@ module Toolbars
|
|||
name: 'duplicate',
|
||||
label: I18n.t('label_templates.index.toolbar.duplicate'),
|
||||
icon: 'sn-icon sn-icon-duplicate',
|
||||
button_id: 'duplicateLabelTemplate',
|
||||
path: duplicate_label_templates_path,
|
||||
type: :legacy
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -87,8 +86,8 @@ module Toolbars
|
|||
name: 'delete',
|
||||
label: I18n.t('label_templates.index.toolbar.delete'),
|
||||
icon: 'sn-icon sn-icon-delete',
|
||||
button_id: 'deleteLabelTemplate',
|
||||
type: :legacy
|
||||
path: delete_label_templates_path,
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
<div class="modal" id="deleteLabelTemplatesModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title">
|
||||
<%= t('label_templates.index.delete_modal.title') %>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<%= t('label_templates.index.delete_modal.description') %>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t("general.cancel") %></button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
id="confirmLabeleDeletion" data-url="<%= delete_label_templates_path %>"><%= t("general.delete") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<template id="labelTemplatesToolbar">
|
||||
<% if can_manage_label_templates?(current_team) %>
|
||||
<button data-url="<%= label_templates_path %>" title="<%= t('label_templates.index.toolbar.new_tooltip') %>"
|
||||
class="btn btn-primary auto-shrink-button" id="newLabelTemplate">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
<span class="button-text"><%= t('label_templates.index.toolbar.new') %></span>
|
||||
</button>
|
||||
<button title="<%= t('label_templates.index.toolbar.update_fluics_labels') %>"
|
||||
class="btn btn-light" id="syncFluicsTemplates" data-url="<%= sync_fluics_templates_label_templates_path %>">
|
||||
<i class="fas fa-sync"></i>
|
||||
<span class="button-text"><%= t('label_templates.index.toolbar.update_fluics_labels') %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</template>
|
||||
|
|
@ -1,18 +1,7 @@
|
|||
<% 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 %>
|
||||
<meta name="turbolinks-cache-control" content="no-cache">
|
||||
<% end %>
|
||||
|
||||
<%= stylesheet_link_tag 'datatables' %>
|
||||
|
||||
|
||||
<div class="content-pane flexible label-templates-index">
|
||||
<div class="content-pane flexible label-templates-index">
|
||||
<div class="content-header sticky-header">
|
||||
<div class="title-row">
|
||||
<h1>
|
||||
|
|
@ -20,41 +9,15 @@
|
|||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content-label-templates-index">
|
||||
<div class="label-templates-datatable">
|
||||
<table id="label-templates-table"
|
||||
class="table"
|
||||
data-source="<%= datatable_label_templates_path(format: :json) %>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select-all">
|
||||
<div class="sci-checkbox-container">
|
||||
<input name="select_all" type="checkbox" class="sci-checkbox">
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th id="label-template-selected"><i class="fas fa-thumbtack"></i></th>
|
||||
<th id="label-template-name"><%= t('label_templates.index.thead_name') %></th>
|
||||
<th id="label-template-format"><%= t('label_templates.index.format') %></th>
|
||||
<th id="label-template-description"><%= t('label_templates.index.description') %></th>
|
||||
<th id="label-template-updated-by"><%= t('label_templates.index.updated_by') %></th>
|
||||
<th id="label-template-updated-at"><%= t('label_templates.index.updated_at') %></th>
|
||||
<th id="label-template-created-by"><%= t('label_templates.index.created_by') %></th>
|
||||
<th id="label-template-created-at"><%= t('label_templates.index.created_at') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="actionToolbar" data-behaviour="vue">
|
||||
<action-toolbar actions-url="<%= actions_toolbar_label_templates_url %>" />
|
||||
<div id="labelTemplatesTable" class="fixed-content-body"
|
||||
>
|
||||
<label-templates-table
|
||||
:actions-url="'<%= actions_toolbar_label_templates_url %>'"
|
||||
:data-source="'<%= datatable_label_templates_path(format: :json) %>'"
|
||||
:create-url="'<%= label_templates_path if can_manage_label_templates?(current_team) %>'"
|
||||
:sync-fluics-url="'<%= sync_fluics_templates_label_templates_path if can_manage_label_templates?(current_team) %>'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render partial: "index_toolbar" %>
|
||||
<%= render partial: "delete_modal" %>
|
||||
<%= javascript_include_tag 'label_templates/label_templates_datatable' %>
|
||||
<%= javascript_include_tag 'vue_components_action_toolbar' %>
|
||||
<%= javascript_include_tag 'vue_label_templates_table' %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ Rails.application.config.assets.precompile += %w(reports/reports_datatable.js)
|
|||
Rails.application.config.assets.precompile += %w(reports/save_pdf_to_inventory.js)
|
||||
Rails.application.config.assets.precompile += %w(reports/content.js)
|
||||
Rails.application.config.assets.precompile += %w(session_end.js)
|
||||
Rails.application.config.assets.precompile += %w(label_templates/label_templates_datatable.js)
|
||||
Rails.application.config.assets.precompile += %w(users/connected_devices.js)
|
||||
Rails.application.config.assets.precompile += %w(BrowserPrint-3.0.216.min.js)
|
||||
Rails.application.config.assets.precompile += %w(BrowserPrint-Zebra-1.0.216.min.js)
|
||||
|
|
|
|||
|
|
@ -3583,6 +3583,9 @@ en:
|
|||
secret_key_hint: "(Optional) A secret key that will be included in the Webhook-Secret-Key header, for authentication purposes."
|
||||
# This section contains general words that can be used in any parts of
|
||||
# application.
|
||||
datatable:
|
||||
show: 'Show:'
|
||||
rows: 'rows'
|
||||
tiny_mce:
|
||||
upload_window_title: 'Insert an image from your computer'
|
||||
upload_window_label: 'Choose an image'
|
||||
|
|
@ -3603,6 +3606,7 @@ en:
|
|||
edit: "Edit"
|
||||
delete: "Delete"
|
||||
cancel: "Cancel"
|
||||
confirm: "Confirm"
|
||||
duplicate: "Duplicate"
|
||||
okay: "Okay"
|
||||
back: "Back"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
collection do
|
||||
post :duplicate
|
||||
post :delete
|
||||
delete :delete
|
||||
get :datatable
|
||||
get :template_tags
|
||||
get :zpl_preview
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ const entryList = {
|
|||
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js',
|
||||
vue_components_open_vector_editor: './app/javascript/packs/vue/open_vector_editor.js',
|
||||
vue_navigation_breadcrumbs: './app/javascript/packs/vue/navigation/breadcrumbs.js',
|
||||
vue_protocol_file_import_modal: './app/javascript/packs/vue/protocol_file_import_modal.js'
|
||||
vue_protocol_file_import_modal: './app/javascript/packs/vue/protocol_file_import_modal.js',
|
||||
vue_label_templates_table: './app/javascript/packs/vue/label_templates_table.js'
|
||||
}
|
||||
|
||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||
|
|
@ -125,7 +126,10 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
// Add additional file types
|
||||
extensions: ['.js', '.jsx', '.scss', '.css', '.vue', '.less']
|
||||
extensions: ['.js', '.jsx', '.scss', '.css', '.vue', '.less'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@
|
|||
"@fortawesome/fontawesome-free": "^5.2.0",
|
||||
"@joeattardi/emoji-button": "^4.6.2",
|
||||
"@teselagen/ove": "^0.3.15",
|
||||
"ag-grid-community": "^30.1.0",
|
||||
"ag-grid-vue": "^30.1.0",
|
||||
"ajv": "6.12.6",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.4.0",
|
||||
|
|
@ -78,6 +80,7 @@
|
|||
"twemoji": "^12.1.4",
|
||||
"vue": "^2.6.11",
|
||||
"vue-loader": "^15.9.1",
|
||||
"vue-property-decorator": "^8.0.0",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vue-turbolinks": "^2.2.1",
|
||||
"vue2-perfect-scrollbar": "^1.5.56",
|
||||
|
|
|
|||
22
yarn.lock
22
yarn.lock
|
|
@ -1780,6 +1780,16 @@ acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||
|
||||
ag-grid-community@^30.1.0:
|
||||
version "30.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-30.1.0.tgz#50c532df2e6cc22596300b801651eca76459a189"
|
||||
integrity sha512-D69e63CUALxfgLZSu1rXC8Xiyhu6+17zxzTV8cWsyvt5GeSDv2frQ3BKOqGZbUfVoOCLv2SQoHVTTqw8OjxavA==
|
||||
|
||||
ag-grid-vue@^30.1.0:
|
||||
version "30.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ag-grid-vue/-/ag-grid-vue-30.1.0.tgz#79d43c5884aefe63c9169f6294f23343d7e8d7b9"
|
||||
integrity sha512-5ALSbCG5u4g5Lt1nVVYD5fMLA3WhLd3sR92fCTKKGlCCoZrrnNdoqwf9q1wO1/ZbhwBp1t8c+JmfUsnzS3wFcg==
|
||||
|
||||
agent-base@6, agent-base@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
|
|
@ -7302,6 +7312,11 @@ vm-browserify@^1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
||||
|
||||
vue-class-component@^7.1.0:
|
||||
version "7.2.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4"
|
||||
integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==
|
||||
|
||||
vue-hot-reload-api@^2.3.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||
|
|
@ -7318,6 +7333,13 @@ vue-loader@^15.9.1:
|
|||
vue-hot-reload-api "^2.3.0"
|
||||
vue-style-loader "^4.1.0"
|
||||
|
||||
vue-property-decorator@^8.0.0:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz#571a91cf8d2b507f537d79bf8275af3184572fff"
|
||||
integrity sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q==
|
||||
dependencies:
|
||||
vue-class-component "^7.1.0"
|
||||
|
||||
vue-style-loader@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue