mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-01-26 09:43:29 +08:00
Implement typed label templates [SCI-7046] (#4316)
* Implement typed label templates [SCI-7046] * Add structure.sql, fix migration [SCI-7046] * Fix method name [SCI-7046] * Add dependent option to team - label_template relation [SCI-7046] * Migration code style, add input sanitization [SCI-7046] * Fix new label creation [SCI-7046]
This commit is contained in:
parent
e5448f3d1f
commit
eb619184a5
15 changed files with 254 additions and 41 deletions
6
app/assets/images/label_template_icons/fluics.svg
Normal file
6
app/assets/images/label_template_icons/fluics.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="2" fill="#FFE000"/>
|
||||
<path d="M11.6466 3.72125L14.5012 8.00001L11.6466 12.2788L11.6461 12.2795C11.6008 12.3476 11.5394 12.4034 11.4673 12.4418C11.3953 12.4803 11.3148 12.5003 11.233 12.5H11.2312H2C1.86739 12.5 1.74021 12.4473 1.64645 12.3536C1.55268 12.2598 1.5 12.1326 1.5 12V4.00001C1.5 3.8674 1.55268 3.74022 1.64645 3.64645C1.74021 3.55268 1.86739 3.50001 2 3.50001L11.2312 3.50001L11.233 3.5C11.3148 3.49971 11.3953 3.5197 11.4673 3.55818C11.5394 3.59666 11.6008 3.65243 11.6461 3.72048L11.6466 3.72125Z" stroke="#00A4CE"/>
|
||||
<circle cx="12" cy="8" r="1" fill="#00A4CE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H6H9V6H6V8H8V9H6V11H5V5Z" fill="#363332"/>
|
||||
</svg>
|
After Width: | Height: | Size: 806 B |
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
|
||||
function renderNameHTML(data, type, row) {
|
||||
return `${data.icon_url}<a
|
||||
return `${data.icon_image_tag}<a
|
||||
href='${row.DT_RowAttr['data-edit-url']}'
|
||||
class='record-info-link'
|
||||
onclick='window.open(this.href, "_self")'
|
||||
|
|
|
@ -33,14 +33,7 @@ class LabelTemplatesController < ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
label_template = LabelTemplate.create!(
|
||||
team_id: current_team.id,
|
||||
name: I18n.t('label_templates.new_label_template'),
|
||||
language_type: :zpl,
|
||||
format: 'ZPL',
|
||||
size: '1" x 0.5" / 25.4mm x 12.7mm',
|
||||
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
|
||||
)
|
||||
label_template = ZebraLabelTemplate.default.save!
|
||||
|
||||
redirect_to label_template_path(label_template, new_label: true)
|
||||
end
|
||||
|
@ -82,7 +75,7 @@ class LabelTemplatesController < ApplicationController
|
|||
def set_default
|
||||
ActiveRecord::Base.transaction do
|
||||
LabelTemplate.find_by(team_id: current_team.id,
|
||||
language_type: @label_template.language_type,
|
||||
type: @label_template.type,
|
||||
default: true)&.update!(default: false)
|
||||
@label_template.update!(default: true)
|
||||
render json: { message: I18n.t('label_templates.index.template_set_as_default') }
|
||||
|
|
|
@ -35,8 +35,8 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
{
|
||||
'0' => record.id,
|
||||
'1' => record.default,
|
||||
'2' => append_format_icon(sanitize_input(record.name)),
|
||||
'3' => sanitize_input(record.format),
|
||||
'2' => append_format_icon(record),
|
||||
'3' => sanitize_input(record.label_format),
|
||||
'4' => sanitize_input(record.description),
|
||||
'5' => sanitize_input(record.modified_by),
|
||||
'6' => I18n.l(record.updated_at, format: :full),
|
||||
|
@ -52,9 +52,15 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
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 }
|
||||
def append_format_icon(record)
|
||||
{
|
||||
icon_image_tag:
|
||||
ActionController::Base.helpers.image_tag(
|
||||
"label_template_icons/#{record.icon}.svg",
|
||||
class: 'label-template-icon'
|
||||
),
|
||||
name: sanitize_input(record.name)
|
||||
}
|
||||
end
|
||||
|
||||
def get_raw_records
|
||||
|
@ -67,12 +73,16 @@ class LabelTemplateDatatable < CustomDatatable
|
|||
).select('label_templates.* AS label_templates')
|
||||
.select('creators.full_name AS created_by_user')
|
||||
.select('modifiers.full_name AS modified_by')
|
||||
.select(
|
||||
"('#{Extends::LABEL_TEMPLATE_FORMAT_MAP.to_json}'::jsonb -> label_templates.type)::text "\
|
||||
"AS label_format"
|
||||
)
|
||||
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.name', 'label_templates.label_format', 'label_templates.description',
|
||||
'label_templates.modified_by', 'label_templates.created_by_user'],
|
||||
dt_params.dig(:search, :value)
|
||||
)
|
||||
|
|
32
app/models/fluics_label_template.rb
Normal file
32
app/models/fluics_label_template.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FluicsLabelTemplate < LabelTemplate
|
||||
def self.default
|
||||
FluicsLabelTemplate.new(
|
||||
name: I18n.t('label_templates.default_fluics_name'),
|
||||
width_mm: 25.4,
|
||||
height_mm: 12.7,
|
||||
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
|
||||
)
|
||||
end
|
||||
|
||||
def label_format
|
||||
'Fluics'
|
||||
end
|
||||
|
||||
def created_by_user
|
||||
'Fluics GmbH'
|
||||
end
|
||||
|
||||
def modified_by
|
||||
'Fluics GmbH'
|
||||
end
|
||||
|
||||
def icon
|
||||
'fluics'
|
||||
end
|
||||
|
||||
def read_only?
|
||||
true
|
||||
end
|
||||
end
|
|
@ -3,13 +3,13 @@
|
|||
class LabelTemplate < ApplicationRecord
|
||||
include SearchableModel
|
||||
|
||||
enum language_type: { zpl: 0 }
|
||||
belongs_to :team
|
||||
|
||||
validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH,
|
||||
maximum: Constants::NAME_MAX_LENGTH }
|
||||
validates :size, presence: true
|
||||
validates :content, presence: true
|
||||
|
||||
validate :default_template
|
||||
validate :ensure_single_default_template!
|
||||
|
||||
def self.enabled?
|
||||
ApplicationSettings.instance.values['label_templates_enabled']
|
||||
|
@ -21,18 +21,27 @@ class LabelTemplate < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def icon_url
|
||||
case language_type
|
||||
when 'zpl'
|
||||
'/images/label_template_icons/zpl.svg'
|
||||
end
|
||||
def icon
|
||||
'zpl'
|
||||
end
|
||||
|
||||
def language_type
|
||||
'zpl'
|
||||
end
|
||||
|
||||
def read_only?
|
||||
false
|
||||
end
|
||||
|
||||
def label_format
|
||||
Extends::LABEL_TEMPLATE_FORMAT_MAP[type]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_template
|
||||
if default && LabelTemplate.where(team_id: team_id, default: true, language_type: language_type)
|
||||
.where.not(id: id).any?
|
||||
def ensure_single_default_template!
|
||||
if default && self.class.where(team_id: team_id, default: true, language_type: language_type)
|
||||
.where.not(id: id).any?
|
||||
errors.add(:default, I18n.t('activerecord.errors.models.label_template.attributes.default.already_exist'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ class Team < ApplicationRecord
|
|||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
after_create :generate_template_project
|
||||
after_create :create_default_label_templates
|
||||
scope :teams_select, -> { select(:id, :name).order(name: :asc) }
|
||||
scope :ordered, -> { order('LOWER(name)') }
|
||||
|
||||
|
@ -41,6 +42,7 @@ class Team < ApplicationRecord
|
|||
has_many :assets, inverse_of: :team, dependent: :destroy
|
||||
has_many :team_repositories, inverse_of: :team, dependent: :destroy
|
||||
has_many :shared_repositories, through: :team_repositories, source: :repository
|
||||
has_many :label_templates, dependent: :destroy
|
||||
|
||||
attr_accessor :without_templates
|
||||
|
||||
|
@ -170,4 +172,9 @@ class Team < ApplicationRecord
|
|||
return if without_templates
|
||||
TemplatesService.new.delay(queue: :templates).update_team(self)
|
||||
end
|
||||
|
||||
def create_default_label_templates
|
||||
ZebraLabelTemplate.default.update(team: self, default: true)
|
||||
FluicsLabelTemplate.default.update(team: self, default: true)
|
||||
end
|
||||
end
|
||||
|
|
12
app/models/zebra_label_template.rb
Normal file
12
app/models/zebra_label_template.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ZebraLabelTemplate < LabelTemplate
|
||||
def self.default
|
||||
ZebraLabelTemplate.new(
|
||||
name: I18n.t('label_templates.default_zebra_name'),
|
||||
width_mm: 25.4,
|
||||
height_mm: 12.7,
|
||||
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -5,6 +5,10 @@ class LabelTemplateSerializer < ActiveModel::Serializer
|
|||
|
||||
attributes :name, :description, :language_type, :icon_url, :urls, :content
|
||||
|
||||
def icon_url
|
||||
ActionController::Base.helpers.image_path("label_template_icons/#{object.icon}.svg")
|
||||
end
|
||||
|
||||
def urls
|
||||
{
|
||||
update: label_template_path(object)
|
||||
|
|
50
app/services/label_templates/repository_row_service.rb
Normal file
50
app/services/label_templates/repository_row_service.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module LabelTemplates
|
||||
class RepositoryRowService
|
||||
class UnsupportedKeyError < StandardError; end
|
||||
|
||||
class ColumnNotFoundError < StandardError; end
|
||||
|
||||
def initialize(label_template, repository_row)
|
||||
@label_template = label_template
|
||||
@repository_row = repository_row
|
||||
end
|
||||
|
||||
def render
|
||||
keys = @label_template.content.scan(/(?<=\{\{).*?(?=\}\})/).uniq
|
||||
|
||||
keys.reduce(@label_template.content.dup) do |rendered_content, key|
|
||||
rendered_content.gsub!(/\{\{#{key}\}\}/, fetch_value(key))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_value(key)
|
||||
case key
|
||||
when /^COLUMN_\[(.*)\]/
|
||||
name = Regexp.last_match(1)
|
||||
repository_cell = @repository_row.repository_cells.joins(:repository_column).find_by(
|
||||
repository_columns: { name: name }
|
||||
)
|
||||
|
||||
unless repository_cell
|
||||
raise UnsupportedKeyError, I18n.t('label_templates.repository_row.errors.column_not_found', column: name)
|
||||
end
|
||||
|
||||
repository_cell.value.formatted
|
||||
when 'ITEM_ID'
|
||||
@repository_row.code
|
||||
when 'NAME'
|
||||
@repository_row.name
|
||||
when 'ADDED_BY'
|
||||
@repository_row.created_by.full_name
|
||||
when 'ADDED_ON'
|
||||
@repository_row.created_at.to_s
|
||||
else
|
||||
raise UnsupportedKeyError, I18n.t('label_templates.repository_row.errors.unsupported_key', key: key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -502,12 +502,17 @@ class Extends
|
|||
^LH20,20
|
||||
^PW310
|
||||
^CF0,23
|
||||
^FO0,0^FD{{item_id}}^FS
|
||||
^FO0,20^BQN,2,4^FDMA,{{item_id}}^FS
|
||||
^FO95,30^FB180,4,0,L^FD{{item_name}}^FS^FS
|
||||
^FO0,0^FD{{ITEM_ID}}^FS
|
||||
^FO0,20^BQN,2,4^FDMA,{{ITEM_ID}}^FS
|
||||
^FO95,30^FB180,4,0,L^FD{{NAME}}^FS^FS
|
||||
^XZ
|
||||
HEREDOC
|
||||
}
|
||||
|
||||
LABEL_TEMPLATE_FORMAT_MAP = {
|
||||
'ZebraLabelTemplate' => 'ZPL',
|
||||
'FluicsLabelTemplate' => 'Fluics'
|
||||
}
|
||||
end
|
||||
|
||||
# rubocop:enable Style/MutableConstant
|
||||
|
|
|
@ -802,6 +802,12 @@ en:
|
|||
normal_user: "Normal user"
|
||||
admin: "Administrator"
|
||||
|
||||
label_templates:
|
||||
repository_row:
|
||||
errors:
|
||||
unsupported_key: "Key %{key} is not supported by this label template renderer."
|
||||
column_not_found: "Column '%{column}' not found on inventory item."
|
||||
|
||||
user_projects:
|
||||
view_users:
|
||||
modal_title: "Members of %{name}"
|
||||
|
@ -815,6 +821,8 @@ en:
|
|||
can_add_user_to_project: "Can not add user to the project."
|
||||
|
||||
label_templates:
|
||||
default_zebra_name: 'SciNote Item (ZPL)'
|
||||
default_fluics_name: 'SciNote Item (Fluics)'
|
||||
new_label_template: 'New label'
|
||||
index:
|
||||
head_title: 'Label templates'
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
class FixLabelTemplate < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
LabelTemplate.last.update(
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
LabelTemplate.last.update_columns(
|
||||
name: 'SciNote Item',
|
||||
size: '1" x 0.5" / 25.4mm x 12.7mm',
|
||||
language_type: :zpl,
|
||||
|
@ -23,5 +24,6 @@ class FixLabelTemplate < ActiveRecord::Migration[6.1]
|
|||
^XZ
|
||||
HEREDOC
|
||||
)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
|
30
db/migrate/20220803122405_update_label_templates.rb
Normal file
30
db/migrate/20220803122405_update_label_templates.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateLabelTemplates < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
change_table :label_templates, bulk: true do |t|
|
||||
t.string :type
|
||||
t.float :width_mm
|
||||
t.float :height_mm
|
||||
t.remove :format
|
||||
t.remove :language_type
|
||||
t.remove :size
|
||||
end
|
||||
|
||||
# Remove our original default template
|
||||
LabelTemplate.order(created_at: :asc).find_by(default: true)&.destroy
|
||||
|
||||
Team.find_each { |t| t.__send__(:create_default_label_templates) }
|
||||
end
|
||||
|
||||
def down
|
||||
change_table :label_templates, bulk: true do |t|
|
||||
t.remove :type
|
||||
t.remove :width_mm
|
||||
t.remoe :height_mm
|
||||
t.string :format, null: false, default: 'ZPL'
|
||||
t.integer :language_type
|
||||
t.string :size
|
||||
end
|
||||
end
|
||||
end
|
|
@ -698,11 +698,16 @@ CREATE TABLE public.label_templates (
|
|||
id bigint NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
content text NOT NULL,
|
||||
language_type integer,
|
||||
"default" boolean DEFAULT false NOT NULL,
|
||||
size character varying,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
description character varying,
|
||||
last_modified_by_id integer,
|
||||
created_by_id integer,
|
||||
team_id bigint,
|
||||
type character varying,
|
||||
width_mm double precision,
|
||||
height_mm double precision
|
||||
);
|
||||
|
||||
|
||||
|
@ -5165,10 +5170,24 @@ CREATE INDEX index_hidden_repository_cell_reminders_on_user_id ON public.hidden_
|
|||
|
||||
|
||||
--
|
||||
-- Name: index_label_templates_on_language_type; Type: INDEX; Schema: public; Owner: -
|
||||
-- Name: index_label_templates_on_created_by_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_label_templates_on_language_type ON public.label_templates USING btree (language_type);
|
||||
CREATE INDEX index_label_templates_on_created_by_id ON public.label_templates USING btree (created_by_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_label_templates_on_last_modified_by_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_label_templates_on_last_modified_by_id ON public.label_templates USING btree (last_modified_by_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_label_templates_on_team_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_label_templates_on_team_id ON public.label_templates USING btree (team_id);
|
||||
|
||||
|
||||
--
|
||||
|
@ -5816,10 +5835,10 @@ CREATE INDEX index_repository_cells_on_repository_row_id ON public.repository_ce
|
|||
|
||||
|
||||
--
|
||||
-- Name: index_repository_cells_on_value; Type: INDEX; Schema: public; Owner: -
|
||||
-- Name: index_repository_cells_on_value_type_and_value_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_repository_cells_on_value ON public.repository_cells USING btree (value_type, value_id);
|
||||
CREATE INDEX index_repository_cells_on_value_type_and_value_id ON public.repository_cells USING btree (value_type, value_id);
|
||||
|
||||
|
||||
--
|
||||
|
@ -6845,10 +6864,10 @@ CREATE INDEX index_view_states_on_user_id ON public.view_states USING btree (use
|
|||
|
||||
|
||||
--
|
||||
-- Name: index_view_states_on_viewable; Type: INDEX; Schema: public; Owner: -
|
||||
-- Name: index_view_states_on_viewable_type_and_viewable_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_view_states_on_viewable ON public.view_states USING btree (viewable_type, viewable_id);
|
||||
CREATE INDEX index_view_states_on_viewable_type_and_viewable_id ON public.view_states USING btree (viewable_type, viewable_id);
|
||||
|
||||
|
||||
--
|
||||
|
@ -6943,6 +6962,14 @@ ALTER TABLE ONLY public.assets
|
|||
ADD CONSTRAINT fk_rails_0916329f9e FOREIGN KEY (created_by_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: label_templates fk_rails_09d7cc0c34; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.label_templates
|
||||
ADD CONSTRAINT fk_rails_09d7cc0c34 FOREIGN KEY (team_id) REFERENCES public.teams(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_assignments fk_rails_0b13c65ab0; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -7007,6 +7034,14 @@ ALTER TABLE ONLY public.user_assignments
|
|||
ADD CONSTRAINT fk_rails_19dca62dfc FOREIGN KEY (user_role_id) REFERENCES public.user_roles(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: label_templates fk_rails_1aa41d1093; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.label_templates
|
||||
ADD CONSTRAINT fk_rails_1aa41d1093 FOREIGN KEY (created_by_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_system_notifications fk_rails_20d9487a3c; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -8087,6 +8122,14 @@ ALTER TABLE ONLY public.experiments
|
|||
ADD CONSTRAINT fk_rails_d683124fa4 FOREIGN KEY (restored_by_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: label_templates fk_rails_d6ac71e421; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.label_templates
|
||||
ADD CONSTRAINT fk_rails_d6ac71e421 FOREIGN KEY (last_modified_by_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: protocols fk_rails_d8007e2f63; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -8516,6 +8559,8 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20220429083335'),
|
||||
('20220530144300'),
|
||||
('20220602120714'),
|
||||
('20220705091621');
|
||||
('20220705091621'),
|
||||
('20220726133419'),
|
||||
('20220803122405');
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue